geneseed 2.1.0 → 3.1.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 +155 -0
- package/DESIGN.md +112 -61
- package/QUICKSTART.md +1 -1
- package/README.md +24 -14
- package/SETUP.md +83 -17
- package/SHIPPED.md +5 -4
- 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/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 +72 -6
- package/docs/extending.md +164 -55
- package/docs/limits.md +4 -4
- package/docs/token-footprint.md +15 -4
- 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/harness.config.json +1 -1
- package/js/catalog.mjs +29 -4
- package/js/checkout.mjs +24 -1
- package/js/cli-table.json +8 -6
- package/js/diff.mjs +8 -3
- package/js/doctor.mjs +307 -4
- 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 -21
- package/js/migrate.mjs +10 -4
- package/js/render.mjs +110 -7
- package/js/settings.mjs +218 -27
- package/js/setup.mjs +95 -5
- package/js/status.mjs +45 -4
- package/js/tui.mjs +44 -1
- package/js/uninstall.mjs +25 -6
- package/js/web/actions.mjs +56 -5
- package/js/web/api.mjs +102 -16
- package/js/web/docs.mjs +33 -5
- package/js/web/jobs.mjs +12 -3
- package/js/web/server.mjs +6 -0
- 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 -336
- 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 -36
- package/themes/biker.json +37 -36
- package/themes/commentator.json +37 -36
- package/themes/cyberpunk.json +37 -36
- package/themes/gamer.json +37 -36
- package/themes/imperial.json +37 -36
- package/themes/joker.json +37 -36
- package/themes/marvin.json +37 -36
- package/themes/mean.json +37 -36
- package/themes/military.json +37 -36
- package/themes/neutral.json +37 -36
- package/themes/pirate.json +37 -36
- package/themes/sports.json +37 -36
- package/themes/verstappen.json +37 -36
- package/themes/wizard.json +37 -36
- package/web/dist/assets/{About-BYXlmbyo.js → About-_MnJcwb_.js} +1 -1
- package/web/dist/assets/{Activity-us5MPf4u.js → Activity-DpjqO-FR.js} +1 -1
- package/web/dist/assets/{ActivityDetail-CiaQXUk2.js → ActivityDetail-Bvrjv8oW.js} +1 -1
- package/web/dist/assets/{Diff-Bh0qbZ2s.js → Diff-DZ0IehBV.js} +1 -1
- package/web/dist/assets/{Doctor-BIlQME45.js → Doctor-cu4krooO.js} +1 -1
- package/web/dist/assets/{Graph-4dYD1dPG.js → Graph-DUOPY65j.js} +1 -1
- package/web/dist/assets/{Harnesses-PzYZ3LOC.js → Harnesses-B5Osg-BF.js} +1 -1
- package/web/dist/assets/Laws-D8DJgTBp.js +3 -0
- package/web/dist/assets/{Library-CjMbksJW.js → Library-8y69I3_z.js} +1 -1
- package/web/dist/assets/{Markdown-Bk81c3aZ.js → Markdown-Ds5GS1OD.js} +1 -1
- package/web/dist/assets/{Profile-Curui_-j.js → Profile-7Jxa7ZUX.js} +1 -1
- package/web/dist/assets/{Rules-Bm283wdv.js → Rules-UZ0jWkjg.js} +1 -1
- package/web/dist/assets/{Skills-gruqYuhl.js → Skills-Bgkl0xLd.js} +1 -1
- package/web/dist/assets/{StatusBadge-EiM_FPqv.js → StatusBadge-Cx4caF_2.js} +1 -1
- package/web/dist/assets/{Themes-DU99j4Hr.js → Themes-De8az7Sv.js} +1 -1
- package/web/dist/assets/{activity-0rvQ6Vra.js → activity-VDAdVQ3q.js} +1 -1
- package/web/dist/assets/index-1pPSz7Kl.css +1 -0
- package/web/dist/assets/{index-0b2hobOo.js → index-BKrjBRV4.js} +1 -1
- package/web/dist/assets/index-C-gb-q-r.js +12 -0
- package/web/dist/assets/{index-IiGFmCW_.js → index-wNiNzFFQ.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +401 -93
- package/web/dist/assets/Laws-DtZweXal.js +0 -1
- package/web/dist/assets/index-D5VY3TY_.js +0 -12
- package/web/dist/assets/index-DUCsDv7z.css +0 -1
package/js/uninstall.mjs
CHANGED
|
@@ -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 {
|
|
@@ -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
|
|
|
@@ -702,6 +732,13 @@ export function apiRestore(state, files) {
|
|
|
702
732
|
footprint: state.footprint,
|
|
703
733
|
posture: state.posture,
|
|
704
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),
|
|
705
742
|
}));
|
|
706
743
|
for (const raw of (pyTruthy(files) ? files : [])) {
|
|
707
744
|
const rel = pyStripSpace(pyStr(raw).replace(/\\/g, '/')).replace(/^\/+/, '');
|
|
@@ -781,8 +818,15 @@ export function apiInstallCmd(state, body) {
|
|
|
781
818
|
const bmode = bget(body, 'mode');
|
|
782
819
|
const mode = discoverNames('modes', 'direct').includes(bmode)
|
|
783
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);
|
|
784
828
|
const out = scope === 'global' ? null : String(root);
|
|
785
|
-
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode);
|
|
829
|
+
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines);
|
|
786
830
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'geneseed.mjs'), ...argv] };
|
|
787
831
|
}
|
|
788
832
|
|
|
@@ -878,7 +922,14 @@ export function apiDeployCmd(state, body) {
|
|
|
878
922
|
const pos = discoverNames('postures', 'peer').includes(bpos) ? bpos : 'peer';
|
|
879
923
|
const bmode = bget(body, 'mode');
|
|
880
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);
|
|
881
932
|
// project-scope emit name == host name (opencode / claude / bob / copilot)
|
|
882
|
-
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode);
|
|
933
|
+
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines);
|
|
883
934
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'geneseed.mjs'), ...argv] };
|
|
884
935
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -61,7 +61,7 @@ import { doctorCollect } from '../doctor.mjs';
|
|
|
61
61
|
import { excludesSnapshot } from '../excludes.mjs';
|
|
62
62
|
import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir, pyResolve } from '../hosts.mjs';
|
|
63
63
|
import {
|
|
64
|
-
footprintOfDir, installState, installTargets, installedDefaults, modeOfDir,
|
|
64
|
+
doctrinesForBuild, footprintOfDir, installState, installTargets, installedDefaults, modeOfDir,
|
|
65
65
|
postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
|
|
66
66
|
} from '../installs.mjs';
|
|
67
67
|
import { frontmatter } from '../hooks.mjs';
|
|
@@ -273,16 +273,22 @@ export function specEntries(root, nested) {
|
|
|
273
273
|
* `_web_core._deployed_inventory` — the agents and skills actually installed at
|
|
274
274
|
* `state.target`, not a fresh render of `src/`.
|
|
275
275
|
*
|
|
276
|
-
*
|
|
277
|
-
* separate files, so the deployed arm replaces
|
|
278
|
-
* deployed spec file carries neither a category nor a
|
|
279
|
-
* BY NAME from the same two sources the source render
|
|
280
|
-
* does not know reads "personal" for both, which is the
|
|
281
|
-
* borrowed one: filing it under the `build` fallback would claim
|
|
282
|
-
* it was never given.
|
|
276
|
+
* All three constitutional tiers still come from the render: once deployed they live inside
|
|
277
|
+
* AGENT.md rather than as separate files, so the deployed arm replaces the two ENTITY rosters
|
|
278
|
+
* and none of the constitution. A deployed spec file carries neither a category nor a
|
|
279
|
+
* lifecycle status, so each is tagged BY NAME from the same two sources the source render
|
|
280
|
+
* uses — and an entity the registry does not know reads "personal" for both, which is the
|
|
281
|
+
* honest answer rather than a borrowed one: filing it under the `build` fallback would claim
|
|
282
|
+
* a Geneseed taxonomy slot it was never given.
|
|
283
|
+
*
|
|
284
|
+
* ⚠ THE PACK SELECTION IS THE ONE THING THE RENDER CANNOT KNOW. `renderAll` emits all four
|
|
285
|
+
* pack files whatever `cfg.doctrines` says — measured: `makeCfg({doctrines:['craft']})` yields
|
|
286
|
+
* the same 105 items — because the filtering happens when AGENT.md's §2 is assembled, not in
|
|
287
|
+
* the walk. So the catalogue is always whole, and which packs this INSTALL built in comes from
|
|
288
|
+
* its own carrier via `doctrinesForBuild`, which resolves "no marker" to every pack.
|
|
283
289
|
*/
|
|
284
290
|
export function deployedInventory(state) {
|
|
285
|
-
const render = tuiInventory(state.theme);
|
|
291
|
+
const render = tuiInventory(state.theme, doctrinesForBuild(state.target));
|
|
286
292
|
const registry = loadRegistry();
|
|
287
293
|
const skills = specEntries(path.join(state.target, 'skills'), true);
|
|
288
294
|
for (const e of skills) {
|
|
@@ -292,7 +298,12 @@ export function deployedInventory(state) {
|
|
|
292
298
|
}
|
|
293
299
|
const agents = specEntries(path.join(state.target, 'agents'), false);
|
|
294
300
|
for (const e of agents) e.status = entityStatus(registry, `agents/${e.name}`);
|
|
295
|
-
return { agents,
|
|
301
|
+
return { agents,
|
|
302
|
+
skills,
|
|
303
|
+
laws: render.laws,
|
|
304
|
+
ontology: render.ontology,
|
|
305
|
+
doctrines: render.doctrines,
|
|
306
|
+
theme: state.theme };
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
/** `WebState.inventory` — the deployed record when there is one, else the source render. */
|
|
@@ -592,9 +603,53 @@ function configManifest(name, p) {
|
|
|
592
603
|
return null;
|
|
593
604
|
}
|
|
594
605
|
|
|
595
|
-
/**
|
|
606
|
+
/**
|
|
607
|
+
* `_web_core.SECTIONS` — a closed list; anything else is a 404.
|
|
608
|
+
*
|
|
609
|
+
* ⚠ `laws` STAYS ONE SECTION FOR ALL THREE TIERS, and that is a decision rather than an
|
|
610
|
+
* oversight. The console has ONE Constitution entry, not three, so a reader keeps the "read it
|
|
611
|
+
* top to bottom" property; the id stays `laws` because `tests/helpers/cli_golden.mjs`
|
|
612
|
+
* hard-requires `web/src/pages/Laws.jsx` and because a route rename buys nothing and costs a
|
|
613
|
+
* redirect. The tier lives on the ITEM (`tier: 'ontology' | 'invariant' | 'doctrine'`), which
|
|
614
|
+
* is where a consumer can act on it.
|
|
615
|
+
*/
|
|
596
616
|
export const SECTIONS = ['agents', 'skills', 'laws', 'memory', 'notebook', 'wiki', 'config'];
|
|
597
617
|
|
|
618
|
+
/**
|
|
619
|
+
* The `laws` section's items — the whole constitution, in constitutional order, one flat list.
|
|
620
|
+
*
|
|
621
|
+
* FLAT AND NOT NESTED, because every other section is `{section, items}` and a second shape
|
|
622
|
+
* for one section is a second client path for one roster. The pack's grouping metadata rides
|
|
623
|
+
* on each doctrine row (`pack`, `packTitle`, `packDesc`, `active`), so a consumer groups by
|
|
624
|
+
* `pack` and has everything a header needs without a second fetch.
|
|
625
|
+
*
|
|
626
|
+
* Names are ADDRESSES and the three shapes cannot collide: `ont:<slug>` carries a colon no
|
|
627
|
+
* numeral has, an invariant is `[IVXLCDM]+`, a doctrine rule is `<pack>.<n>`. `apiItem`
|
|
628
|
+
* resolves all three off the same list.
|
|
629
|
+
*/
|
|
630
|
+
function constitutionItems(inv) {
|
|
631
|
+
return [
|
|
632
|
+
...(inv.ontology ?? []).map((e) => ({ name: `ont:${e.id}`, title: e.title, desc: '',
|
|
633
|
+
tier: 'ontology' })),
|
|
634
|
+
...inv.laws.map((e) => ({ name: e.num, title: `Rule ${e.num} — ${e.title}`, desc: '',
|
|
635
|
+
klass: e.klass ?? 'craft', tier: 'invariant' })),
|
|
636
|
+
...(inv.doctrines ?? []).flatMap((p) => p.rules.map((r) => ({
|
|
637
|
+
name: `${r.pack}.${r.n}`,
|
|
638
|
+
title: `Doctrine ${r.pack} ${r.n} — ${r.title}`,
|
|
639
|
+
desc: '',
|
|
640
|
+
klass: r.klass ?? p.pack,
|
|
641
|
+
tier: 'doctrine',
|
|
642
|
+
pack: p.pack,
|
|
643
|
+
packTitle: p.title,
|
|
644
|
+
packDesc: p.desc,
|
|
645
|
+
// ⚠ EVERY RULE IS LISTED WHETHER OR NOT ITS PACK IS BUILT IN. An inactive pack that
|
|
646
|
+
// vanished from the payload would be indistinguishable from one that never shipped, and
|
|
647
|
+
// the console could not then offer the command that turns it back on.
|
|
648
|
+
active: p.active,
|
|
649
|
+
}))),
|
|
650
|
+
];
|
|
651
|
+
}
|
|
652
|
+
|
|
598
653
|
/** `_web_catalog.api_catalog`. */
|
|
599
654
|
export function apiCatalog(state, section) {
|
|
600
655
|
if (!SECTIONS.includes(section)) throw new NotFound(section);
|
|
@@ -607,8 +662,7 @@ export function apiCatalog(state, section) {
|
|
|
607
662
|
items = inv.skills.map((e) => ({ name: e.name, title: e.name, desc: e.desc,
|
|
608
663
|
source: e.source ?? null, klass: e.klass ?? 'build', status: e.status ?? 'unknown' }));
|
|
609
664
|
} else if (section === 'laws') {
|
|
610
|
-
items = inv
|
|
611
|
-
desc: '', klass: e.klass ?? 'craft' }));
|
|
665
|
+
items = constitutionItems(inv);
|
|
612
666
|
} else if (section === 'memory') {
|
|
613
667
|
items = memoryItems(state);
|
|
614
668
|
} else if (section === 'notebook') {
|
|
@@ -634,10 +688,21 @@ export function apiItem(state, type, name) {
|
|
|
634
688
|
return out;
|
|
635
689
|
}
|
|
636
690
|
if (type === 'law') {
|
|
637
|
-
|
|
691
|
+
// ONE ARM FOR THREE TIERS, resolved off the same list the catalogue publishes — so a name
|
|
692
|
+
// that appears in `/api/catalog/laws` always opens, and one that does not always 404s.
|
|
693
|
+
// Building the list twice is what would let the two drift.
|
|
694
|
+
const e = constitutionItems(inv).find((x) => x.name === name);
|
|
638
695
|
if (!e) throw new NotFound(name);
|
|
639
|
-
|
|
640
|
-
|
|
696
|
+
const body = e.tier === 'ontology'
|
|
697
|
+
? (inv.ontology.find((s) => `ont:${s.id}` === name)?.body ?? '')
|
|
698
|
+
: (e.tier === 'invariant'
|
|
699
|
+
? (inv.laws.find((x) => x.num === name)?.body ?? '')
|
|
700
|
+
: (inv.doctrines.flatMap((p) => p.rules)
|
|
701
|
+
.find((r) => `${r.pack}.${r.n}` === name)?.body ?? ''));
|
|
702
|
+
// `links: []` — a constitution body is not link-resolved. It cites its siblings by
|
|
703
|
+
// ADDRESS (`Rule IV`, `Doctrine ops 1`, `Ontology: Telos`), not by path, and there is no
|
|
704
|
+
// file behind those to resolve to.
|
|
705
|
+
return { ...e, type, name, body, links: [] };
|
|
641
706
|
}
|
|
642
707
|
if (type === 'memory' || type === 'notebook') {
|
|
643
708
|
flatName(name);
|
|
@@ -789,13 +854,34 @@ export function apiOverview(state) {
|
|
|
789
854
|
accent: accentFor(state.theme),
|
|
790
855
|
emit: state.emit,
|
|
791
856
|
footprint: install ? install.footprint : state.footprint,
|
|
857
|
+
// ⚠ NO PACK ROSTER HERE, DELIBERATELY. One was added when the toggle lived in Settings,
|
|
858
|
+
// which has no other source for the pack names. The control moved to the Constitution
|
|
859
|
+
// page, which already holds the whole catalogue — `/api/catalog/laws` carries `pack`,
|
|
860
|
+
// `packTitle`, `packDesc` and `active` on every doctrine row — so a second copy of the
|
|
861
|
+
// roster on this endpoint would be API surface with no reader, and two places for the
|
|
862
|
+
// same fact to disagree. The SUMMARY stays, under `counts.doctrines`: the dashboard
|
|
863
|
+
// tiles need the fraction and must not fetch a catalogue to draw it.
|
|
792
864
|
install,
|
|
793
865
|
target: state.target,
|
|
794
866
|
deployed: deployed(state),
|
|
795
867
|
counts: {
|
|
796
868
|
agents: inv.agents.length,
|
|
797
869
|
skills: inv.skills.length,
|
|
870
|
+
// ⚠ `laws` STAYS THE INVARIANT COUNT. The rail badge reads it, `docCounts` mirrors it as
|
|
871
|
+
// `{N_LAWS}`, and the doctor's frozen `proseMirrorProblems` compares README and
|
|
872
|
+
// SHIPPED.md prose against the same number. Widening it to "the whole constitution"
|
|
873
|
+
// would silently move all four at once.
|
|
798
874
|
laws: inv.laws.length,
|
|
875
|
+
ontology: (inv.ontology ?? []).length,
|
|
876
|
+
// The tier's own summary, kept out of the section counts because it is not a section:
|
|
877
|
+
// `active`/`total` is the fraction the dashboard shows, `rules` counts only the packs
|
|
878
|
+
// this install built in — what it is bound by, not what its bundle carries.
|
|
879
|
+
doctrines: {
|
|
880
|
+
active: (inv.doctrines ?? []).filter((p) => p.active).length,
|
|
881
|
+
total: (inv.doctrines ?? []).length,
|
|
882
|
+
rules: (inv.doctrines ?? []).filter((p) => p.active)
|
|
883
|
+
.reduce((n, p) => n + p.rules.length, 0),
|
|
884
|
+
},
|
|
799
885
|
memory: memoryItems(state).length,
|
|
800
886
|
notebook: notebookItems(state).length,
|
|
801
887
|
wiki: wikiItems(state).length,
|
package/js/web/docs.mjs
CHANGED
|
@@ -130,8 +130,15 @@ export function docGroups() {
|
|
|
130
130
|
* the one-line description. A key of `null` marks a term no theme renames.
|
|
131
131
|
*/
|
|
132
132
|
const GLOSSARY_KEYS = [
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
// The three tiers, in constitutional order and before the entities — a reader who does not
|
|
134
|
+
// know what an Ontology is here cannot read the rest of the glossary's first rows.
|
|
135
|
+
['Ontology', 'ONTOLOGY', 'the always-on worldview under the rules — telos, evidence, '
|
|
136
|
+
+ 'decisions, conduct; it holds the Pact'],
|
|
137
|
+
['Rule (Law)', 'LAW', 'one of the nine always-on invariants: what is never done'],
|
|
138
|
+
['Rules (Laws)', 'LAWS', 'the body of always-on invariants'],
|
|
139
|
+
['Doctrine', 'DOCTRINE', 'one practice rule, cited `<pack> <n>` — how work is done here'],
|
|
140
|
+
['Doctrines', 'DOCTRINES', 'the practice packs, chosen at build time; inactive packs still '
|
|
141
|
+
+ 'ship in the bundle'],
|
|
135
142
|
['Agent', 'AGENT', 'a capability specialist'],
|
|
136
143
|
['Agents', 'AGENTS', 'the roster of specialists'],
|
|
137
144
|
['Skill', 'SKILL', 'a repeatable workflow'],
|
|
@@ -139,7 +146,10 @@ const GLOSSARY_KEYS = [
|
|
|
139
146
|
['Memory', 'MEMORY', 'durable, one-fact-per-file knowledge'],
|
|
140
147
|
['Notebook', 'NOTEBOOK', "the agent's sovereign space"],
|
|
141
148
|
['Wiki', 'WIKI', 'the machine-wide knowledge base'],
|
|
142
|
-
|
|
149
|
+
// Still a token, still themed, but it names a CONCEPT INSIDE the ontology's Telos now
|
|
150
|
+
// rather than a peer of the Rules — `src/postures/peer.md` cites it and the section that
|
|
151
|
+
// used to carry it is the Ontology.
|
|
152
|
+
['Pact', 'PACT', "the two-way collaboration contract, stated in the Ontology's Telos"],
|
|
143
153
|
['Posture', null, 'the relationship register the agent works in '
|
|
144
154
|
+ '(peer, mentor, expert, assistant, artisan)'],
|
|
145
155
|
['Mode', null, 'how work gets executed — direct (the agent works every '
|
|
@@ -156,8 +166,14 @@ const GLOSSARY_KEYS = [
|
|
|
156
166
|
|
|
157
167
|
// ---- counts substituted into concept bodies -------------------------------------------
|
|
158
168
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
169
|
+
/**
|
|
170
|
+
* `_web_docs._doc_counts`.
|
|
171
|
+
*
|
|
172
|
+
* Exported so the tier tokens can be gated on the day they are ADDED rather than on the day a
|
|
173
|
+
* page first spends one. Only one page carries `{N_LAWS}` today; a test that could only reach
|
|
174
|
+
* these through a rendered body would leave four of the seven untested until Task 6.2.
|
|
175
|
+
*/
|
|
176
|
+
export function docCounts(state) {
|
|
161
177
|
const inv = state.inventory;
|
|
162
178
|
const plugins = path.join(ROOT, 'adapters', 'opencode', 'plugins');
|
|
163
179
|
let n = 0;
|
|
@@ -165,11 +181,23 @@ function docCounts(state) {
|
|
|
165
181
|
n = readdirSync(plugins).filter((f) => f.startsWith('geneseed-') && f.endsWith('.js'))
|
|
166
182
|
.length;
|
|
167
183
|
} catch { n = 0; }
|
|
184
|
+
// `{N_LAWS}` IS THE INVARIANT COUNT AND NOTHING ELSE. The doctor's frozen
|
|
185
|
+
// `proseMirrorProblems` holds README and SHIPPED.md prose to the same number, so a token
|
|
186
|
+
// that quietly meant "the whole constitution" would put the docs and the gate at odds with
|
|
187
|
+
// no way to re-bless either. The other three tiers get their own tokens.
|
|
188
|
+
const packs = inv.doctrines ?? [];
|
|
189
|
+
const on = packs.filter((p) => p.active);
|
|
168
190
|
return {
|
|
169
191
|
'{N_LAWS}': (inv.laws || []).length,
|
|
170
192
|
'{N_AGENTS}': (inv.agents || []).length,
|
|
171
193
|
'{N_SKILLS}': (inv.skills || []).length,
|
|
172
194
|
'{N_PLUGINS}': n,
|
|
195
|
+
'{N_ONTOLOGY}': (inv.ontology ?? []).length,
|
|
196
|
+
'{N_PACKS}': packs.length,
|
|
197
|
+
'{N_PACKS_ACTIVE}': on.length,
|
|
198
|
+
// Rules in the packs this install BUILT IN — the number a reader of these pages is
|
|
199
|
+
// actually bound by. The catalogue always ships whole, so it is never the smaller claim.
|
|
200
|
+
'{N_DOCTRINE_RULES}': on.reduce((t, p) => t + p.rules.length, 0),
|
|
173
201
|
};
|
|
174
202
|
}
|
|
175
203
|
|
package/js/web/jobs.mjs
CHANGED
|
@@ -107,7 +107,7 @@ import { statSync } from 'node:fs';
|
|
|
107
107
|
import { StringDecoder } from 'node:string_decoder';
|
|
108
108
|
import path from 'node:path';
|
|
109
109
|
|
|
110
|
-
import { ROOT } from '../checkout.mjs';
|
|
110
|
+
import { ROOT, PACK_ORDER } from '../checkout.mjs';
|
|
111
111
|
import { setupBuildArgs } from '../generate.mjs';
|
|
112
112
|
import { isDict } from '../mcp.mjs';
|
|
113
113
|
import { jsonDumpsCompact, parseJson, pyTruthy, readText, writeText } from '../lib/fs.mjs';
|
|
@@ -444,11 +444,20 @@ export const NOT_PORTED_ACTIONS = new Set();
|
|
|
444
444
|
*/
|
|
445
445
|
export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
|
|
446
446
|
|
|
447
|
+
/**
|
|
448
|
+
* ⚠ `doctrines` DEFAULTS TO THE FULL SET, and it is the one axis here whose default is not the
|
|
449
|
+
* generator's own. `theme`/`emit`/`footprint`/`posture`/`mode` fall back to a frozen literal
|
|
450
|
+
* that costs nothing when it is wrong; a missing `--doctrines` falls through to
|
|
451
|
+
* `harness.config.json`, and a `{"doctrines":["craft"]}` there re-emits the deployed install
|
|
452
|
+
* without its commit/push consent gate. So a caller that forgets the axis gets ALL packs —
|
|
453
|
+
* unknown resolves to the full set, never to a config value (`doctrinesForBuild`).
|
|
454
|
+
*/
|
|
447
455
|
function actionTable({
|
|
448
456
|
theme = 'neutral', emit = 'opencode-global', footprint = 'full',
|
|
449
|
-
posture = 'peer', mode = 'direct',
|
|
457
|
+
posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER],
|
|
450
458
|
} = {}) {
|
|
451
|
-
const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode
|
|
459
|
+
const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode,
|
|
460
|
+
doctrines);
|
|
452
461
|
return {
|
|
453
462
|
doctor: [[NODE(), CLI(), 'doctor']],
|
|
454
463
|
build: [[NODE(), GEN(), ...buildArgv]],
|
package/js/web/server.mjs
CHANGED
|
@@ -60,6 +60,7 @@ import { promptLine } from '../setup.mjs';
|
|
|
60
60
|
// which resolves because everything either side needs of the other is a hoisted function
|
|
61
61
|
// declaration, not a value read at module-evaluation time.
|
|
62
62
|
import { preflight } from '../update.mjs';
|
|
63
|
+
import { doctrinesForBuild } from '../installs.mjs';
|
|
63
64
|
import { NotFound, PREFIX_ROUTES, STATE_ROUTES, webState } from './api.mjs';
|
|
64
65
|
import {
|
|
65
66
|
apiDeployCmd, apiExcludesMutate, apiInstallCmd, apiInstallToggle, apiMcpToggle,
|
|
@@ -549,8 +550,13 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
549
550
|
// install, so a re-theme preserves lean/full, the register and the operating mode.
|
|
550
551
|
const [theme, emit] = action === 'build'
|
|
551
552
|
? buildOverride(state, body) : [state.theme, state.emit];
|
|
553
|
+
// The pack selection follows the install like footprint/posture/mode, and is read here
|
|
554
|
+
// rather than left to the table's default: a console Build must re-emit the constitution
|
|
555
|
+
// the deployment already carries. Unknown (no `Active packs:` marker) resolves to ALL
|
|
556
|
+
// packs — never to `harness.config.json`, which would drop the consent gate.
|
|
552
557
|
const cmds = actionCommands(action, {
|
|
553
558
|
theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
|
|
559
|
+
doctrines: doctrinesForBuild(state.target),
|
|
554
560
|
});
|
|
555
561
|
if (!pyTruthy(cmds)) {
|
|
556
562
|
return sendJson(res, { error: `unknown action ${action}` }, 404, ae);
|
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": "3.1.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",
|