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/doctor.mjs
CHANGED
|
@@ -33,7 +33,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
33
33
|
import os from 'node:os';
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
CONFIG, PACK_ORDER, PLUGIN_SRC, ROOT, SRC, THEMES, WORKFLOW_SRC, makeCfg,
|
|
38
|
+
} from './checkout.mjs';
|
|
37
39
|
import {
|
|
38
40
|
buildInto, main as driverMain, emitGlobalInto, emitProjectInto,
|
|
39
41
|
} from '../bin/geneseed.mjs';
|
|
@@ -61,7 +63,10 @@ import {
|
|
|
61
63
|
// --------------------------------------------------------------------------------------
|
|
62
64
|
|
|
63
65
|
/** `_harness_core.TOKEN_RE` / `LINK_RE` / `ABS_LINK_RE`. `g` where `findall` is used. */
|
|
64
|
-
|
|
66
|
+
// Must stay in step with `js/render.mjs`'s TOKEN_RE: a token the renderer substitutes but this
|
|
67
|
+
// scan cannot see is an unresolved-token gate that silently stops gating. Digits are legal after
|
|
68
|
+
// the first character for `DOC_<PACK>_<n>`; a leading digit is still not a token.
|
|
69
|
+
const TOKEN_RE = /\{\{[A-Z_][A-Z0-9_]*\}\}/g;
|
|
65
70
|
const LINK_RE = /\]\((?!https?:\/\/|#)([^)]+)\)/g;
|
|
66
71
|
const ABS_LINK_RE = /^([A-Za-z]:[\\/]|\/|~)/;
|
|
67
72
|
const FENCE_RE = /```[\s\S]*?```/g;
|
|
@@ -660,6 +665,63 @@ export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
|
660
665
|
return problems;
|
|
661
666
|
}
|
|
662
667
|
|
|
668
|
+
/** `LAW_META_ROW`'s twin for a STRING key — `'<pack>.<n>': ['<pack>', '<principle>'],`. */
|
|
669
|
+
const DOCTRINE_META_ROW =
|
|
670
|
+
/^[^\S\n]*(['"])([a-z]+\.\d+)\1:\s*\[\s*(['"])(.*?)\3\s*,\s*(['"])(.*?)\5\s*,?\s*\]\s*,?[^\S\n]*$/gm;
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* `lawMetaProblems`' doctrine half — the console's per-rule Principle column stays complete.
|
|
674
|
+
*
|
|
675
|
+
* SAME DEFECT, ONE TIER OVER. `DOCTRINE_META` holds copy that exists nowhere else in the tree,
|
|
676
|
+
* and a rule with no row falls back to an empty principle: it renders with a blank description
|
|
677
|
+
* while every count upstream reads correct. Two invariants shipped exactly that way before
|
|
678
|
+
* `lawMetaProblems` existed, and 23 new rules is a much larger surface to lose one in.
|
|
679
|
+
*
|
|
680
|
+
* ⚠ THE CLASS ARM IS DIFFERENT FROM THE INVARIANTS'. An invariant's class is checked against
|
|
681
|
+
* `LAW_CLASSES`, a vocabulary of six; a doctrine rule has no second taxonomy — its class IS its
|
|
682
|
+
* pack — so the check is an EQUALITY against the pack in its own key. That catches the copy
|
|
683
|
+
* error a six-way vocabulary cannot: a row pasted from the pack above it, keeping the class of
|
|
684
|
+
* the pack it came from, which would colour the row and label its chip with the wrong pack.
|
|
685
|
+
*/
|
|
686
|
+
export function doctrineMetaProblems(addrs) {
|
|
687
|
+
const page = path.join(ROOT, 'web', 'src', 'pages', 'Laws.jsx');
|
|
688
|
+
let text;
|
|
689
|
+
try { text = readText(page); } catch (e) {
|
|
690
|
+
return [`[authoring] web/src/pages/Laws.jsx unreadable: ${e.message}`];
|
|
691
|
+
}
|
|
692
|
+
const block = /^const DOCTRINE_META = \{$([\s\S]*?)^\}$/m.exec(text);
|
|
693
|
+
if (!block) {
|
|
694
|
+
return ['[authoring] DOCTRINE_META literal not found in web/src/pages/Laws.jsx — the '
|
|
695
|
+
+ "console's doctrine rows would have no gate on their Principle column"];
|
|
696
|
+
}
|
|
697
|
+
const meta = new Map();
|
|
698
|
+
for (const m of block[1].matchAll(DOCTRINE_META_ROW)) meta.set(m[2], [m[4], m[6]]);
|
|
699
|
+
const problems = [];
|
|
700
|
+
const seen = new Set();
|
|
701
|
+
for (const addr of addrs) {
|
|
702
|
+
seen.add(addr);
|
|
703
|
+
if (!meta.has(addr)) {
|
|
704
|
+
problems.push(`[authoring] doctrine rule ${addr} has no row in DOCTRINE_META `
|
|
705
|
+
+ '(web/src/pages/Laws.jsx) — the console would render it with a blank Principle');
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const [klass, principle] = meta.get(addr);
|
|
709
|
+
if (!principle.trim()) {
|
|
710
|
+
problems.push(`[authoring] DOCTRINE_META['${addr}'] has an empty principle line — that `
|
|
711
|
+
+ 'rule would render with a blank description');
|
|
712
|
+
}
|
|
713
|
+
const pack = addr.split('.')[0];
|
|
714
|
+
if (klass !== pack) {
|
|
715
|
+
problems.push(`[authoring] DOCTRINE_META['${addr}'] carries pack '${klass}' — a doctrine `
|
|
716
|
+
+ `rule's class IS its pack, so this must read '${pack}'`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for (const addr of [...meta.keys()].filter((k) => !seen.has(k)).sort()) {
|
|
720
|
+
problems.push(`[authoring] DOCTRINE_META lists ${addr} but no pack file defines it`);
|
|
721
|
+
}
|
|
722
|
+
return problems;
|
|
723
|
+
}
|
|
724
|
+
|
|
663
725
|
/**
|
|
664
726
|
* `_harness_build._prose_mirror_problems` — the human-readable count mirrors, the ones the
|
|
665
727
|
* badge regex never sees.
|
|
@@ -760,6 +822,237 @@ export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '
|
|
|
760
822
|
const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
761
823
|
const LAW_HEADING_RE = /^### \{\{LAW\}\} ([IVXLCDM]+)\b/gm;
|
|
762
824
|
|
|
825
|
+
// The doctrine twins of `LAW_HEADING_RE`, over the UNRENDERED source — `js/inventory.mjs`'s
|
|
826
|
+
// pair of the same names read the rendered form. A citation is the heading minus its `### `,
|
|
827
|
+
// so one regex cannot serve both: a heading is an anchored line, a citation is anywhere.
|
|
828
|
+
const DOCTRINE_HEADING_RE = /^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/gm;
|
|
829
|
+
const DOCTRINE_CITE_RE = /\{\{DOCTRINE\}\}\s+([a-z]+)\s+(\d+)/g;
|
|
830
|
+
// `\{\{DOCTRINE\}\}` and not `DOCTRINE`: `{{DOCTRINES}}` is the SECTION noun and is legal
|
|
831
|
+
// everywhere, including in an invariant that points at the tier as a whole.
|
|
832
|
+
const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE\}\}/;
|
|
833
|
+
const DOC_TOKEN_RE = /\{\{(DOC_[A-Z0-9_]+)\}\}/g;
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* A line reported by the doctor that is NOT a failure — `[note] …` rather than `[authoring] …`.
|
|
837
|
+
*
|
|
838
|
+
* ⚠ ONE PRODUCER, AND IT IS THE ONLY THING THAT NEEDED A SECOND CHANNEL. A build with a
|
|
839
|
+
* doctrine pack off is LEGAL, and it leaves body prose citing rules that pack owns. That state
|
|
840
|
+
* has to be visible — a silent dangling citation is how prose and boundary drift apart — but
|
|
841
|
+
* reporting it as a problem would make `doctor` exit 1 over a configuration its owner chose.
|
|
842
|
+
* So `cmdDoctor` prints notes and does not count them.
|
|
843
|
+
*/
|
|
844
|
+
const NOTE = '[note] ';
|
|
845
|
+
export const isDoctorNote = (p) => p.startsWith(NOTE);
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* The three-tier constitution's own authoring gates — the doctrine twin of the `LAW_CLASS` /
|
|
849
|
+
* `LAW_META` family, plus the two purity rules the tiering rests on.
|
|
850
|
+
*
|
|
851
|
+
* IT LIVES BESIDE `countTableProblems` RATHER THAN INSIDE IT, and is called from
|
|
852
|
+
* `authoringProblems` beside `registry`/`secret`/`vendorPin`. `countTableProblems` opens by
|
|
853
|
+
* reading `AGENT.md.tmpl` and HARD-RETURNS if that read fails, so an arm placed inside it
|
|
854
|
+
* would go quiet on exactly the tree that is most broken. Nothing here needs the template's
|
|
855
|
+
* tables, and `proseMirrorProblems` — the frozen recording — is untouched either way.
|
|
856
|
+
*
|
|
857
|
+
* ⚠ THE VOCABULARY GATES ARE HERE BECAUSE `themeParityProblems` CANNOT SEE THEM. That check is
|
|
858
|
+
* presence-only and SYMMETRIC over the union of every theme's keys: a key missing from all
|
|
859
|
+
* fourteen is in parity, and a key whose VALUE breaks a parser is not its business at all. So
|
|
860
|
+
* `DOC_*` coverage, the `LEX_I..LEX_IX` equality and the single-word tier noun each need a gate
|
|
861
|
+
* that reads the SOURCE and asks what the source requires.
|
|
862
|
+
*/
|
|
863
|
+
export function constitutionProblems() {
|
|
864
|
+
const problems = [];
|
|
865
|
+
const dir = path.join(SRC, 'doctrines');
|
|
866
|
+
|
|
867
|
+
// ---- the packs themselves: present, contiguous from 1, and filed where they say they are.
|
|
868
|
+
const rules = new Map(); // 'craft.1' -> title-less marker
|
|
869
|
+
for (const pack of PACK_ORDER) {
|
|
870
|
+
const file = path.join(dir, `${pack}.md`);
|
|
871
|
+
let text;
|
|
872
|
+
try { text = readText(file); } catch (e) {
|
|
873
|
+
problems.push(`[authoring] doctrines/${pack}.md is named in PACK_ORDER but unreadable: `
|
|
874
|
+
+ `${e.message} — the build ships the whole catalogue, so a missing pack file breaks `
|
|
875
|
+
+ 'every citation into it, active or not');
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const found = [...text.matchAll(DOCTRINE_HEADING_RE)];
|
|
879
|
+
if (!found.length) {
|
|
880
|
+
problems.push(`[authoring] doctrines/${pack}.md carries no '### {{DOCTRINE}} ${pack} <n>' `
|
|
881
|
+
+ 'heading — the pack would render as an empty section');
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
found.forEach(([, named, n], i) => {
|
|
885
|
+
if (named !== pack) {
|
|
886
|
+
problems.push(`[authoring] doctrines/${pack}.md holds a rule addressed to '${named} `
|
|
887
|
+
+ `${n}' — a rule's pack is read from its heading, so this one is unreachable at `
|
|
888
|
+
+ `'${pack}.${n}' and shadows whatever ${named}.md numbers ${n}`);
|
|
889
|
+
}
|
|
890
|
+
if (Number(n) !== i + 1) {
|
|
891
|
+
problems.push(`[authoring] doctrines/${pack}.md rule ${n} is at position ${i + 1} — `
|
|
892
|
+
+ 'pack ids must run contiguously from 1, or a citation addresses a gap');
|
|
893
|
+
}
|
|
894
|
+
rules.set(`${named}.${Number(n)}`, true);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// The console's Principle column, keyed off the rules just parsed — so the gate is driven by
|
|
899
|
+
// the SOURCE and a rule added to a pack file is caught the same day, not when someone
|
|
900
|
+
// notices a blank description in the browser.
|
|
901
|
+
problems.push(...doctrineMetaProblems([...rules.keys()]));
|
|
902
|
+
|
|
903
|
+
// ---- every `{{DOCTRINE}} <pack> <n>` anywhere in src/ resolves to a rule that exists.
|
|
904
|
+
//
|
|
905
|
+
// The whole tree, not the template: a skill footer citing `process 9` is as broken as a
|
|
906
|
+
// template one, and it is the shape 339 rewired citations could have left behind.
|
|
907
|
+
const cited = new Map(); // 'process.5' -> [rel, …]
|
|
908
|
+
for (const p of rglob(SRC)) {
|
|
909
|
+
if (!isFile(p) || !(p.endsWith('.md') || p.endsWith('.tmpl'))) continue;
|
|
910
|
+
let text;
|
|
911
|
+
try { text = readText(p); } catch { continue; }
|
|
912
|
+
const rel = path.relative(SRC, p).split(path.sep).join('/');
|
|
913
|
+
// ⚠ AN ALWAYS-ON TIER MAY NEVER CITE A TOGGLEABLE ONE (D2). A `--doctrines craft` build
|
|
914
|
+
// ships invariants that point at rules its AGENT.md does not contain; the ontology is
|
|
915
|
+
// worse still, being the tier that states the order the others sit in. Doctrine->doctrine
|
|
916
|
+
// citations are fine and stay: the full catalogue ships in every bundle.
|
|
917
|
+
if ((rel.startsWith('ontology/') || rel.startsWith('laws/')) && DOCTRINE_TOKEN_RE.test(text)) {
|
|
918
|
+
problems.push(`[authoring] ${rel} cites {{DOCTRINE}} — an always-on tier may never `
|
|
919
|
+
+ 'reference a toggleable one, or a pack-off build ships a rule pointing at text '
|
|
920
|
+
+ 'that is not there. State the point directly instead');
|
|
921
|
+
}
|
|
922
|
+
for (const [, pack, n] of text.matchAll(DOCTRINE_CITE_RE)) {
|
|
923
|
+
const key = `${pack}.${Number(n)}`;
|
|
924
|
+
if (!cited.has(key)) cited.set(key, []);
|
|
925
|
+
cited.get(key).push(rel);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
for (const [key, where] of [...cited].sort()) {
|
|
929
|
+
if (rules.has(key)) continue;
|
|
930
|
+
problems.push(`[authoring] ${where[0]} cites {{DOCTRINE}} ${key.replace('.', ' ')}, which `
|
|
931
|
+
+ `no pack file defines${where.length > 1 ? ` (and ${where.length - 1} more)` : ''}`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ---- the vocabulary every theme owes the source.
|
|
935
|
+
const wanted = new Set();
|
|
936
|
+
for (const pack of PACK_ORDER) {
|
|
937
|
+
let text;
|
|
938
|
+
try { text = readText(path.join(dir, `${pack}.md`)); } catch { continue; }
|
|
939
|
+
for (const [, key] of text.matchAll(DOC_TOKEN_RE)) wanted.add(key);
|
|
940
|
+
}
|
|
941
|
+
const lexWant = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'].map((r) => `LEX_${r}`);
|
|
942
|
+
// `_TEMPLATE.json` is included — it is the file `--sync-themes` seeds a new theme from, so a
|
|
943
|
+
// stale key there propagates into every voice added afterwards. `themeFiles()` excludes it.
|
|
944
|
+
const files = [...themeFiles(), path.join(THEMES, '_TEMPLATE.json')];
|
|
945
|
+
for (const f of files) {
|
|
946
|
+
const name = path.basename(f);
|
|
947
|
+
let theme;
|
|
948
|
+
try { theme = parseJson(readText(f)); } catch (e) {
|
|
949
|
+
problems.push(`[authoring] ${name} unreadable: ${e.message}`);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (!theme || typeof theme !== 'object' || Array.isArray(theme)) continue;
|
|
953
|
+
const keys = Object.keys(theme);
|
|
954
|
+
for (const key of [...wanted].sort()) {
|
|
955
|
+
if (!Object.hasOwn(theme, key)) {
|
|
956
|
+
problems.push(`[authoring] ${name} has no {${key}} — a doctrine file names it, and `
|
|
957
|
+
+ 'theme parity cannot see a key that is missing from every theme at once');
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
for (const key of keys.filter((k) => k.startsWith('DOC_')).sort()) {
|
|
961
|
+
if (!wanted.has(key)) {
|
|
962
|
+
problems.push(`[authoring] ${name} defines {${key}}, which no doctrine file uses — a `
|
|
963
|
+
+ 'dead voice key ships a title for a rule that does not exist');
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// I1 — an EQUALITY, not a presence check. The renumber left `LEX_XXII`, `LEX_XXIII`,
|
|
967
|
+
// `LEX_XXIV` and `LEX_XXXVI` behind in all fifteen files, and parity was silent because
|
|
968
|
+
// they were absent from nowhere.
|
|
969
|
+
const lex = keys.filter((k) => k.startsWith('LEX_')).sort();
|
|
970
|
+
const stale = lex.filter((k) => !lexWant.includes(k));
|
|
971
|
+
const missing = lexWant.filter((k) => !lex.includes(k));
|
|
972
|
+
for (const k of stale) {
|
|
973
|
+
problems.push(`[authoring] ${name} still defines {${k}} — the invariants are I..IX and `
|
|
974
|
+
+ 'nothing renders this key, so it is a title for a rule that no longer exists');
|
|
975
|
+
}
|
|
976
|
+
for (const k of missing) {
|
|
977
|
+
problems.push(`[authoring] ${name} has no {${k}} — one of the nine invariants would `
|
|
978
|
+
+ 'render with an empty title');
|
|
979
|
+
}
|
|
980
|
+
// M10 — ⚠ BOTH HEADING PARSERS MATCH THE TIER NOUN WITH `\S+`. A two-word value does not
|
|
981
|
+
// error; the heading stops matching and the tier silently parses to nothing. `_TEMPLATE`'s
|
|
982
|
+
// values are descriptive placeholders, so only real voices are held to this.
|
|
983
|
+
if (name === '_TEMPLATE.json') continue;
|
|
984
|
+
for (const key of ['LAW', 'DOCTRINE']) {
|
|
985
|
+
const v = theme[key];
|
|
986
|
+
if (typeof v === 'string' && /\s/.test(v)) {
|
|
987
|
+
problems.push(`[authoring] ${name} spells {${key}} as ${pyRepr(v)} — the heading `
|
|
988
|
+
+ 'parsers match the tier noun with \\S+, so a value with a space makes every '
|
|
989
|
+
+ `${key === 'LAW' ? 'invariant' : 'doctrine rule'} parse to nothing, in silence`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ---- every `§N` in src/ addresses a section the template actually declares.
|
|
995
|
+
//
|
|
996
|
+
// ⚠ THIS GATE IS NARROW AND SAYS SO. Inserting `## 2. Doctrines` pushed every later section
|
|
997
|
+
// down one, and the sweep that fixed the template's own cross-references stopped at the
|
|
998
|
+
// template — four satellites under `src/skills/` and `src/agents/` kept pointing at the
|
|
999
|
+
// section that used to be there, and they RENDER INTO EVERY BUNDLE. A range check would not
|
|
1000
|
+
// have caught those: §7 still existed, it had just stopped being the Wiki.
|
|
1001
|
+
//
|
|
1002
|
+
// What it does catch is the other half of a renumber — a pointer past the end, which is what
|
|
1003
|
+
// REMOVING a section leaves behind — and it is nearly free. The half it cannot catch is a
|
|
1004
|
+
// judgement about intent, and `docs/extending.md` carries the manual sweep for it. Naming
|
|
1005
|
+
// that limit here is the point: a reader who assumes this gate is total will skip the sweep.
|
|
1006
|
+
const declared = new Set();
|
|
1007
|
+
// Read here rather than borrowing `countTableProblems`' copy: that function hard-returns when
|
|
1008
|
+
// the template is unreadable, and this gate must not inherit a silence it did not choose.
|
|
1009
|
+
let tmpl = '';
|
|
1010
|
+
try { tmpl = readText(path.join(SRC, 'AGENT.md.tmpl')); } catch { tmpl = ''; }
|
|
1011
|
+
for (const [, n] of tmpl.matchAll(/^## (\d+)\.\s/gm)) declared.add(Number(n));
|
|
1012
|
+
if (declared.size) {
|
|
1013
|
+
for (const p of rglob(SRC)) {
|
|
1014
|
+
if (!isFile(p) || !(p.endsWith('.md') || p.endsWith('.tmpl'))) continue;
|
|
1015
|
+
let text;
|
|
1016
|
+
try { text = readText(p); } catch { continue; }
|
|
1017
|
+
const rel = path.relative(SRC, p).split(path.sep).join('/');
|
|
1018
|
+
for (const [, n] of text.matchAll(/§(\d+)/g)) {
|
|
1019
|
+
if (declared.has(Number(n))) continue;
|
|
1020
|
+
problems.push(`[authoring] ${rel} cites AGENT.md §${n}, and the template declares no `
|
|
1021
|
+
+ `such section ${pyRepr([...declared].sort((a, b) => a - b).map(String))} — a `
|
|
1022
|
+
+ 'renumber moved it, and this pointer renders into every bundle');
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ---- the build default names packs that exist.
|
|
1028
|
+
let cfgDoctrines = null;
|
|
1029
|
+
try {
|
|
1030
|
+
const cfg = parseJson(readText(CONFIG));
|
|
1031
|
+
if (cfg && typeof cfg === 'object' && Array.isArray(cfg.doctrines)) cfgDoctrines = cfg.doctrines;
|
|
1032
|
+
} catch { /* no config, or unreadable — `configDefaults()` is forgiving here too */ }
|
|
1033
|
+
if (cfgDoctrines) {
|
|
1034
|
+
for (const pack of cfgDoctrines) {
|
|
1035
|
+
if (!PACK_ORDER.includes(pack)) {
|
|
1036
|
+
problems.push(`[authoring] harness.config.json names doctrine pack ${pyRepr(pack)}, `
|
|
1037
|
+
+ `which this checkout does not ship ${pyRepr([...PACK_ORDER])} — every build reading `
|
|
1038
|
+
+ 'that default would fail');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// ---- D5, a NOTE and not a problem: what a narrowed default leaves pointing at the shelf.
|
|
1044
|
+
const on = cfgDoctrines ?? [...PACK_ORDER];
|
|
1045
|
+
const dangling = [...cited].filter(([k]) => rules.has(k) && !on.includes(k.split('.')[0]));
|
|
1046
|
+
if (dangling.length) {
|
|
1047
|
+
const packs = [...new Set(dangling.map(([k]) => k.split('.')[0]))].sort();
|
|
1048
|
+
problems.push(`${NOTE}harness.config.json builds ${pyRepr(on)}, and ${dangling.length} `
|
|
1049
|
+
+ `citation(s) in src/ point into ${packs.join(', ')}. Those rules still SHIP — every `
|
|
1050
|
+
+ 'pack file is in the bundle — so the reference resolves on disk; it is the rendered '
|
|
1051
|
+
+ 'AGENT.md that will not contain them. Legal, and recorded here so it is not silent');
|
|
1052
|
+
}
|
|
1053
|
+
return problems;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
763
1056
|
/**
|
|
764
1057
|
* `_harness_build._count_table_problems` — the hand-authored AGENT.md tables and the README
|
|
765
1058
|
* badges, against `src/`.
|
|
@@ -987,6 +1280,7 @@ export function authoringProblems() {
|
|
|
987
1280
|
problems.push(...registryProblems());
|
|
988
1281
|
problems.push(...secretProblems());
|
|
989
1282
|
problems.push(...vendorPinProblems());
|
|
1283
|
+
problems.push(...constitutionProblems());
|
|
990
1284
|
problems.push(...countTableProblems());
|
|
991
1285
|
return problems;
|
|
992
1286
|
}
|
|
@@ -1188,17 +1482,26 @@ function captureBoth(fn) {
|
|
|
1188
1482
|
* maintainer sweep. The cross-theme parity check runs in every mode.
|
|
1189
1483
|
*/
|
|
1190
1484
|
export function cmdDoctor(args) {
|
|
1191
|
-
const [themes,
|
|
1485
|
+
const [themes, collected] = doctorCollect({
|
|
1192
1486
|
theme: args.theme, allThemes: Boolean(args.all), bundle: args.bundle,
|
|
1193
1487
|
noBundle: Boolean(args.noBundle),
|
|
1194
1488
|
});
|
|
1195
1489
|
if (!themes.length) {
|
|
1196
|
-
pyPrint(`${
|
|
1490
|
+
pyPrint(`${collected.length ? collected[0] : '[doctor] no themes found'}\n`);
|
|
1197
1491
|
return 1;
|
|
1198
1492
|
}
|
|
1493
|
+
// ⚠ NOTES ARE PRINTED AND NOT COUNTED. The only producer is the pack-off citation report in
|
|
1494
|
+
// `constitutionProblems`, and the state it describes is one the install's owner chose: a
|
|
1495
|
+
// build that leaves body prose citing a pack it did not render. Silence there is how prose
|
|
1496
|
+
// and boundary drift apart, and a non-zero exit there is doctor failing a legal
|
|
1497
|
+
// configuration. Nothing in the recorded CLI corpus produces one — the checkout's own
|
|
1498
|
+
// `harness.config.json` names no packs — so the byte-compared output is unmoved.
|
|
1499
|
+
const notes = collected.filter(isDoctorNote);
|
|
1500
|
+
const problems = collected.filter((p) => !isDoctorNote(p));
|
|
1199
1501
|
const scoped = !args.theme && !args.all && themes.length === 1;
|
|
1200
1502
|
const note = scoped
|
|
1201
1503
|
? ` (scoped to installed theme '${themes[0]}'; run with --all to sweep every theme)` : '';
|
|
1504
|
+
for (const n of notes) pyPrint(`${n}\n`);
|
|
1202
1505
|
if (problems.length) {
|
|
1203
1506
|
pyPrint(`[doctor] ${problems.length} problem(s) across ${themes.length} theme(s):\n`);
|
|
1204
1507
|
for (const p of problems) pyPrint(` - ${p}\n`);
|
package/js/emit.mjs
CHANGED
|
@@ -87,8 +87,29 @@ import {
|
|
|
87
87
|
} from './lib/fs.mjs';
|
|
88
88
|
/** `_build_render.SRC_DIRS_MARKER`. */
|
|
89
89
|
const SRC_DIRS_MARKER = '.geneseed-srcdirs.json';
|
|
90
|
-
/**
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* `_build_render.OWNED_SRC_DIRS` — wiped and regenerated each run.
|
|
92
|
+
*
|
|
93
|
+
* `ontology` and `doctrines` are appended, not inserted: this array's iteration order is the
|
|
94
|
+
* key order of the `.geneseed-srcdirs.json` marker it fills, and appending keeps the three
|
|
95
|
+
* original keys where an existing install's marker already has them.
|
|
96
|
+
*
|
|
97
|
+
* They belong here for the same reason `laws` does — they are GENERATED, so an install must
|
|
98
|
+
* not keep a copy the source no longer produces. Without them a build writes the dirs on
|
|
99
|
+
* every run and never wipes them, which turns the doctrines toggle into a one-way switch:
|
|
100
|
+
* disable a pack, rebuild, and its rendered file sits in the install forever, contradicting
|
|
101
|
+
* the `Active packs:` line in the AGENT.md right beside it.
|
|
102
|
+
*/
|
|
103
|
+
const OWNED_SRC_DIRS = ['laws', 'agents', 'skills', 'ontology', 'doctrines'];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The constitution dirs a LEAN emit re-ships in full text — see `shipLeanLaws`.
|
|
107
|
+
*
|
|
108
|
+
* Not all of `OWNED_SRC_DIRS`: `agents` and `skills` are already written as whole files by
|
|
109
|
+
* the native layer, and only the three constitution dirs are inlined into AGENT.md in a
|
|
110
|
+
* form (terse, or pack-filtered) that loses text the reader may still need.
|
|
111
|
+
*/
|
|
112
|
+
const LEAN_FULL_TEXT_DIRS = ['laws', 'ontology', 'doctrines'];
|
|
92
113
|
|
|
93
114
|
/**
|
|
94
115
|
* `_build_core.CAPABILITY_LINK_RE` — the DEFAULT only.
|
|
@@ -824,7 +845,8 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
824
845
|
// (`_rel_under` is the Python side by design), and only the BASENAME is consumed
|
|
825
846
|
// downstream: `opencode.json`, or the `.jsonc` sibling when that is what is on disk.
|
|
826
847
|
phaseLog('WIRE');
|
|
827
|
-
const cfgName = path.basename(mergeOpencodeJson(path.join(root, 'opencode.json'), agentPath
|
|
848
|
+
const cfgName = path.basename(mergeOpencodeJson(path.join(root, 'opencode.json'), agentPath,
|
|
849
|
+
cfg.doctrines));
|
|
828
850
|
|
|
829
851
|
return {
|
|
830
852
|
owned,
|
|
@@ -929,16 +951,27 @@ function globalNotebook(cfgDir, items, legacy, srcRoot) {
|
|
|
929
951
|
* output besides the files is what it APPENDS to `owned` — which travels back in the
|
|
930
952
|
* payload for the manifest and the prune. It reads nothing any wiring stage writes, so
|
|
931
953
|
* its new position is not merely legal, it has no dependency on the old one.
|
|
954
|
+
*
|
|
955
|
+
* NO LONGER LEAN-ONLY, and the name is kept because the lean case is still the whole reason
|
|
956
|
+
* it exists. `doctrines` ships at BOTH footprints, because the two footprints lose different
|
|
957
|
+
* things: lean loses text (every rule is truncated to its first sentence, so the full text has
|
|
958
|
+
* to be on disk), while FULL loses whole packs (only the active ones are inlined, so the
|
|
959
|
+
* inactive ones have to be on disk). AGENT.md tells the agent it may read a rule in a pack
|
|
960
|
+
* that is not active, and `src/doctrines/README.md` rests cross-pack citations on exactly
|
|
961
|
+
* that — a claim the emit has to make true, not merely the `files` bundle.
|
|
932
962
|
*/
|
|
933
|
-
function shipLeanLaws(items, theme, cfgDir, owned) {
|
|
934
|
-
|
|
963
|
+
function shipLeanLaws(items, theme, cfgDir, owned, footprint) {
|
|
964
|
+
// Themed names, resolved through the same token table `build`'s wipe resolves, so the dir
|
|
965
|
+
// a lean emit re-ships and the dir it prunes can never be two different spellings.
|
|
966
|
+
const wanted = footprint === 'lean' ? LEAN_FULL_TEXT_DIRS : ['doctrines'];
|
|
967
|
+
const dirs = new Set(wanted.map((d) => theme[SRC_DIR_TOKENS[d]] ?? d));
|
|
935
968
|
for (const { rel, text } of items) {
|
|
936
969
|
const parts = rel.split('/');
|
|
937
970
|
// `parts.length` mirrors Python's `parts and parts[0]`, where `Path("").parts` really
|
|
938
971
|
// is empty. `String.split` never returns an empty array, so this half can never be
|
|
939
972
|
// false here — kept for the mirror, and its mutation stays green because there is
|
|
940
973
|
// nothing to detect, not because the gate cannot see it.
|
|
941
|
-
if (text !== null && parts.length && parts[0]
|
|
974
|
+
if (text !== null && parts.length && dirs.has(parts[0])) {
|
|
942
975
|
const dest = path.join(cfgDir, ...parts);
|
|
943
976
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
944
977
|
writeText(dest, text);
|
|
@@ -1039,7 +1072,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
1039
1072
|
writeVersion(cfg, cfgDir);
|
|
1040
1073
|
owned.push(VERSION_MARKER);
|
|
1041
1074
|
|
|
1042
|
-
|
|
1075
|
+
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
1043
1076
|
|
|
1044
1077
|
// WIRE — the one file of this emit the user co-owns, and the last render is now behind
|
|
1045
1078
|
// us. Inlined the way `emitOpencodeRender`'s is (one call, the same merge, the same
|
|
@@ -1047,7 +1080,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
1047
1080
|
// that split is what `tests/test_emit_phase_order.py` walks, and it walks the Python.
|
|
1048
1081
|
phaseLog('WIRE');
|
|
1049
1082
|
const cfgName = path.basename(mergeOpencodeJson(path.join(cfgDir, 'opencode.json'),
|
|
1050
|
-
agentPath));
|
|
1083
|
+
agentPath, cfg.doctrines));
|
|
1051
1084
|
|
|
1052
1085
|
return {
|
|
1053
1086
|
owned,
|
|
@@ -1170,7 +1203,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
1170
1203
|
writeVersion(cfg, cfgDir);
|
|
1171
1204
|
owned.push(VERSION_MARKER);
|
|
1172
1205
|
|
|
1173
|
-
|
|
1206
|
+
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
1174
1207
|
|
|
1175
1208
|
// The last render of the emit, and the only one whose product is not written here.
|
|
1176
1209
|
// Bob at GLOBAL scope gets no managed block at all, so nothing is rendered for it —
|
|
@@ -1183,7 +1216,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
1183
1216
|
|
|
1184
1217
|
// WIRE — see `claudeWire` below. One child per emit, so the two halves are two
|
|
1185
1218
|
// functions rather than two spawns; the seam between them is a call, not a process.
|
|
1186
|
-
const managed = claudeWire(job, claudeMdText, agentText !== null);
|
|
1219
|
+
const managed = claudeWire(job, claudeMdText, agentText !== null, cfg.doctrines);
|
|
1187
1220
|
|
|
1188
1221
|
return {
|
|
1189
1222
|
owned,
|
|
@@ -1218,7 +1251,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
1218
1251
|
* not needed for it: Python tests `os.environ.get(...)` for truthiness and an env var is
|
|
1219
1252
|
* always a string, so `''` is the only falsy value either language sees.
|
|
1220
1253
|
*/
|
|
1221
|
-
function claudeWire(job, claudeMdText, hasAgentText) {
|
|
1254
|
+
function claudeWire(job, claudeMdText, hasAgentText, doctrines = null) {
|
|
1222
1255
|
phaseLog('WIRE');
|
|
1223
1256
|
const { cfgDir, claudeMd, scope, host, oldManaged, preambleExclude, hookOpts } = job;
|
|
1224
1257
|
const old = oldManaged && typeof oldManaged === 'object' && !Array.isArray(oldManaged)
|
|
@@ -1274,8 +1307,12 @@ function claudeWire(job, claudeMdText, hasAgentText) {
|
|
|
1274
1307
|
// The merge prunes recorded groups that are no longer canonical and returns the
|
|
1275
1308
|
// COMPLETE current claim set — store it as-is; unioning with prior would resurrect the
|
|
1276
1309
|
// stale claims.
|
|
1310
|
+
// `doctrines` decides whether the git-gate group is part of the canonical claim set. It
|
|
1311
|
+
// travels from `cfg` rather than being re-read off the deployment: this is the emit that
|
|
1312
|
+
// DECIDES the install's packs, so the marker on disk is still the previous build's.
|
|
1277
1313
|
const [, managedHooks] = mergeClaudeSettings(
|
|
1278
1314
|
settingsPath, scope, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
|
|
1315
|
+
doctrines,
|
|
1279
1316
|
);
|
|
1280
1317
|
managed.settings_hooks = managedHooks;
|
|
1281
1318
|
|
package/js/generate.mjs
CHANGED
|
@@ -33,11 +33,11 @@ import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
|
|
35
35
|
import { main as driverMain } from '../bin/geneseed.mjs';
|
|
36
|
-
import { makeCfg } from './checkout.mjs';
|
|
36
|
+
import { makeCfg, PACK_ORDER } from './checkout.mjs';
|
|
37
37
|
import { opencodeConfigDir, pyResolve } from './hosts.mjs';
|
|
38
38
|
import {
|
|
39
|
-
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme,
|
|
40
|
-
installTargets, modeOfDir, postureOfDir, readMaybe, themeOfDir,
|
|
39
|
+
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
|
|
40
|
+
footprintOfDir, installState, installTargets, modeOfDir, postureOfDir, readMaybe, themeOfDir,
|
|
41
41
|
} from './installs.mjs';
|
|
42
42
|
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from './opencode.mjs';
|
|
43
43
|
import { renderAll } from './render.mjs';
|
|
@@ -101,9 +101,28 @@ export function cmdBuild(args) {
|
|
|
101
101
|
* because omitting it when it matched the generator's default silently coupled the answer to
|
|
102
102
|
* whatever that default happened to be — the moment the default moved, picking the old one
|
|
103
103
|
* produced the new one. `posture` and `mode` still elide at theirs, and neither has moved.
|
|
104
|
+
*
|
|
105
|
+
* ⚠ `doctrines` DOES NOT ELIDE, AND THAT IS THE SAFETY DECISION, NOT A STYLE ONE. It used to
|
|
106
|
+
* elide on the full set, by analogy with posture and mode — but those two compare against a
|
|
107
|
+
* FROZEN LITERAL (`'peer'`, `'direct'`), while the default a missing `--doctrines` actually
|
|
108
|
+
* lands on is `configDefaults()`, which reads `harness.config.json`. With `{"doctrines":
|
|
109
|
+
* ["craft"]}` in that file, an install carrying all four packs re-emitted by `upgrade` was
|
|
110
|
+
* silently narrowed to one — and the narrowing takes the commit/push consent gate off an
|
|
111
|
+
* install whose owner never asked for that. Same trap as `footprint`, one axis over, with a
|
|
112
|
+
* boundary behind it. A caller that knows the selection now always says so.
|
|
113
|
+
*
|
|
114
|
+
* `allPacks` survives the elision's removal because it is still what CANONICALISES the value:
|
|
115
|
+
* the flag is emitted in the checkout's pack order, not the order handed in, and this function
|
|
116
|
+
* is pure and has no discovery of its own.
|
|
117
|
+
*
|
|
118
|
+
* Two states: `null` means "the caller expressed no opinion" and emits no flag at all, so the
|
|
119
|
+
* generator falls back to `harness.config.json` — the only route left to that fallback, and it
|
|
120
|
+
* is reached only where nothing was read back off an install; `[]` is a deliberate empty
|
|
121
|
+
* selection and emits `--doctrines none`, which is the spelling the driver's `parseDoctrines`
|
|
122
|
+
* accepts (an empty `--doctrines ` is a usage error there, so it can never be produced here).
|
|
104
123
|
*/
|
|
105
124
|
export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
|
|
106
|
-
posture = 'peer', mode = 'direct') {
|
|
125
|
+
posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER) {
|
|
107
126
|
const argv = ['--theme', theme, '--emit', emit];
|
|
108
127
|
if (!['opencode-global', 'claude-global', 'bob-global', 'copilot-global'].includes(emit)) {
|
|
109
128
|
if (out) argv.push('--out', out);
|
|
@@ -112,6 +131,13 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
|
|
|
112
131
|
if (footprint) argv.push('--footprint', footprint);
|
|
113
132
|
if (posture && posture !== 'peer') argv.push('--posture', posture);
|
|
114
133
|
if (mode && mode !== 'direct') argv.push('--mode', mode);
|
|
134
|
+
if (Array.isArray(doctrines)) {
|
|
135
|
+
// Emitted in `allPacks` order, not the order handed in: a caller may legitimately pass an
|
|
136
|
+
// unsorted list, and the flag has to be canonical for the same reason the driver
|
|
137
|
+
// re-normalises on parse — the `Active packs:` marker is compared against itself.
|
|
138
|
+
const picked = allPacks.filter((p) => doctrines.includes(p));
|
|
139
|
+
argv.push('--doctrines', picked.length ? picked.join(',') : 'none');
|
|
140
|
+
}
|
|
115
141
|
return argv;
|
|
116
142
|
}
|
|
117
143
|
|
|
@@ -167,7 +193,15 @@ export function cmdRebuildAll() {
|
|
|
167
193
|
const posture = postureOfDir(root) || defaultPosture();
|
|
168
194
|
const mode = modeOfDir(root) || defaultMode();
|
|
169
195
|
const out = scope === 'global' ? null : root;
|
|
170
|
-
|
|
196
|
+
// SIX values now, not five. A rebuild that defaulted the pack selection would re-emit an
|
|
197
|
+
// install into a constitution its owner did not choose — and in one direction that is not
|
|
198
|
+
// merely surprising: dropping the process pack takes the commit/push consent RULES out of
|
|
199
|
+
// AGENT.md while `claudeHookGroups` keeps the gate wired, or the reverse. ⚠ AND THE
|
|
200
|
+
// FALLBACK IS NOT `harness.config.json`: a pre-migration carrier has no marker, and
|
|
201
|
+
// resolving that silence out of a machine-wide config narrows every such install on its
|
|
202
|
+
// first upgrade. `doctrinesForBuild` resolves unknown to ALL packs — see its docblock.
|
|
203
|
+
const doctrines = doctrinesForBuild(root);
|
|
204
|
+
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines);
|
|
171
205
|
const label = `${host}:${scope} (${root})`;
|
|
172
206
|
pyPrint(`[rebuild-all] ${label}: theme=${theme} emit=${emit} footprint=${footprint} `
|
|
173
207
|
+ `posture=${posture} mode=${mode}\n`);
|
package/js/hooks.mjs
CHANGED
|
@@ -214,7 +214,7 @@ const out = pyPrint;
|
|
|
214
214
|
const err = pyPrintErr;
|
|
215
215
|
|
|
216
216
|
// ======================================================================================
|
|
217
|
-
// context — project-context discovery
|
|
217
|
+
// context — project-context discovery
|
|
218
218
|
// ======================================================================================
|
|
219
219
|
|
|
220
220
|
// Kept in step with adapters/opencode/plugins/geneseed-context.js, same as the Python.
|
|
@@ -494,7 +494,7 @@ export function cmdContext(args) {
|
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
const lines = [
|
|
497
|
-
`=== PROJECT CONTEXT \u2014 binding for this repo
|
|
497
|
+
`=== PROJECT CONTEXT \u2014 binding for this repo (via ${source}) ===`,
|
|
498
498
|
'',
|
|
499
499
|
];
|
|
500
500
|
for (const entry of eager) {
|
|
@@ -525,7 +525,7 @@ export function cmdContext(args) {
|
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
// ======================================================================================
|
|
528
|
-
// git-gate —
|
|
528
|
+
// git-gate — Doctrine process 5's tool-boundary backstop
|
|
529
529
|
// ======================================================================================
|
|
530
530
|
|
|
531
531
|
// A `git` verb ANYWHERE in the command trips the gate, including the chained
|
|
@@ -555,12 +555,13 @@ export function cmdGitGate(args) {
|
|
|
555
555
|
return 0;
|
|
556
556
|
}
|
|
557
557
|
if (typeof command !== 'string' || !GIT_GATE_RE.test(command)) return 0;
|
|
558
|
-
out(askDecision('Geneseed
|
|
558
|
+
out(askDecision('Geneseed Doctrine process 5 \u2014 every git commit/push needs explicit '
|
|
559
|
+
+ 'approval'));
|
|
559
560
|
return 0;
|
|
560
561
|
}
|
|
561
562
|
|
|
562
563
|
// ======================================================================================
|
|
563
|
-
// rule-gate —
|
|
564
|
+
// rule-gate — Doctrine process 1's tool-boundary backstop
|
|
564
565
|
// ======================================================================================
|
|
565
566
|
|
|
566
567
|
/**
|
|
@@ -604,8 +605,8 @@ export function cmdRuleGate(args) {
|
|
|
604
605
|
if (typeof p !== 'string' || !p) return 0;
|
|
605
606
|
const target = ruleGateTarget(p, args.root);
|
|
606
607
|
if (!target) return 0;
|
|
607
|
-
out(askDecision(`Geneseed
|
|
608
|
-
+ "remember? That choice is the user's. Run the rule skill first."));
|
|
608
|
+
out(askDecision(`Geneseed Doctrine process 1 \u2014 writing to ${target}: a standing rule, or a `
|
|
609
|
+
+ "fact to remember? That choice is the user's. Run the rule skill first."));
|
|
609
610
|
return 0;
|
|
610
611
|
}
|
|
611
612
|
|