geneseed 3.19.0 → 4.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 +41 -0
- package/README.md +9 -1
- package/SHIPPED.md +1 -1
- package/adapters/bob/README.md +31 -12
- package/adapters/claude-code/README.md +24 -17
- package/adapters/claude-code/settings.json +4 -6
- package/adapters/openclaude/README.md +13 -5
- package/adapters/opencode/plugins/geneseed-guard.js +291 -174
- package/bin/geneseed-hook.mjs +36 -5
- package/docs/README.md +3 -1
- package/docs/candidate-hosts.md +0 -8
- package/docs/concepts/enforcement-table.md +66 -0
- package/docs/concepts/hook-gates.md +2 -2
- package/docs/concepts/hooks.md +6 -6
- package/docs/concepts/host-matrix.md +4 -3
- package/docs/concepts/loop-branch.md +6 -1
- package/docs/concepts/loops.md +4 -3
- package/docs/concepts/rules.md +1 -1
- package/docs/design-history.md +59 -0
- package/docs/extending.md +11 -3
- package/docs/guides/install-opencode.md +1 -1
- package/docs/guides/loop-running.md +1 -1
- package/docs/reference/env-context.md +3 -2
- package/docs/reference/environment.md +2 -1
- package/docs/reference/plugin-guard.md +3 -3
- package/docs/reference/troubleshoot-hooks.md +34 -0
- package/docs/understand/design-choices-engineering.md +29 -0
- package/docs/understand/design-choices-gates.md +23 -0
- package/docs/understand/design-choices-hosts.md +21 -0
- package/docs/understand/design-choices-install.md +19 -0
- package/docs/understand/design-choices-shape.md +21 -0
- package/docs/understand/design-choices.md +34 -0
- package/docs/understand/enforced-vs-asked.md +5 -4
- package/docs/understand/machine-bob.md +1 -1
- package/docs/understand/machine-opencode.md +2 -2
- package/harness.config.json +1 -1
- package/js/build/driver.mjs +42 -16
- package/js/build/emit-claude.mjs +24 -8
- package/js/build/emit-opencode.mjs +33 -15
- package/js/build/generate.mjs +38 -9
- package/js/build/source.mjs +39 -0
- package/js/build/stubs.mjs +2 -1
- package/js/cli-table.json +6 -6
- package/js/hosts/gitref.mjs +3 -3
- package/js/hosts/hooks-context.mjs +13 -31
- package/js/hosts/hooks-learn.mjs +89 -8
- package/js/hosts/hooks.mjs +212 -18
- package/js/hosts/hosts.mjs +217 -13
- package/js/hosts/installs.mjs +259 -46
- package/js/hosts/native.mjs +48 -12
- package/js/hosts/opencode.mjs +47 -17
- package/js/hosts/settings.mjs +366 -42
- package/js/hosts/shim.mjs +37 -0
- package/js/inspect/checks-authoring.mjs +81 -1
- package/js/inspect/checks-constitution.mjs +2 -1
- package/js/inspect/checks-repo.mjs +68 -4
- package/js/inspect/doctor.mjs +40 -9
- package/js/inspect/scan.mjs +5 -4
- package/js/inspect/status.mjs +62 -12
- package/js/lib/text.mjs +23 -0
- package/js/loop/catalog.mjs +7 -2
- package/js/loop/state.mjs +2 -2
- package/js/maintain/migrate.mjs +42 -5
- package/js/maintain/uninstall.mjs +150 -38
- package/js/maintain/update.mjs +22 -2
- package/js/web/actions.mjs +28 -10
- package/js/web/api.mjs +5 -3
- package/js/web/catalog.mjs +1 -1
- package/js/web/state.mjs +28 -15
- package/package.json +1 -1
- package/src/agents/_template.md +1 -1
- package/src/bricks/_template.md +1 -1
- package/src/bricks/apply.md +1 -1
- package/src/bricks/test.md +1 -1
- package/src/doctrines/ops.md +9 -7
- package/src/doctrines/process.md +3 -3
- package/src/laws/universal.md +1 -1
- package/src/loops/api-endpoint.json +1 -1
- package/src/loops/bugfix.json +1 -1
- package/src/loops/ci-repair.json +1 -1
- package/src/loops/deps-upgrade.json +1 -1
- package/src/loops/enforce-architecture-rule.json +1 -1
- package/src/loops/feature.json +1 -1
- package/src/loops/fix-flaky-tests.json +1 -1
- package/src/loops/legacy-refactor.json +1 -1
- package/src/loops/legacy-tests.json +1 -1
- package/src/loops/refactor.json +1 -1
- package/src/loops/remove-dead-code.json +1 -1
- package/src/loops/spec-first-feature.json +1 -1
- package/src/loops/tdd.json +1 -1
- package/src/loops/update-contract.json +1 -1
- package/src/skills/_template.md +3 -3
- package/src/skills/herdr.md +1 -1
- package/web/dist/assets/{Activity-IbwdHHyx.js → Activity-BCtqxjfy.js} +1 -1
- package/web/dist/assets/{ActivityDetail-B5uivdWI.js → ActivityDetail-Ba3Oo4dE.js} +1 -1
- package/web/dist/assets/{Dashboard-v6WQ37bD.js → Dashboard-DpHza68-.js} +2 -2
- package/web/dist/assets/{Docs-CypIfDBX.js → Docs-Bw_-UK0O.js} +1 -1
- package/web/dist/assets/{GreenhouseView-ClxP0hQG.js → GreenhouseView-DnJ1Ggp1.js} +1 -1
- package/web/dist/assets/{Installs-CPkQeFjO.js → Installs-CWoNKFBk.js} +1 -1
- package/web/dist/assets/{JournalView-BeEInhUf.js → JournalView-g6-bw4c8.js} +1 -1
- package/web/dist/assets/{Laws-Ds6U5xBQ.js → Laws-DkVWR1tb.js} +1 -1
- package/web/dist/assets/{Library-BraUKPyQ.js → Library-CK9HZzjV.js} +1 -1
- package/web/dist/assets/{Loops-DWcy3mwK.js → Loops-BP98SMOH.js} +1 -1
- package/web/dist/assets/{OperatorHudView-CQyrJQ9m.js → OperatorHudView-DI_n12M9.js} +1 -1
- package/web/dist/assets/{Personal-YWqs6qFL.js → Personal-Bk89f13a.js} +1 -1
- package/web/dist/assets/{Tabs-EhzEC-Xh.js → Tabs-3WuYIE1b.js} +1 -1
- package/web/dist/assets/{drawable-CEKPNV7X.js → drawable-oCUeq9e5.js} +1 -1
- package/web/dist/assets/{index-CPi22b7y.js → index-CiXwChvf.js} +2 -2
- package/web/dist/index.html +1 -1
|
@@ -50,18 +50,20 @@ import path from 'node:path';
|
|
|
50
50
|
import { hookRunnerEntry } from '../hosts/shim.mjs';
|
|
51
51
|
import { confirm } from '../lib/prompt.mjs';
|
|
52
52
|
import {
|
|
53
|
-
claudeCfg, claudeReadManifest, doctrinesOfDir,
|
|
54
|
-
installState,
|
|
53
|
+
claudeCfg, claudeReadManifest, doctrinesOfDir, EMIT_HOST_SCOPE, emitHostScopeOf,
|
|
54
|
+
excludedRulesOfDir, installKind, installState,
|
|
55
55
|
registeredTargets, DISABLED_STASH,
|
|
56
56
|
} from '../hosts/installs.mjs';
|
|
57
57
|
import {
|
|
58
|
-
CLAUDE_STYLE, GLOBAL_MANIFEST, HOSTS, VERSION_MARKER, expanduser,
|
|
58
|
+
CLAUDE_STYLE, GLOBAL_MANIFEST, HOSTS, VERSION_MARKER, expanduser, isHostGlobalDir, opencodeConfigDir,
|
|
59
|
+
resolvePath, settingsFile,
|
|
59
60
|
} from '../hosts/hosts.mjs';
|
|
60
61
|
import { mcpCommented, mcpLoad } from '../hosts/mcp.mjs';
|
|
61
62
|
import {
|
|
62
63
|
atomicWriteJson, managedBlockRead, managedBlockRemove, managedBlockWrite,
|
|
63
|
-
loadJsonObject, mergeClaudeSettings, opencodeTarget,
|
|
64
|
-
settingsIntegrityCheck, wireClaudeExcludes, unwireClaudeExcludes,
|
|
64
|
+
loadJsonObject, mergeClaudeSettings, opencodeTarget, OPENCODE_SENTINEL, opencodeSentinelWrite,
|
|
65
|
+
settingsIntegrityCheck, wireClaudeExcludes, unwireClaudeExcludes,
|
|
66
|
+
unwireClaudeSettings,
|
|
65
67
|
} from '../hosts/settings.mjs';
|
|
66
68
|
import { printOut, printErr, readText, writeText, isFile, isDir, isOsError } from '../lib/fs.mjs';
|
|
67
69
|
import { indexOfDeepEqual, isDict, jsonDumps, deepEquals } from '../lib/json.mjs';
|
|
@@ -212,27 +214,63 @@ function warnMarkersKept() {
|
|
|
212
214
|
const REVERSAL_MARKERS = [GLOBAL_MANIFEST, '.geneseed-theme', '.geneseed-emit',
|
|
213
215
|
'.geneseed-footprint', VERSION_MARKER];
|
|
214
216
|
|
|
217
|
+
/**
|
|
218
|
+
* The CURRENT manifest's `skill_denies` — the `permission.skill` denies Geneseed itself added
|
|
219
|
+
* (`opencodeLayer` in emit-opencode.mjs records them; a deny the user wrote, even on a Geneseed
|
|
220
|
+
* skill, is never in it). Task 4 (O-1) wires one per user-only skill, and unlike the Law IV
|
|
221
|
+
* `permission.bash` gates (permanent invariants, never taken back — see
|
|
222
|
+
* `reconcileOpencodePermission`), a skill-visibility deny is reversible: once uninstalled there
|
|
223
|
+
* is no skill left to hide, and the name is the user's to reuse. No record takes back nothing.
|
|
224
|
+
*/
|
|
225
|
+
function skillPermissionNames(man) {
|
|
226
|
+
return Array.isArray(man.skill_denies) ? man.skill_denies.filter((n) => typeof n === 'string') : [];
|
|
227
|
+
}
|
|
228
|
+
|
|
215
229
|
/**
|
|
216
230
|
* `_harness_mcp._unmerge_opencode_json` — drop one `instructions` entry, leave every other
|
|
217
231
|
* key intact.
|
|
218
232
|
*
|
|
219
|
-
*
|
|
233
|
+
* `denyNames`, Task 4 (O-1): the denies this install's manifest records writing
|
|
234
|
+
* (`skill_denies`) — remove `permission.skill[name]` for each one, but ONLY where the value is
|
|
235
|
+
* still exactly `"deny"`, and drop the `skill` object once it is empty. Ownership comes from the
|
|
236
|
+
* record, not the value; the value check on top guards a user who re-scoped a recorded name to
|
|
237
|
+
* their own `"allow"`/`"ask"`. A
|
|
238
|
+
* caller passing `[]` (every non-uninstall unwire — `installDeactivate`, same as
|
|
239
|
+
* `permission.bash`) leaves `permission.skill` untouched, so a paused install keeps the model
|
|
240
|
+
* blind to its skills rather than re-exposing them until the next `geneseed build`.
|
|
241
|
+
*
|
|
242
|
+
* The sweep runs even when the `instructions` entry is already gone — an uninstall after a
|
|
243
|
+
* deactivate, which removed the entry but kept the denies (PR #241 review) — so either half
|
|
244
|
+
* alone is a change, and only "neither" declines.
|
|
245
|
+
*
|
|
246
|
+
* A COMMENTED `.jsonc` is not rewritten and the user is told to do it by hand — the entry AND
|
|
247
|
+
* the recorded denies still set to `"deny"`, each named: rewriting it
|
|
220
248
|
* would drop the comments. That branch returns False, so the caller's `unmerged` reports
|
|
221
249
|
* REALITY rather than intent.
|
|
222
250
|
*/
|
|
223
|
-
export function unmergeOpencodeJson(p, entry) {
|
|
251
|
+
export function unmergeOpencodeJson(p, entry, denyNames = []) {
|
|
224
252
|
const target = opencodeTarget(p);
|
|
225
253
|
// Absent, unreadable (the Python's `except OSError: return False`) or not an object: decline.
|
|
226
254
|
const { state, data: cfg, hadComments } = loadJsonObject(target);
|
|
227
255
|
if (state !== 'ok') return false;
|
|
228
256
|
const instr = cfg.instructions;
|
|
229
|
-
|
|
257
|
+
const hasEntry = Array.isArray(instr) && instr.includes(entry);
|
|
258
|
+
// The deny sweep does NOT hang off the entry: `installDeactivate` already dropped the entry
|
|
259
|
+
// (and kept the denies), so an uninstall after a deactivate finds no entry but still owns them.
|
|
260
|
+
const skill = isDict(cfg.permission) && isDict(cfg.permission.skill) ? cfg.permission.skill : null;
|
|
261
|
+
const denies = skill ? denyNames.filter((n) => skill[n] === 'deny') : [];
|
|
262
|
+
if (!hasEntry && !denies.length) return false;
|
|
230
263
|
if (path.extname(target) === '.jsonc' && hadComments) {
|
|
231
|
-
printOut(`[uninstall] ${path.basename(target)} has comments — not rewriting it
|
|
232
|
-
+ `this from its "instructions" by hand: ${jsonDumps(entry)}
|
|
264
|
+
printOut(`[uninstall] ${path.basename(target)} has comments — not rewriting it.`
|
|
265
|
+
+ (hasEntry ? ` Remove this from its "instructions" by hand: ${jsonDumps(entry)}` : '')
|
|
266
|
+
+ (denies.length ? ` Remove these from its "permission.skill" by hand: `
|
|
267
|
+
+ `${denies.map((n) => jsonDumps(n)).join(', ')}` : '')
|
|
268
|
+
+ '\n');
|
|
233
269
|
return false;
|
|
234
270
|
}
|
|
235
|
-
cfg.instructions = instr.filter((i) => i !== entry);
|
|
271
|
+
if (hasEntry) cfg.instructions = instr.filter((i) => i !== entry);
|
|
272
|
+
for (const name of denies) delete skill[name];
|
|
273
|
+
if (skill && denies.length && !Object.keys(skill).length) delete cfg.permission.skill;
|
|
236
274
|
atomicWriteJson(target, cfg);
|
|
237
275
|
return true;
|
|
238
276
|
}
|
|
@@ -265,16 +303,9 @@ export function archiveStore(store) {
|
|
|
265
303
|
return dest;
|
|
266
304
|
}
|
|
267
305
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
* `settings.local.json` for a Claude PROJECT install (personal, untracked), `settings.json`
|
|
272
|
-
* everywhere else, and the manifest is the authority. Every lifecycle path must target the
|
|
273
|
-
* file the EMIT wrote, or the hooks linger in one file while the claims chase another.
|
|
274
|
-
*/
|
|
275
|
-
function settingsFile(cfg, managed) {
|
|
276
|
-
return path.join(cfg, (managed && managed.settings_file) || 'settings.json');
|
|
277
|
-
}
|
|
306
|
+
// `settingsFile` lives in `hosts.mjs` (the hook stand-down reads it, and must not import this
|
|
307
|
+
// module); re-exported here for `migrate`.
|
|
308
|
+
export { settingsFile };
|
|
278
309
|
|
|
279
310
|
/** `_harness_mcp._claude_md_path` — where the manifest says the managed block lives. */
|
|
280
311
|
function claudeMdPath(cfg, managed) {
|
|
@@ -343,7 +374,8 @@ export function uninstallGlobal(target, archiveMemory, host = 'opencode') {
|
|
|
343
374
|
if (isDir(p) && isEmptyDir(p)) rmdirQuiet(p);
|
|
344
375
|
}
|
|
345
376
|
const unmerged = unmergeOpencodeJson(path.join(target, 'opencode.json'),
|
|
346
|
-
path.join(target, 'AGENT.md').split(path.sep).join('/'));
|
|
377
|
+
path.join(target, 'AGENT.md').split(path.sep).join('/'), skillPermissionNames(man));
|
|
378
|
+
managedBlockRemove(path.join(target, OPENCODE_SENTINEL));
|
|
347
379
|
if (failed.length) warnMarkersKept();
|
|
348
380
|
else for (const m of REVERSAL_MARKERS) unlinkQuiet(path.join(target, m));
|
|
349
381
|
let archived = null;
|
|
@@ -403,10 +435,11 @@ function opencodeProjectUninstall(root) {
|
|
|
403
435
|
const entry = installAgentEntry(root, 'project');
|
|
404
436
|
let removed = 0;
|
|
405
437
|
let failed = [];
|
|
438
|
+
let man = null;
|
|
406
439
|
const oc = path.join(root, '.opencode');
|
|
407
440
|
const manifestPath = path.join(oc, GLOBAL_MANIFEST);
|
|
408
441
|
if (isFile(manifestPath)) {
|
|
409
|
-
|
|
442
|
+
man = claudeReadManifest(oc);
|
|
410
443
|
[removed, failed] = unlinkOwned(oc, ownedOf(man), '.opencode/');
|
|
411
444
|
if (failed.length) warnSurvivors(failed);
|
|
412
445
|
// Survivors gate, mirroring the other two reversals: `_project_qualifies` keys off the
|
|
@@ -427,7 +460,8 @@ function opencodeProjectUninstall(root) {
|
|
|
427
460
|
}
|
|
428
461
|
const am = path.join(root, 'AGENT.md');
|
|
429
462
|
if (isFile(am)) { unlinkQuiet(am); removed += 1; }
|
|
430
|
-
const unmerged = unmergeOpencodeJson(path.join(root, 'opencode.json'), entry
|
|
463
|
+
const unmerged = unmergeOpencodeJson(path.join(root, 'opencode.json'), entry,
|
|
464
|
+
man ? skillPermissionNames(man) : []);
|
|
431
465
|
const result = { removed, unmerged, archived: null };
|
|
432
466
|
if (failed.length) result.failed = failed;
|
|
433
467
|
return result;
|
|
@@ -519,10 +553,75 @@ export function installUninstall(root, host = 'opencode', scope = 'global', memo
|
|
|
519
553
|
if (archived.length) out.archived = archived;
|
|
520
554
|
return out;
|
|
521
555
|
}
|
|
522
|
-
|
|
523
|
-
|
|
556
|
+
// The root markers are ONE PER ROOT (`generate.mjs`'s "THE MARKER IS TRUSTED ONLY FOR ITS
|
|
557
|
+
// OWN HOST") and `registryRoots` keeps a row alive only while `.geneseed-emit` exists there
|
|
558
|
+
// — so in a repo sharing two project installs (`.claude/` + `.opencode/` at the same cwd,
|
|
559
|
+
// last deploy wins the marker), deleting it unconditionally because THIS host is being
|
|
560
|
+
// uninstalled would silently deregister the OTHER host's still-live install, regardless of
|
|
561
|
+
// which host's name the marker happened to carry.
|
|
562
|
+
//
|
|
563
|
+
// Delete every marker ONLY when nothing else is installed here. Otherwise:
|
|
564
|
+
// - `.geneseed-emit` carries HOST IDENTITY (`EMIT_HOST_SCOPE`'s key names a (host, scope)
|
|
565
|
+
// pair) and `installProfile`/`migrateSurvey` trust it only for the host it names
|
|
566
|
+
// (`generate.mjs`'s own docblock). If it currently names the host just removed, REWRITE
|
|
567
|
+
// it to the survivor's own emit name — the same string `writeMarkers` (`driver.mjs`)
|
|
568
|
+
// would have written for that survivor — rather than deleting it and leaving the
|
|
569
|
+
// registry row to self-prune a live install. If it already names a survivor, it is
|
|
570
|
+
// already correct; leave it untouched.
|
|
571
|
+
// - `.geneseed-theme` and `VERSION_MARKER` carry NO host identity — a bare theme name and
|
|
572
|
+
// a version/fingerprint string — so there is no survivor value to rewrite them TO; the
|
|
573
|
+
// only question is whether they are THIS host's own litter to clear. At PROJECT scope
|
|
574
|
+
// `.geneseed-theme` here is OpenCode's alone: `driver.mjs`'s own comment says the
|
|
575
|
+
// claude/bob/openclaude project emits never write it, and `themeOfDir`'s host-narrowing
|
|
576
|
+
// (host-compat B1 round 3, `js/hosts/installs.mjs`) means no Claude-style sibling ever
|
|
577
|
+
// reads it either — so delete it only when OpenCode is the host being removed, and
|
|
578
|
+
// leave it for a Claude-style uninstall (it belongs to a surviving or absent OpenCode
|
|
579
|
+
// install either way, never to the host that just left). `VERSION_MARKER` is written at
|
|
580
|
+
// `cfgDir`, never bare `root`, by every PROJECT emit (`js/build/version.mjs`'s callers),
|
|
581
|
+
// so a project root never actually carries this file regardless of host — deleting it
|
|
582
|
+
// here is always a no-op there. At GLOBAL scope neither file is ever shared (each host
|
|
583
|
+
// owns its own config dir), so both are always this host's own and always safe to drop.
|
|
584
|
+
// PROJECT scope uses `projectQualifies` (the same predicate `uninstallResolve` already
|
|
585
|
+
// trusts for exactly this question), never `installState`: `installState`'s OpenCode
|
|
586
|
+
// branch answers 'active' off a bare `.opencode/` DIRECTORY's existence alone, with no
|
|
587
|
+
// manifest or marker check, so an unrelated non-Geneseed `.opencode/` sitting next to a
|
|
588
|
+
// real Claude install would read as a "surviving" OpenCode install and keep the marker
|
|
589
|
+
// alive for a host that was never actually here.
|
|
590
|
+
const survivor = HOSTS.find(({ host: h }) => h !== host && (
|
|
591
|
+
scope === 'project'
|
|
592
|
+
? projectQualifies(root, h)
|
|
593
|
+
// GLOBAL: each host owns a distinct config dir, so no two hosts' global installs ever
|
|
594
|
+
// share a root — this can only match THIS host's own global install, and by the time
|
|
595
|
+
// execution reaches here (step 3) step 1 has already unlinked THIS host's own
|
|
596
|
+
// manifest/markers, so the lookback never mistakes its own just-removed install for a
|
|
597
|
+
// surviving sibling.
|
|
598
|
+
: installState(root, h, scope) !== 'absent'
|
|
599
|
+
));
|
|
600
|
+
if (!survivor) {
|
|
601
|
+
for (const m of ['.geneseed-emit', '.geneseed-theme', VERSION_MARKER]) {
|
|
602
|
+
unlinkQuiet(path.join(root, m));
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
const markerScope = emitHostScopeOf(root);
|
|
606
|
+
if (markerScope !== null && markerScope[0] === host) {
|
|
607
|
+
const survivorEmit = [...EMIT_HOST_SCOPE.entries()]
|
|
608
|
+
.find(([, hs]) => hs[0] === survivor.host && hs[1] === scope)?.[0];
|
|
609
|
+
if (survivorEmit) {
|
|
610
|
+
writeText(path.join(root, '.geneseed-emit'), `${survivorEmit}\n`);
|
|
611
|
+
} else {
|
|
612
|
+
// Defensive: every (host, scope) pair in `HOSTS` has an `EMIT_HOST_SCOPE` entry, so
|
|
613
|
+
// this should be unreachable — but silently leaving a marker naming the just-removed
|
|
614
|
+
// host would deregister the survivor exactly as before, so it is loud instead of quiet.
|
|
615
|
+
printErr(`[uninstall] WARN: could not resolve an emit name for the surviving `
|
|
616
|
+
+ `${survivor.host}:${scope} install here — the shared .geneseed-emit marker still `
|
|
617
|
+
+ `names ${host}, which was just removed. Re-run \`geneseed rebuild-all\` or `
|
|
618
|
+
+ `re-emit ${survivor.host} to refresh it.\n`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (scope === 'global' || host === 'opencode') unlinkQuiet(path.join(root, '.geneseed-theme'));
|
|
622
|
+
unlinkQuiet(path.join(root, VERSION_MARKER));
|
|
524
623
|
}
|
|
525
|
-
// 4. Tidy an emptied marker dir (.claude/.bob) so no husk lingers in the repo.
|
|
624
|
+
// 4. Tidy an emptied marker dir (.claude/.bob/.openclaude) so no husk lingers in the repo.
|
|
526
625
|
if (data !== root && isDir(data) && isEmptyDir(data)) rmdirQuiet(data);
|
|
527
626
|
const out = { ok: true, removed: summary.removed ?? 0, memory };
|
|
528
627
|
if (archived.length) out.archived = archived;
|
|
@@ -532,8 +631,9 @@ export function installUninstall(root, host = 'opencode', scope = 'global', memo
|
|
|
532
631
|
/**
|
|
533
632
|
* `_harness_mcp._project_qualifies` — does `root` carry a REAL Geneseed project install?
|
|
534
633
|
*
|
|
535
|
-
* The marker dir exists, is not the host's global
|
|
536
|
-
* `installTargets` aliasing guard
|
|
634
|
+
* The marker dir exists, is not the host's global seen from its parent (`isHostGlobalDir`, the
|
|
635
|
+
* `installTargets` aliasing guard — the default `~/.claude` too, under `$CLAUDE_CONFIG_DIR`),
|
|
636
|
+
* and shows Geneseed's own tracks: the manifest, or the
|
|
537
637
|
* root `.geneseed-emit` naming this host's project emit for a pre-manifest legacy install.
|
|
538
638
|
* A bare non-Geneseed `.claude/` is very common and must never hijack the resolve.
|
|
539
639
|
*/
|
|
@@ -542,7 +642,7 @@ export function projectQualifies(root, host) {
|
|
|
542
642
|
const cfg = path.join(root, spec.projectMarker);
|
|
543
643
|
if (!isDir(cfg)) return false;
|
|
544
644
|
try {
|
|
545
|
-
if (
|
|
645
|
+
if (isHostGlobalDir(host, cfg)) return false;
|
|
546
646
|
} catch { /* as the Python's bare `except Exception: pass` */ }
|
|
547
647
|
if (isFile(path.join(cfg, GLOBAL_MANIFEST))) return true;
|
|
548
648
|
const hs = emitHostScopeOf(root);
|
|
@@ -556,7 +656,8 @@ export function projectQualifies(root, host) {
|
|
|
556
656
|
* is NAMED like a project marker and is the global install, never `claude:project` rooted at
|
|
557
657
|
* `$HOME`, so the global-config-dir case is checked before the marker-name case.
|
|
558
658
|
*
|
|
559
|
-
* 1. `--target` IS a host's global
|
|
659
|
+
* 1. `--target` IS a host's global (`isHostGlobalDir`: its env-resolved dir, its default
|
|
660
|
+
* `~/<marker>`, or a dir whose emit marker names that host's `-global` emit).
|
|
560
661
|
* 2. `--target` IS a project marker dir itself (…/.claude) — root is its parent.
|
|
561
662
|
* 3. `--target` (as a root) carries a Geneseed project install.
|
|
562
663
|
* 4. `--target` given and unrecognised — null, and the caller reports it.
|
|
@@ -566,7 +667,7 @@ export function uninstallResolve(targetArg) {
|
|
|
566
667
|
const globalHit = (p) => {
|
|
567
668
|
for (const spec of HOSTS) {
|
|
568
669
|
try {
|
|
569
|
-
if (
|
|
670
|
+
if (isHostGlobalDir(spec.host, p)) return [spec.host, 'global', p];
|
|
570
671
|
} catch { continue; }
|
|
571
672
|
}
|
|
572
673
|
return null;
|
|
@@ -673,12 +774,17 @@ export function cmdUninstall(args) {
|
|
|
673
774
|
const stores = ['memory', 'notebook'].filter((n) => isDir(path.join(data, n)));
|
|
674
775
|
printOut(`[uninstall] target: ${root} (${host}:${scope})\n`);
|
|
675
776
|
if (CLAUDE_STYLE.includes(host)) {
|
|
777
|
+
// Not always settings.json: a Claude/OpenClaude PROJECT install wires settings.local.json
|
|
778
|
+
// (host-compat B4) — read back from the manifest `settingsFile` already resolves, rather
|
|
779
|
+
// than restate the rule here a second time.
|
|
780
|
+
const cfgForSf = claudeCfg(root, scope, host);
|
|
781
|
+
const sfName = path.basename(settingsFile(cfgForSf, managedOf(claudeReadManifest(cfgForSf))));
|
|
676
782
|
printOut('[uninstall] removes: agents/, skills/, markers, the '
|
|
677
783
|
+ `${hostSpec(host).agentFile} managed block, and Geneseed's `
|
|
678
|
-
+
|
|
784
|
+
+ `${sfName} hooks/excludes (your own keys/hooks are kept).\n`);
|
|
679
785
|
} else if (scope === 'global') {
|
|
680
|
-
printOut('[uninstall] removes: AGENT.md,
|
|
681
|
-
+ 'opencode.json instructions entry.\n');
|
|
786
|
+
printOut('[uninstall] removes: AGENT.md, the AGENTS.md managed block, agents/, skills/, '
|
|
787
|
+
+ 'plugins/, markers, and the opencode.json instructions entry.\n');
|
|
682
788
|
} else {
|
|
683
789
|
printOut('[uninstall] removes: AGENT.md, .opencode/, laws/, agents/, skills/, and the '
|
|
684
790
|
+ 'opencode.json instructions entry.\n');
|
|
@@ -949,6 +1055,8 @@ export function installDeactivate(root, host = 'opencode', scope = 'global') {
|
|
|
949
1055
|
return { ok: false, failed, rolled_back: done.length };
|
|
950
1056
|
}
|
|
951
1057
|
unmergeOpencodeJson(path.join(root, 'opencode.json'), installAgentEntry(root, kind));
|
|
1058
|
+
// The sentinel goes too, or a disabled install keeps the user's ~/.claude/CLAUDE.md from loading.
|
|
1059
|
+
if (kind === 'global') managedBlockRemove(path.join(root, OPENCODE_SENTINEL));
|
|
952
1060
|
for (const rel of done) pruneAncestors(path.dirname(path.join(root, rel)), root);
|
|
953
1061
|
return { ok: true, kind, moved: done.length };
|
|
954
1062
|
}
|
|
@@ -975,6 +1083,7 @@ export function installReactivate(root, host = 'opencode', scope = 'global') {
|
|
|
975
1083
|
if (leftovers.length) return { ok: false, failed: leftovers, moved };
|
|
976
1084
|
const kind = installKind(root) || 'global';
|
|
977
1085
|
installReaddEntry(target, installAgentEntry(root, kind));
|
|
1086
|
+
if (kind === 'global') opencodeSentinelWrite(root);
|
|
978
1087
|
rmtreeQuiet(stash);
|
|
979
1088
|
return { ok: true, kind, moved };
|
|
980
1089
|
}
|
|
@@ -1026,11 +1135,14 @@ function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
|
|
|
1026
1135
|
// unconditionally (fail-closed, so not a hole — but wrong, and it made the toggle one-way
|
|
1027
1136
|
// for project installs). `root` first, `cfg` second: on a global install the two are the
|
|
1028
1137
|
// same directory anyway. Both silent ⇒ `null` ⇒ the gate stays.
|
|
1029
|
-
|
|
1138
|
+
// Both reads are narrowed to `host`'s OWN carrier (host-compat B1): in a repo shared with
|
|
1139
|
+
// another Claude-style host, an un-narrowed scan can answer with a SIBLING host's carrier —
|
|
1140
|
+
// the same failure `installProfile` had, reached here because `root` is the shared repo.
|
|
1141
|
+
const doctrines = doctrinesOfDir(root, host) ?? doctrinesOfDir(cfg, host);
|
|
1030
1142
|
// Same two-carrier read for the second axis, and its own default: an absent line means
|
|
1031
1143
|
// NOTHING excluded, so a reactivate can only ever restore a gate, never remove one.
|
|
1032
|
-
const excluded = excludedRulesOfDir(root).length ? excludedRulesOfDir(root)
|
|
1033
|
-
: excludedRulesOfDir(cfg);
|
|
1144
|
+
const excluded = excludedRulesOfDir(root, host).length ? excludedRulesOfDir(root, host)
|
|
1145
|
+
: excludedRulesOfDir(cfg, host);
|
|
1034
1146
|
// `host` reaches the group builder: a Bob reactivate must re-wire Gemini-named groups.
|
|
1035
1147
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed),
|
|
1036
1148
|
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host, cfg);
|
package/js/maintain/update.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import path from 'node:path';
|
|
|
46
46
|
import { ROOT } from '../build/source.mjs';
|
|
47
47
|
import { exportImprovements, flushExportNotes } from '../inspect/diff.mjs';
|
|
48
48
|
import { opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
49
|
+
import { legacyProcessCarrierOfDir } from '../hosts/installs.mjs';
|
|
49
50
|
import { appendText, copyFile, printOut, printErr, readText, writeText } from '../lib/fs.mjs';
|
|
50
51
|
import { comparePaths, toPlatformPath, which } from '../lib/paths.mjs';
|
|
51
52
|
import { WHITESPACE, parseIntStrict, stripWhitespace } from '../lib/text.mjs';
|
|
@@ -617,9 +618,18 @@ async function drainStdout() {
|
|
|
617
618
|
* Only the TAIL of the argv is kept: theme and emit are `upgrade`'s to decide (a theme argument
|
|
618
619
|
* may override the marker), and `setupBuildArgs` opens with exactly those four tokens when it
|
|
619
620
|
* is given no out and no root.
|
|
621
|
+
*
|
|
622
|
+
* `null` — distinct from `[]`'s "never built, nothing to carry over" — means `dir` carries a
|
|
623
|
+
* pre-comms carrier (same guard as `rebuild-all`/`migrate`/the web console, reusing
|
|
624
|
+
* `legacyProcessCarrierOfDir`): `installProfile` would read it exactly as written, and feeding
|
|
625
|
+
* that into a rebuild would write the loss back PERMANENTLY. `[]` here would be WORSE, not
|
|
626
|
+
* neutral — it resets every axis to the generator's config default, the exact failure this
|
|
627
|
+
* function exists to prevent, on top of the comms/process-address loss. `upgrade` must skip the
|
|
628
|
+
* bundle rebuild entirely on `null`, never fall through to an empty axes list.
|
|
620
629
|
*/
|
|
621
630
|
export async function installAxes(dir, { installProfile, setupBuildArgs }, notify = null) {
|
|
622
631
|
if (!existsSync(path.join(String(dir), '.geneseed-emit'))) return [];
|
|
632
|
+
if (legacyProcessCarrierOfDir(String(dir), 'opencode')) return null;
|
|
623
633
|
const p = installProfile('opencode', 'global', String(dir), notify);
|
|
624
634
|
return setupBuildArgs('', 'files', null, null, p.footprint, p.posture, p.mode, p.doctrines,
|
|
625
635
|
undefined, p.excludeRules, p.trust, p.excludeSkills).slice(4);
|
|
@@ -756,8 +766,18 @@ export async function upgrade(ref = null, themeArg = null) {
|
|
|
756
766
|
} catch (e) {
|
|
757
767
|
log(`[geneseed] ⚠️ could not migrate the old in-folder bundle (${e && e.message ? e.message : e}) — continuing.`);
|
|
758
768
|
}
|
|
759
|
-
let rc =
|
|
760
|
-
|
|
769
|
+
let rc = 0;
|
|
770
|
+
if (axes === null) {
|
|
771
|
+
// `installAxes` found a pre-comms carrier at this bundle's own target — skip the rebuild
|
|
772
|
+
// entirely rather than fall through with no axes (which would reset it to the generator's
|
|
773
|
+
// config default, worse than the loss this guard exists to stop). `rebuildInstalls` below
|
|
774
|
+
// reaches the same directory through `rebuild-all`'s own, identically-reasoned skip.
|
|
775
|
+
log(`[geneseed] SKIPPED bundle rebuild: pre-comms carrier at `
|
|
776
|
+
+ `${emit === 'opencode-global' ? cfg : out} (names 'process 8') — run `
|
|
777
|
+
+ '`geneseed setup` for it.');
|
|
778
|
+
} else {
|
|
779
|
+
rc = await rebuildBundle(here, out, theme, emit, rootDir, log, axes, bounceDaemonIfRunning);
|
|
780
|
+
}
|
|
761
781
|
if (rc !== 0) {
|
|
762
782
|
log(`[geneseed][E-BUILD] ✗ the bundle build FAILED (theme: ${theme || 'default'}, emit: ${emit}).`);
|
|
763
783
|
return 1;
|
package/js/web/actions.mjs
CHANGED
|
@@ -47,8 +47,7 @@ import {
|
|
|
47
47
|
} from '../hosts/hosts.mjs';
|
|
48
48
|
import {
|
|
49
49
|
EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir,
|
|
50
|
-
installState,
|
|
51
|
-
installTargets, modeOfDir, postureOfDir, trustOfDir,
|
|
50
|
+
installState, installTargets, legacyProcessCarrierOfDir, modeOfDir, postureOfDir, trustOfDir,
|
|
52
51
|
} from '../hosts/installs.mjs';
|
|
53
52
|
import {
|
|
54
53
|
MCP_PRESETS, isDict, mcpApply, mcpCommented, mcpInstallTargets, mcpKnownNames, mcpLoad,
|
|
@@ -605,26 +604,38 @@ export function apiInstallCmd(state, body) {
|
|
|
605
604
|
if (installState(root, host, scope) === 'disabled') {
|
|
606
605
|
return { error: 'install is disabled — reactivate it before (re)building' };
|
|
607
606
|
}
|
|
607
|
+
// A LEGACY CARRIER IS REFUSED, NEVER REBUILT — same guard as `rebuild-all`/`migrate` (see
|
|
608
|
+
// `legacyProcessCarrierOfDir`'s docblock): `doctrinesForBuild`/`excludedRulesOfDir` below
|
|
609
|
+
// read a pre-comms carrier exactly as written, and this endpoint's `{cmd}` is EXECUTED by
|
|
610
|
+
// `startJob` (`js/web/handler.mjs`'s `install` action) — building that argv here would write
|
|
611
|
+
// the same permanent loss a console click away.
|
|
612
|
+
if (legacyProcessCarrierOfDir(root, host)) {
|
|
613
|
+
return { error: `${host}:${scope} (${root}) predates the comms pack (its carrier names `
|
|
614
|
+
+ "'process 8') — run `geneseed setup` for it before rebuilding from the console" };
|
|
615
|
+
}
|
|
608
616
|
const emit = EMIT_FOR.get(`${host} ${scope}`);
|
|
609
617
|
if (emit === undefined) return { error: `no install mode for ${host}:${scope}` };
|
|
618
|
+
// Every read-back below is narrowed to `host`'s OWN carrier (host-compat B1) — see the
|
|
619
|
+
// comment at `installProfile` in `js/build/generate.mjs`.
|
|
610
620
|
const { theme, footprint: fp, posture: pos, mode, trust } = bodyAxes(body, {
|
|
611
|
-
theme: state.theme, footprint: footprintOfDir(root),
|
|
612
|
-
|
|
621
|
+
theme: state.theme, footprint: footprintOfDir(root),
|
|
622
|
+
posture: postureOfDir(root, host) || 'peer',
|
|
623
|
+
mode: modeOfDir(root, host) || 'direct', trust: trustOfDir(root, host) || DEFAULT_PRESET });
|
|
613
624
|
// Unspecified means "keep what this install already has", exactly as theme, footprint,
|
|
614
625
|
// posture and mode above do — a rebuild through the console is not a place to silently
|
|
615
626
|
// re-decide the constitution. ⚠ AND A CARRIER WITH NO `Active packs:` MARKER (a pre-2.3
|
|
616
627
|
// install) MUST NOT LAND ON `harness.config.json`: `doctrinesOfDir` answers `null` there,
|
|
617
628
|
// `null` elided the flag, and the generator's config fallback then narrowed the install and
|
|
618
629
|
// took its consent gate with it. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
619
|
-
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root);
|
|
630
|
+
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root, host);
|
|
620
631
|
// The per-rule axis needs NO `doctrinesForBuild`-style rescue: the marker it reads is only
|
|
621
632
|
// written when something is excluded, so its absence is a statement ("nothing") rather than
|
|
622
633
|
// the silence a pre-2.3 carrier gives on packs. Unspecified therefore keeps the install's
|
|
623
634
|
// own answer, and a pre-marker install answers the empty list — which is what it has.
|
|
624
|
-
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
635
|
+
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root, host);
|
|
625
636
|
const out = scope === 'global' ? null : String(root);
|
|
626
637
|
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
627
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root));
|
|
638
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, null, host));
|
|
628
639
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
629
640
|
}
|
|
630
641
|
|
|
@@ -704,6 +715,13 @@ export function apiDeployCmd(state, body) {
|
|
|
704
715
|
if (cfgdirs.has(root)) {
|
|
705
716
|
return { error: "that's a host global config dir — use its existing row to build a global install" };
|
|
706
717
|
}
|
|
718
|
+
// Same guard as `apiInstallCmd` above: a landing folder that already holds a pre-comms
|
|
719
|
+
// install must not be rebuilt through `doctrinesForBuild`/`excludedRulesOfDir` below. A
|
|
720
|
+
// fresh, never-built folder has no carrier at all, so this is a no-op for the common case.
|
|
721
|
+
if (legacyProcessCarrierOfDir(root, host)) {
|
|
722
|
+
return { error: `${root} predates the comms pack (its carrier names 'process 8') — `
|
|
723
|
+
+ 'run `geneseed setup` for it before deploying over it' };
|
|
724
|
+
}
|
|
707
725
|
// A fresh deploy is full, at the default register, mode and trust.
|
|
708
726
|
const { theme, footprint: fp, posture: pos, mode, trust } = bodyAxes(body, {
|
|
709
727
|
theme: state.theme, footprint: 'full', posture: 'peer', mode: 'direct',
|
|
@@ -714,10 +732,10 @@ export function apiDeployCmd(state, body) {
|
|
|
714
732
|
// the flag off, the generator fell back to `harness.config.json`, and deploying onto an
|
|
715
733
|
// existing all-four Claude install dropped it to one pack — measured: 6 hook groups became 5
|
|
716
734
|
// and `PreToolUse::Bash` went with them. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
717
|
-
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root);
|
|
718
|
-
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
735
|
+
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root, host);
|
|
736
|
+
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root, host);
|
|
719
737
|
// project-scope emit name == host name (opencode / claude / bob / openclaude)
|
|
720
738
|
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
721
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root));
|
|
739
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, null, host));
|
|
722
740
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
723
741
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -119,10 +119,12 @@ export function apiInstalls(state) {
|
|
|
119
119
|
out.push({
|
|
120
120
|
id: `${host}:${scope}`, host, scope, path: root,
|
|
121
121
|
state: installState(root, host, scope),
|
|
122
|
-
|
|
122
|
+
// Narrowed to this row's own host (host-compat B1): a shared repo otherwise answers
|
|
123
|
+
// every row with whichever host's carrier the host-agnostic scan favours.
|
|
124
|
+
theme: themeOfDir(root, host),
|
|
123
125
|
footprint: footprintOfDir(root),
|
|
124
|
-
posture: postureOfDir(root),
|
|
125
|
-
mode: modeOfDir(root),
|
|
126
|
+
posture: postureOfDir(root, host),
|
|
127
|
+
mode: modeOfDir(root, host),
|
|
126
128
|
trust: trustOfDir(root, host),
|
|
127
129
|
selected: samePath(viewCfg(host, scope, root), state.target),
|
|
128
130
|
});
|
package/js/web/catalog.mjs
CHANGED
|
@@ -185,7 +185,7 @@ export function notebookItems(state) {
|
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
187
|
* The wiki manifest's names, current first. `wiki.jsonc` is the name before the `geneseed-`
|
|
188
|
-
* prefix: the build renames it on the next emit, and until then it is still read
|
|
188
|
+
* prefix: the build renames it on the next emit, and until then it is still read (no end date; see `LEGACY_WIKI_FILES`).
|
|
189
189
|
*/
|
|
190
190
|
const WIKI_NAMES = ['geneseed-wiki.jsonc', 'wiki.jsonc'];
|
|
191
191
|
const WIKI_META = ['Wiki manifest', 'your machine-wide knowledge base(s)'];
|
package/js/web/state.mjs
CHANGED
|
@@ -10,7 +10,8 @@ import path from 'node:path';
|
|
|
10
10
|
|
|
11
11
|
import { opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
12
12
|
import {
|
|
13
|
-
footprintOfDir, installedDefaults, modeOfDir, postureOfDir, themeOfDir,
|
|
13
|
+
EMIT_HOST_SCOPE, footprintOfDir, installedDefaults, modeOfDir, postureOfDir, themeOfDir,
|
|
14
|
+
trustOfDir,
|
|
14
15
|
} from '../hosts/installs.mjs';
|
|
15
16
|
import { doctorCollect } from '../inspect/doctor.mjs';
|
|
16
17
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
@@ -56,18 +57,25 @@ export function webState(theme = null, target = null) {
|
|
|
56
57
|
_inv: null,
|
|
57
58
|
_doctor: null,
|
|
58
59
|
};
|
|
59
|
-
// The INSTALL ROOT (== build --out). For globals it IS the data dir; a claude/bob
|
|
60
|
-
// PROJECT install keeps its data under <repo>/.claude while the markers
|
|
60
|
+
// The INSTALL ROOT (== build --out). For globals it IS the data dir; a claude/bob/openclaude
|
|
61
|
+
// PROJECT install keeps its data under <repo>/.claude|.bob|.openclaude while the markers
|
|
62
|
+
// land at <repo>/.
|
|
61
63
|
st.root = st.target;
|
|
62
64
|
st.theme = theme || themeOfDir(st.target) || 'neutral';
|
|
63
65
|
st.emit = installedDefaults().emit || 'opencode-global';
|
|
66
|
+
// `st.emit` to `host`, the same lookup `apiInstallCmd`/`diffCollect` already make off the
|
|
67
|
+
// same map — unknown or missing falls back to OpenCode, as those two do.
|
|
68
|
+
const hostOf = () => (EMIT_HOST_SCOPE.get(st.emit || '') ?? ['opencode', 'global'])[0];
|
|
64
69
|
// The install's sigils, each defaulted when its marker is absent (footprint to 'full').
|
|
65
|
-
// Read from the ROOT, where every emit writes them.
|
|
66
|
-
|
|
70
|
+
// Read from the ROOT, where every emit writes them. `host`, when given, narrows the carrier
|
|
71
|
+
// scan to its own (host-compat B1) — omitted here at construction, where `st.emit` is only
|
|
72
|
+
// `installedDefaults()`'s best guess and may not be THIS target's own host; `selectView`
|
|
73
|
+
// and `refresh` below pass it once `detectEmit()` has looked at this install directly.
|
|
74
|
+
const readMarkers = (host = null) => {
|
|
67
75
|
st.footprint = footprintOfDir(st.root);
|
|
68
|
-
st.posture = postureOfDir(st.root) || 'peer';
|
|
69
|
-
st.mode = modeOfDir(st.root) || 'direct';
|
|
70
|
-
st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
|
|
76
|
+
st.posture = postureOfDir(st.root, host) || 'peer';
|
|
77
|
+
st.mode = modeOfDir(st.root, host) || 'direct';
|
|
78
|
+
st.trust = trustOfDir(st.root, host) || DEFAULT_PRESET;
|
|
71
79
|
};
|
|
72
80
|
readMarkers();
|
|
73
81
|
|
|
@@ -112,27 +120,32 @@ export function webState(theme = null, target = null) {
|
|
|
112
120
|
/**
|
|
113
121
|
* Re-point the console at another detected install's data dir.
|
|
114
122
|
*
|
|
115
|
-
* `root` is the install ROOT the markers
|
|
116
|
-
*
|
|
123
|
+
* `root` is the install ROOT the markers live at. It defaults to `target` and differs only
|
|
124
|
+
* for claude/bob/openclaude PROJECT installs, where the data sits under
|
|
117
125
|
* `<repo>/.claude|.bob|.openclaude` while `.geneseed-emit`/`-theme`/`-footprint` land at
|
|
118
126
|
* `<repo>/` — reading them from the data dir mis-detects the install as opencode/neutral,
|
|
119
|
-
* and a Diff or a Restore would then overwrite it in the wrong dialect.
|
|
127
|
+
* and a Diff or a Restore would then overwrite it in the wrong dialect. The theme/posture/mode
|
|
128
|
+
* SIGILS are different: for claude/bob they live in the ROOT carrier (`themeOfDir` scans it);
|
|
129
|
+
* for openclaude they live ONLY in the data dir's nested `.openclaude/CLAUDE.md`, since its
|
|
130
|
+
* root carrier is deliberately left untouched (host-compat B1/I1).
|
|
120
131
|
*/
|
|
121
132
|
st.selectView = (target, root = null) => {
|
|
122
133
|
st.target = target;
|
|
123
134
|
st.root = root || target;
|
|
124
|
-
st.theme = themeOfDir(st.root) || themeOfDir(st.target) || 'neutral';
|
|
125
135
|
st.emit = st.detectEmit();
|
|
126
|
-
|
|
136
|
+
const host = hostOf();
|
|
137
|
+
st.theme = themeOfDir(st.root, host) || themeOfDir(st.target, host) || 'neutral';
|
|
138
|
+
readMarkers(host);
|
|
127
139
|
st._inv = null;
|
|
128
140
|
st._doctor = null;
|
|
129
141
|
};
|
|
130
142
|
st.refresh = () => {
|
|
131
143
|
st._inv = null;
|
|
132
144
|
st._doctor = null;
|
|
133
|
-
st.theme = themeOfDir(st.root) || themeOfDir(st.target) || st.theme;
|
|
134
145
|
st.emit = st.detectEmit() || st.emit;
|
|
135
|
-
|
|
146
|
+
const host = hostOf();
|
|
147
|
+
st.theme = themeOfDir(st.root, host) || themeOfDir(st.target, host) || st.theme;
|
|
148
|
+
readMarkers(host);
|
|
136
149
|
};
|
|
137
150
|
return st;
|
|
138
151
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
|
|
3
3
|
"name": "geneseed",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.0",
|
|
5
5
|
"description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai",
|
package/src/agents/_template.md
CHANGED
package/src/bricks/_template.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!--
|
|
1
|
+
<!-- note:
|
|
2
2
|
A brick template — copy this file to `<name>.md` (kebab-case, matching the `name:` field
|
|
3
3
|
below) under `bricks/` at whichever level you are authoring for: shipped (this package's
|
|
4
4
|
`src/bricks/`), global ($XDG_CONFIG_HOME/geneseed/bricks/), or project (<repo>/.geneseed/bricks/).
|
package/src/bricks/apply.md
CHANGED
|
@@ -10,7 +10,7 @@ carries an `amend`, that supersedes the original intent — follow the amendment
|
|
|
10
10
|
replaced.
|
|
11
11
|
|
|
12
12
|
When this is a repair after `test`, act on the latest `test` note only — it is the verdict on
|
|
13
|
-
the code as it stands. Fix every finding it lists in this one pass:
|
|
13
|
+
the code as it stands. Fix every finding it lists in this one pass: repairs are capped at two.
|
|
14
14
|
|
|
15
15
|
Make no unrelated edits: no drive-by rename, no incidental cleanup, no touching a file the card
|
|
16
16
|
did not declare. If the change genuinely needs a file outside the write set, stop and report that
|
package/src/bricks/test.md
CHANGED
|
@@ -11,7 +11,7 @@ brick only observes; a fix belongs in `apply`, not here.
|
|
|
11
11
|
1. **Gate** — the build or compile step. If it fails, mark every check below `SKIPPED (gate
|
|
12
12
|
failed)` and report.
|
|
13
13
|
2. **Focused checks** — the linter and the tests the change can reach. Run every one even after a
|
|
14
|
-
failure: they check independent properties, and
|
|
14
|
+
failure: they check independent properties, and each repair must see every finding at once.
|
|
15
15
|
3. **Full suite** — only when everything above passed; otherwise `SKIPPED (an earlier check failed)`.
|
|
16
16
|
|
|
17
17
|
List every check with `PASS`, `FAIL`, `ERROR` or `SKIPPED (reason)` — one that did not run is
|