geneseed 3.2.0 → 3.3.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 +110 -0
- package/README.md +22 -12
- package/SETUP.md +38 -7
- package/SHIPPED.md +6 -5
- package/adapters/bob/README.md +40 -10
- package/adapters/claude-code/README.md +5 -0
- package/adapters/claude-code/settings.json +8 -1
- package/adapters/copilot/README.md +50 -24
- package/adapters/opencode/README.md +5 -1
- package/adapters/opencode/plugins/geneseed-guard.js +40 -6
- package/bin/build-driver.mjs +16 -10
- package/bin/geneseed-cli.mjs +55 -56
- package/bin/geneseed-hook.mjs +21 -14
- package/docs/candidate-hosts.md +74 -0
- package/docs/extending.md +6 -5
- package/docs/token-footprint.md +32 -21
- package/docs/web-ui.md +5 -13
- package/harness.config.json +1 -1
- package/js/README.md +5 -6
- package/js/build/emit-claude.mjs +22 -7
- package/js/build/render.mjs +37 -19
- package/js/build/source.mjs +22 -2
- package/js/cli-table.json +84 -0
- package/js/hosts/hooks.mjs +240 -78
- package/js/hosts/hosts.mjs +56 -2
- package/js/hosts/link.mjs +1 -1
- package/js/hosts/mcp.mjs +3 -1
- package/js/hosts/native.mjs +17 -0
- package/js/hosts/settings.mjs +173 -5
- package/js/inspect/excludes.mjs +1 -1
- package/js/inspect/inventory.mjs +1 -1
- package/js/inspect/status.mjs +50 -2
- package/js/maintain/uninstall.mjs +38 -11
- package/js/maintain/update.mjs +2 -2
- package/js/web/actions.mjs +8 -8
- package/js/web/api.mjs +5 -10
- package/js/web/docs.mjs +9 -31
- package/js/web/handler.mjs +20 -30
- package/js/web/jobs.mjs +5 -26
- package/js/web/routes.mjs +18 -84
- package/js/web/server.mjs +3 -3
- package/package.json +1 -1
- package/registry.json +14 -0
- package/src/AGENT.md.tmpl +55 -16
- package/src/agents/explorer.md +1 -1
- package/src/doctrines/README.md +3 -3
- package/src/doctrines/rigor.md +13 -0
- package/src/laws/universal.md +98 -20
- package/src/ontology/universal.md +38 -13
- package/src/skills/_template.md +5 -1
- package/src/skills/bruno-collection-generator.md +63 -0
- package/src/skills/bruno-test-writer.md +62 -0
- package/src/skills/crash-course.md +1 -0
- package/src/skills/decode.md +1 -0
- package/src/skills/drill.md +1 -0
- package/src/skills/feynman.md +1 -0
- package/src/skills/forge-mcp.md +1 -1
- package/src/skills/herdr.md +1 -0
- package/src/skills/learning-path.md +1 -0
- package/src/skills/opencode-theme.md +1 -0
- package/src/skills/research.md +1 -1
- package/themes/_TEMPLATE.json +3 -0
- package/themes/biker.json +3 -0
- package/themes/commentator.json +3 -0
- package/themes/cyberpunk.json +3 -0
- package/themes/gamer.json +3 -0
- package/themes/imperial.json +3 -0
- package/themes/joker.json +3 -0
- package/themes/marvin.json +3 -0
- package/themes/mean.json +3 -0
- package/themes/military.json +3 -0
- package/themes/neutral.json +3 -0
- package/themes/pirate.json +3 -0
- package/themes/sports.json +3 -0
- package/themes/verstappen.json +3 -0
- package/themes/wizard.json +3 -0
- package/web/dist/assets/Activity-BfVcLrda.js +1 -0
- package/web/dist/assets/ActivityDetail-CQXccGPl.js +1 -0
- package/web/dist/assets/{CatalogRow-DqS4e5XM.js → CatalogRow-DALEQImZ.js} +1 -1
- package/web/dist/assets/{Diff-CIrGbiqm.js → Diff-BIjxXx46.js} +1 -1
- package/web/dist/assets/{Doctor-CpI53oHY.js → Doctor-CJLbgsrw.js} +1 -1
- package/web/dist/assets/FilterInput-CkUCk_lY.js +1 -0
- package/web/dist/assets/GreenhouseView-DfC5IILJ.js +2 -0
- package/web/dist/assets/{Harness-vRYs11kX.js → Harness-Dm5CCU-v.js} +1 -1
- package/web/dist/assets/JournalView-Cpu2eI4s.js +1 -0
- package/web/dist/assets/Laws-DDDVFuQz.js +3 -0
- package/web/dist/assets/Library-Dao3T9na.js +1 -0
- package/web/dist/assets/{Markdown-kgpg9EQM.js → Markdown-C__nuWX5.js} +1 -1
- package/web/dist/assets/OperatorHudView-DEpCtAjP.js +2 -0
- package/web/dist/assets/{Profile-CHr-PPf6.js → Profile-BlXtUlh2.js} +1 -1
- package/web/dist/assets/{Rules-CsC6hfqk.js → Rules-DIre6gxR.js} +1 -1
- package/web/dist/assets/Skills-rY13ZPhM.js +1 -0
- package/web/dist/assets/{StatusBadge-gRrak0nO.js → StatusBadge-wW_RS_my.js} +1 -1
- package/web/dist/assets/{activity-B0-FXZCj.js → activity-DpYPKNrA.js} +1 -1
- package/web/dist/assets/{index-DJOhOwwR.js → index-BEZvPvpF.js} +1 -1
- package/web/dist/assets/index-DNQE5ydy.js +10 -0
- package/web/dist/assets/index-DzdQjvM1.css +1 -0
- package/web/dist/assets/{index-Bx3I7cRW.js → index-PmQGRS1X.js} +1 -1
- package/web/dist/assets/jobBuckets-DfTUJUxR.js +1 -0
- package/web/dist/assets/lawCats-DUS9UTH4.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +82 -64
- package/js/web/graph.mjs +0 -0
- package/web/dist/assets/Activity-BTCG2gMl.js +0 -1
- package/web/dist/assets/ActivityDetail-julsmPDB.js +0 -1
- package/web/dist/assets/Graph-CS4tA7YS.js +0 -1
- package/web/dist/assets/Laws-BjbHJrJt.js +0 -3
- package/web/dist/assets/Library-CjC7BAWN.js +0 -1
- package/web/dist/assets/Skills-BvatJGfe.js +0 -1
- package/web/dist/assets/index-BnRWQbZb.js +0 -12
- package/web/dist/assets/index-D0f9tJlA.css +0 -1
- package/web/dist/assets/roman-DkYN0LX9.js +0 -1
package/js/hosts/settings.mjs
CHANGED
|
@@ -667,11 +667,34 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
667
667
|
* belongs in — the user's call — not how work is run, so it is not the process pack's to
|
|
668
668
|
* remove. `default` here means unknown, and unknown keeps the gate (see `processPackOn`).
|
|
669
669
|
*/
|
|
670
|
-
export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = []) {
|
|
670
|
+
export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [], host = 'claude') {
|
|
671
671
|
const run = hookPrefix(hookOpts);
|
|
672
672
|
const mem = `--memory "${path.join(cfg, 'memory')}"`;
|
|
673
673
|
// --root carries the install's own dir so a GLOBAL hook can stand down when a project
|
|
674
674
|
// install of the same host sits at/above cwd (project-bypasses-global).
|
|
675
|
+
if (host === 'bob') {
|
|
676
|
+
// BOB'S OWN HOOK CONTRACT, per its hooks doc and 2.0.2 changelog (docs/reviews/
|
|
677
|
+
// bob-global-injection-2026-09.md): FIVE events — SessionStart, UserPromptSubmit,
|
|
678
|
+
// PreToolUse, PostToolUse, Stop — Claude's names, but NOT Claude's protocol. stdout is
|
|
679
|
+
// read as context on SessionStart only; on PreToolUse it is ignored and the one way to
|
|
680
|
+
// refuse a call is EXIT CODE 2. The groups this emit wrote before were Claude's whole
|
|
681
|
+
// set: `SubagentStop`/`PreCompact` are not Bob events, and the gates answered with a
|
|
682
|
+
// stdout JSON Bob never reads — permissive on every call. `--host bob` makes the gates
|
|
683
|
+
// speak exit codes (js/hosts/hooks.mjs). No matcher on PreToolUse: Bob's tool names
|
|
684
|
+
// are undocumented (its IDE and Shell differ), so `tool-gate` reads the payload's shape
|
|
685
|
+
// instead. Which tool payload fields Bob sends is equally undocumented; the gates read
|
|
686
|
+
// Claude's (`command`, `file_path`/`path`, `content`/`new_string`) and defer on anything
|
|
687
|
+
// else. Unverified live: no Bob install on the authoring machine.
|
|
688
|
+
const b = ' --host bob';
|
|
689
|
+
return {
|
|
690
|
+
SessionStart: [{ hooks: [{ type: 'command', command: `${run} context --root "${cfg}"${b} || exit 0` }] }],
|
|
691
|
+
// One group, both gates: process 5's consent is a stderr line here (Bob has no ask
|
|
692
|
+
// tier), so the pack toggle that drops Claude's git-gate group has nothing to drop —
|
|
693
|
+
// `tool-gate` only ever EXITS 2 for Laws I and IV, which every build carries.
|
|
694
|
+
PreToolUse: [{ hooks: [{ type: 'command', command: `${run} tool-gate --root "${cfg}"${b}` }] }],
|
|
695
|
+
Stop: [{ hooks: [{ type: 'command', command: `${run} learn ${mem} || exit 0` }] }],
|
|
696
|
+
};
|
|
697
|
+
}
|
|
675
698
|
const context = `${run} context --root "${cfg}" || exit 0`;
|
|
676
699
|
const gate = `${run} git-gate --root "${cfg}"`;
|
|
677
700
|
const ruleGate = `${run} rule-gate --root "${cfg}"`;
|
|
@@ -687,7 +710,10 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [])
|
|
|
687
710
|
],
|
|
688
711
|
SessionStart: [
|
|
689
712
|
{ matcher: 'startup|clear', hooks: [{ type: 'command', command: context }] },
|
|
690
|
-
|
|
713
|
+
// `compact` too: auto-compaction keeps the instruction file (the host re-reads it) but
|
|
714
|
+
// summarises away the eagerly injected context, memory index and notebook TOC. Re-seed
|
|
715
|
+
// them the way a resume does — the static AGENT.md is NOT re-printed.
|
|
716
|
+
{ matcher: 'resume|compact', hooks: [{ type: 'command', command: context }] },
|
|
691
717
|
],
|
|
692
718
|
// `|| exit 0` (not `|| true`): hooks run under cmd.exe on native Windows, where `true`
|
|
693
719
|
// is not a command and the swallow-failures intent would invert into a 9009 error.
|
|
@@ -695,9 +721,151 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [])
|
|
|
695
721
|
// Same command as Stop: `learn` reads the payload's hook_event_name and routes a
|
|
696
722
|
// SubagentStop to the per-agent lesson path.
|
|
697
723
|
SubagentStop: [{ hooks: [{ type: 'command', command: learn }] }],
|
|
724
|
+
// And once more BEFORE compaction. `learn` distils the tail of the transcript (the newest
|
|
725
|
+
// MAX_NOTES_CHARS), so per-turn Stop is a sliding window over the session — and
|
|
726
|
+
// compaction is the one moment that window is about to be summarised away for good.
|
|
727
|
+
// The payload carries `transcript_path` like Stop's; the SessionStart `compact` matcher
|
|
728
|
+
// re-seeds context AFTER, this captures memory BEFORE. Same `|| exit 0`: never block.
|
|
729
|
+
PreCompact: [{ hooks: [{ type: 'command', command: learn }] }],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Geneseed's Copilot hooks, keyed by Copilot's event names.
|
|
735
|
+
*
|
|
736
|
+
* COPILOT'S SETTINGS ARE NOT CLAUDE'S. `~/.copilot/settings.json` carries `hooks` as a map of
|
|
737
|
+
* event → ONE `{command, shell?, timeout?}` object, not event → list of matcher groups, so
|
|
738
|
+
* nothing in `claudeHookGroups`/`mergeClaudeSettings` can be reused for it: there is no
|
|
739
|
+
* array to append to, and a second Geneseed group per event is not expressible. Two hooks:
|
|
740
|
+
*
|
|
741
|
+
* * `sessionStart` → `context --host copilot`: the same project-context injection Claude
|
|
742
|
+
* gets, in Copilot's `{"additionalContext"}` envelope.
|
|
743
|
+
* * `toolCall` → `tool-gate --host copilot`: both gates behind the one command the event
|
|
744
|
+
* allows, block-only for Laws I/IV, warn for the rest (js/hosts/hooks.mjs's header).
|
|
745
|
+
*
|
|
746
|
+
* No `sessionEnd`/`agentStop` → `learn`: Copilot's payloads for those carry no
|
|
747
|
+
* `transcript_path`, so `learn` would have nothing to distil. The memory write-back stays on
|
|
748
|
+
* the preamble for this host, as before.
|
|
749
|
+
*
|
|
750
|
+
* `|| exit 0` on the non-gate, as in `claudeHookGroups` and for the same reason.
|
|
751
|
+
*/
|
|
752
|
+
export function copilotHooks(cfg, hookOpts) {
|
|
753
|
+
const run = hookPrefix(hookOpts);
|
|
754
|
+
return {
|
|
755
|
+
sessionStart: { command: `${run} context --root "${cfg}" --host copilot || exit 0` },
|
|
756
|
+
toolCall: { command: `${run} tool-gate --root "${cfg}" --host copilot` },
|
|
698
757
|
};
|
|
699
758
|
}
|
|
700
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Wire Copilot's hooks into `p` — returns the complete current claim set, `[{event, hook}]`.
|
|
762
|
+
*
|
|
763
|
+
* ONE SLOT PER EVENT MEANS A FOREIGN HOOK CANNOT BE JOINED, ONLY DISPLACED — and displacing
|
|
764
|
+
* a hook the user wrote is not Geneseed's to do. A slot is taken when it holds a hook that
|
|
765
|
+
* is neither canonical nor a claim recorded by a previous emit; that event is skipped with a
|
|
766
|
+
* warning and never claimed. A recorded stale claim (a moved shim) is replaced, as the Claude
|
|
767
|
+
* merge prunes stale groups. Comments in the file mean it is not rewritten, as everywhere.
|
|
768
|
+
*/
|
|
769
|
+
export function mergeCopilotSettings(p, priorHooks = null, hookOpts = {}) {
|
|
770
|
+
const prior = (priorHooks || []).filter(isDict);
|
|
771
|
+
let config = {};
|
|
772
|
+
let hadComments = false;
|
|
773
|
+
if (existsSync(p)) {
|
|
774
|
+
try {
|
|
775
|
+
const [loaded, hc] = readJsonc(readText(p));
|
|
776
|
+
hadComments = hc;
|
|
777
|
+
if (isDict(loaded)) config = loaded;
|
|
778
|
+
} catch (e) {
|
|
779
|
+
if (!isOsError(e)) throw e;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
let hooks = get(config, 'hooks');
|
|
783
|
+
if (!isDict(hooks)) hooks = {};
|
|
784
|
+
const canonical = copilotHooks(path.dirname(p), hookOpts);
|
|
785
|
+
const claims = [];
|
|
786
|
+
let changed = false;
|
|
787
|
+
for (const [event, hook] of Object.entries(canonical)) {
|
|
788
|
+
const cur = get(hooks, event);
|
|
789
|
+
const recorded = prior.find((r) => get(r, 'event') === event);
|
|
790
|
+
if (isDict(cur) && deepEquals(cur, hook)) { claims.push({ event, hook }); continue; }
|
|
791
|
+
if (isDict(cur) && !(recorded && deepEquals(recorded.hook, cur))) {
|
|
792
|
+
process.stderr.write(`[geneseed] ${path.basename(p)}: hooks.${event} is already set and `
|
|
793
|
+
+ 'is not Geneseed\'s — left alone, so this event is not wired.\n');
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
hooks[event] = hook;
|
|
797
|
+
claims.push({ event, hook });
|
|
798
|
+
changed = true;
|
|
799
|
+
}
|
|
800
|
+
// A recorded claim whose event is no longer canonical: unwire it, or it fires forever.
|
|
801
|
+
for (const rec of prior) {
|
|
802
|
+
const event = get(rec, 'event');
|
|
803
|
+
if (event in canonical) continue;
|
|
804
|
+
if (deepEquals(get(hooks, event), get(rec, 'hook'))) { delete hooks[event]; changed = true; }
|
|
805
|
+
}
|
|
806
|
+
if (!changed) return [p, claims];
|
|
807
|
+
if (hadComments) {
|
|
808
|
+
process.stderr.write(`[geneseed] ${path.basename(p)} has comments — not rewriting it `
|
|
809
|
+
+ "(your edits are kept). Add Geneseed's hooks by hand from adapters/copilot/README.md.\n");
|
|
810
|
+
return [p, prior];
|
|
811
|
+
}
|
|
812
|
+
if (Object.keys(hooks).length) config.hooks = hooks;
|
|
813
|
+
else delete config.hooks;
|
|
814
|
+
atomicWriteJson(p, config);
|
|
815
|
+
return [p, claims];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** Remove exactly the recorded Copilot hooks from `p`. True when the file was rewritten. */
|
|
819
|
+
export function unwireCopilotSettings(p, recorded) {
|
|
820
|
+
if (!existsSync(p) || !recorded || !recorded.length) return false;
|
|
821
|
+
let loaded;
|
|
822
|
+
let hadComments;
|
|
823
|
+
try {
|
|
824
|
+
[loaded, hadComments] = readJsonc(readText(p));
|
|
825
|
+
} catch (e) {
|
|
826
|
+
if (!isOsError(e)) throw e;
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
if (hadComments || !isDict(loaded)) return false;
|
|
830
|
+
const hooks = get(loaded, 'hooks');
|
|
831
|
+
if (!isDict(hooks)) return false;
|
|
832
|
+
for (const rec of recorded) {
|
|
833
|
+
const event = get(rec, 'event');
|
|
834
|
+
if (deepEquals(get(hooks, event), get(rec, 'hook'))) delete hooks[event];
|
|
835
|
+
}
|
|
836
|
+
if (!Object.keys(hooks).length) delete loaded.hooks;
|
|
837
|
+
try {
|
|
838
|
+
atomicWriteJson(p, loaded);
|
|
839
|
+
} catch (e) {
|
|
840
|
+
if (!isOsError(e)) throw e;
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* The Copilot twin of `settingsIntegrityCheck`: every recorded hook present (or absent) in
|
|
848
|
+
* the file it was claimed against. Same contract — problems as strings, each also WARNed on
|
|
849
|
+
* stderr, empty when clean — so a merge that did not stick is loud at emit time rather than
|
|
850
|
+
* a hook that quietly never fires.
|
|
851
|
+
*/
|
|
852
|
+
export function copilotIntegrityCheck(p, recorded, expect = 'present') {
|
|
853
|
+
const problems = [];
|
|
854
|
+
const hooks = existsSync(p) ? (() => {
|
|
855
|
+
try { return get(readJsonc(readText(p))[0], 'hooks'); } catch { return null; }
|
|
856
|
+
})() : null;
|
|
857
|
+
for (const rec of (recorded || []).filter(isDict)) {
|
|
858
|
+
const hit = deepEquals(get(hooks, rec.event), rec.hook);
|
|
859
|
+
if (expect === 'present' && !hit) {
|
|
860
|
+
problems.push(`${p}: recorded Copilot hook missing — event=${rec.event}`);
|
|
861
|
+
} else if (expect === 'absent' && hit) {
|
|
862
|
+
problems.push(`${p}: recorded Copilot hook still present after unwire — event=${rec.event}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
for (const x of problems) process.stderr.write(`[geneseed] WARN: ${x}\n`);
|
|
866
|
+
return problems;
|
|
867
|
+
}
|
|
868
|
+
|
|
701
869
|
/**
|
|
702
870
|
* `_build_settings._merge_claude_settings` — returns `[target, managed]`.
|
|
703
871
|
*
|
|
@@ -710,7 +878,7 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [])
|
|
|
710
878
|
* `scope` is accepted and unused, exactly as in the Python.
|
|
711
879
|
*/
|
|
712
880
|
export function mergeClaudeSettings(p, _scope = 'global', priorHooks = null, hookOpts = {},
|
|
713
|
-
doctrines = null, excluded = []) {
|
|
881
|
+
doctrines = null, excluded = [], host = 'claude') {
|
|
714
882
|
const prior = (priorHooks || []).filter(isDict);
|
|
715
883
|
let config = {};
|
|
716
884
|
let hadComments = false;
|
|
@@ -730,7 +898,7 @@ export function mergeClaudeSettings(p, _scope = 'global', priorHooks = null, hoo
|
|
|
730
898
|
}
|
|
731
899
|
let hooks = get(config, 'hooks');
|
|
732
900
|
if (!isDict(hooks)) hooks = {};
|
|
733
|
-
const canonical = claudeHookGroups(path.dirname(p), hookOpts, doctrines, excluded);
|
|
901
|
+
const canonical = claudeHookGroups(path.dirname(p), hookOpts, doctrines, excluded, host);
|
|
734
902
|
const canonFlat = [];
|
|
735
903
|
for (const [event, groups] of Object.entries(canonical)) {
|
|
736
904
|
for (const g of groups) canonFlat.push({ event, group: g });
|
|
@@ -771,7 +939,7 @@ export function mergeClaudeSettings(p, _scope = 'global', priorHooks = null, hoo
|
|
|
771
939
|
return [p, prior];
|
|
772
940
|
}
|
|
773
941
|
// The `else` is DEAD on both sides and reproduced rather than dropped: `canonical` always
|
|
774
|
-
// carries
|
|
942
|
+
// carries at least three events, so by the time control reaches here `hooks` is never empty. A
|
|
775
943
|
// mutation keeping the empty block instead of deleting it is the one of thirty-three that
|
|
776
944
|
// stays green, and it stays green because there is nothing to detect — recorded the way
|
|
777
945
|
// `themed_rel` is, so "the gate cannot see it" and "there is nothing to see" keep their
|
package/js/inspect/excludes.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The sovereign-repo exclusion WRITER.
|
|
3
3
|
*
|
|
4
|
-
* THE READER WAS ALREADY HERE. `js/hosts/hooks.mjs` has carried `sovereignBypass`, `EXCLUDES_FILE`
|
|
4
|
+
* THE READER WAS ALREADY HERE. `js/hosts/hosts.mjs` (since 2026-09; `hooks.mjs` before) has carried `sovereignBypass`, `EXCLUDES_FILE`
|
|
5
5
|
* and `EXCLUDE_DIRS` since P5a: every hook call reads `excludes.json` and stands down when
|
|
6
6
|
* cwd is under an excluded folder. This module is the other half — `harness exclude
|
|
7
7
|
* add|remove|list`, which maintains that file across every global install and wires the
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export const SKILL_CLASS = {
|
|
|
96
96
|
tickets: 'design',
|
|
97
97
|
tdd: 'build', develop: 'build', refactor: 'build', debug: 'build', migrate: 'build',
|
|
98
98
|
'frontend-design': 'build', 'opencode-theme': 'build', prototype: 'build',
|
|
99
|
-
'forge-mcp': 'build',
|
|
99
|
+
'forge-mcp': 'build', 'bruno-collection-generator': 'build', 'bruno-test-writer': 'build',
|
|
100
100
|
'geneseed-code-review': 'review', 'fresh-eyes': 'review', 'gap-detector': 'review',
|
|
101
101
|
'roast-me': 'review', 'review-response': 'review', ponytail: 'review',
|
|
102
102
|
commit: 'ship', ship: 'ship', release: 'ship', handoff: 'ship', 'git-rescue': 'ship',
|
package/js/inspect/status.mjs
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* is inside the sandbox. It is reachable now; no cell seeds a manifest without an emit
|
|
55
55
|
* marker, so it is still gated only by the corpus, and that is a gap rather than a wall.
|
|
56
56
|
*/
|
|
57
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
57
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
58
58
|
import path from 'node:path';
|
|
59
59
|
|
|
60
60
|
import { THEMES, ROOT, makeCfg } from '../build/source.mjs';
|
|
@@ -62,7 +62,7 @@ import { tuiInventory } from './inventory.mjs';
|
|
|
62
62
|
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
63
63
|
import {
|
|
64
64
|
claudeConfigDir, bobConfigDir, copilotConfigDir, opencodeConfigDir, resolvePath,
|
|
65
|
-
resolveMemoryDir,
|
|
65
|
+
resolveMemoryDir, sovereignBypass,
|
|
66
66
|
} from '../hosts/hosts.mjs';
|
|
67
67
|
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
68
68
|
// deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
|
|
@@ -175,6 +175,41 @@ export function accentFor(theme) {
|
|
|
175
175
|
|
|
176
176
|
// ---- the panel ----------------------------------------------------------------------------
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The gates, as seen from THIS cwd across every install on the machine.
|
|
180
|
+
*
|
|
181
|
+
* Two facts the panel could not show before. `standing_down` names each install whose
|
|
182
|
+
* `excludes.json` silences all three gates for the current directory — a bypass that until
|
|
183
|
+
* now had no visible surface anywhere, so a user could not tell an armed gate from one they
|
|
184
|
+
* had switched off months ago. `asks` counts the gate ledger (`notebook/gates.jsonl`, written
|
|
185
|
+
* by `js/hosts/hooks.mjs` on every ask) by rule; it is the only evidence of whether the gates
|
|
186
|
+
* do anything, and the input to the decision of whether to build more of them. Unreadable
|
|
187
|
+
* ledgers and malformed lines are skipped, not reported: this is a display read.
|
|
188
|
+
*/
|
|
189
|
+
export function gateSummary(cfgDirs) {
|
|
190
|
+
const standingDown = [];
|
|
191
|
+
const asks = {};
|
|
192
|
+
let total = 0;
|
|
193
|
+
for (const cfg of cfgDirs) {
|
|
194
|
+
if (sovereignBypass(cfg)) standingDown.push(String(cfg));
|
|
195
|
+
let raw;
|
|
196
|
+
try {
|
|
197
|
+
raw = readFileSync(path.join(cfg, 'notebook', 'gates.jsonl'), 'utf8');
|
|
198
|
+
} catch {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
for (const line of raw.split('\n')) {
|
|
202
|
+
if (!line.trim()) continue;
|
|
203
|
+
let entry;
|
|
204
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
205
|
+
const rule = entry && typeof entry.rule === 'string' ? entry.rule : 'unknown';
|
|
206
|
+
asks[rule] = (asks[rule] || 0) + 1;
|
|
207
|
+
total += 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { standing_down: standingDown, asks, total };
|
|
211
|
+
}
|
|
212
|
+
|
|
178
213
|
/** `_harness_status._status_data`. */
|
|
179
214
|
export function statusData() {
|
|
180
215
|
const inst = installedDefaults();
|
|
@@ -226,6 +261,7 @@ export function statusData() {
|
|
|
226
261
|
version_verdict: versionVerdict(installedFp, sourceFp),
|
|
227
262
|
agent_md: agentMd ? String(agentMd) : null,
|
|
228
263
|
agent_md_present: Boolean(agentMd && existsSync(agentMd)),
|
|
264
|
+
gates: gateSummary([...(cfgDir ? [cfgDir] : []), ...otherCfg]),
|
|
229
265
|
};
|
|
230
266
|
}
|
|
231
267
|
|
|
@@ -295,6 +331,18 @@ export function statusLines(d, color = false) {
|
|
|
295
331
|
rows.push(['AGENT.md',
|
|
296
332
|
`${d.agent_md} (${d.agent_md_present ? 'present' : 'MISSING'})`]);
|
|
297
333
|
}
|
|
334
|
+
// Conditional for the same recording reason as `constitution` above: the 30 recorded
|
|
335
|
+
// panels carry no `gates` key and must render unchanged.
|
|
336
|
+
if (d.gates) {
|
|
337
|
+
const g = d.gates;
|
|
338
|
+
const armed = g.standing_down.length
|
|
339
|
+
? `STANDING DOWN for this cwd (excludes.json in ${g.standing_down.join(', ')})`
|
|
340
|
+
: 'armed';
|
|
341
|
+
const byRule = Object.entries(g.asks).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
342
|
+
.map(([k, v]) => `${k} ${v}`).join(', ');
|
|
343
|
+
rows.push(['gates', `${armed} ${DOT} ${g.total} ask${g.total === 1 ? '' : 's'}`
|
|
344
|
+
+ (byRule ? ` (${byRule})` : '')]);
|
|
345
|
+
}
|
|
298
346
|
|
|
299
347
|
const labelW = Math.max(...rows.map(([k]) => codePointLength(k)));
|
|
300
348
|
const body = rows.map(([k, v]) => ` ${padEndToWidth(k, labelW)} ${v}`);
|
|
@@ -59,9 +59,10 @@ import {
|
|
|
59
59
|
} from '../hosts/hosts.mjs';
|
|
60
60
|
import { mcpCommented } from '../hosts/mcp.mjs';
|
|
61
61
|
import {
|
|
62
|
-
managedBlockRead, managedBlockRemove, managedBlockWrite,
|
|
63
|
-
|
|
64
|
-
unwireClaudeExcludes, unwireClaudeSettings,
|
|
62
|
+
copilotIntegrityCheck, managedBlockRead, managedBlockRemove, managedBlockWrite,
|
|
63
|
+
mergeClaudeSettings, mergeCopilotSettings, opencodeTarget, readJsonc,
|
|
64
|
+
settingsIntegrityCheck, wireClaudeExcludes, unwireClaudeExcludes, unwireClaudeSettings,
|
|
65
|
+
unwireCopilotSettings,
|
|
65
66
|
} from '../hosts/settings.mjs';
|
|
66
67
|
import { printOut, printErr, readText, writeText, isFile, isDir, isOsError } from '../lib/fs.mjs';
|
|
67
68
|
import { indexOfDeepEqual, jsonDumps, jsonDumpsIndent, deepEquals } from '../lib/json.mjs';
|
|
@@ -294,8 +295,12 @@ export function claudeUninstall(cfg, archiveMemory) {
|
|
|
294
295
|
if (failed.length) warnSurvivors(failed);
|
|
295
296
|
const hooks = managed.settings_hooks || [];
|
|
296
297
|
const sf = settingsFile(cfg, managed);
|
|
297
|
-
|
|
298
|
+
// Two shapes, two unwires: a Copilot install records `copilot_hooks` (event → one hook)
|
|
299
|
+
// and never `settings_hooks`, so exactly one of these does anything.
|
|
300
|
+
const unwired = unwireClaudeSettings(sf, hooks)
|
|
301
|
+
| unwireCopilotSettings(sf, managed.copilot_hooks || []);
|
|
298
302
|
unwireClaudeExcludes(sf, managed.settings_excludes || []);
|
|
303
|
+
copilotIntegrityCheck(sf, managed.copilot_hooks || [], 'absent');
|
|
299
304
|
// The unwire is VERIFIED, not assumed: a commented settings file is never rewritten, so a
|
|
300
305
|
// supposedly-uninstalled repo could keep firing Geneseed's hooks. Loud, never fatal.
|
|
301
306
|
settingsIntegrityCheck(sf, managed, 'absent');
|
|
@@ -692,9 +697,10 @@ export function cmdUninstall(args) {
|
|
|
692
697
|
const stores = ['memory', 'notebook'].filter((n) => isDir(path.join(data, n)));
|
|
693
698
|
printOut(`[uninstall] target: ${root} (${host}:${scope})\n`);
|
|
694
699
|
if (host === 'copilot') {
|
|
695
|
-
printOut('[uninstall] removes: agents/, skills/, markers,
|
|
696
|
-
+ `${hostSpec(host).agentFile} managed block
|
|
697
|
-
+ 'settings.json
|
|
700
|
+
printOut('[uninstall] removes: agents/, skills/, markers, the '
|
|
701
|
+
+ `${hostSpec(host).agentFile} managed block, and Geneseed's `
|
|
702
|
+
+ '~/.copilot/settings.json hooks on a global install (your own keys/hooks and '
|
|
703
|
+
+ '.github files are kept).\n');
|
|
698
704
|
} else if (host === 'claude' || host === 'bob') {
|
|
699
705
|
printOut('[uninstall] removes: agents/, skills/, markers, the '
|
|
700
706
|
+ `${hostSpec(host).agentFile} managed block, and Geneseed's `
|
|
@@ -1049,9 +1055,27 @@ function cleanHostStash(cfg, host = 'claude') {
|
|
|
1049
1055
|
* file and swallows its own OSError, exactly as the reference does — a failed bookkeeping
|
|
1050
1056
|
* write must not fail the reactivate that has already moved every file back.
|
|
1051
1057
|
*/
|
|
1052
|
-
function remergeClaudeHooks(cfg, root = cfg) {
|
|
1058
|
+
function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
|
|
1053
1059
|
const data = claudeReadManifest(cfg);
|
|
1054
1060
|
const managed = managedOf(data);
|
|
1061
|
+
// A Copilot install: its own merge, its own claim key, and none of the pack/exclude axes
|
|
1062
|
+
// below — the Copilot gate is not pack-conditional (see `copilotHooks`). Without this
|
|
1063
|
+
// branch a Copilot reactivate would write CLAUDE-shaped hook groups into
|
|
1064
|
+
// ~/.copilot/settings.json, which Copilot cannot read.
|
|
1065
|
+
if (Array.isArray(managed.copilot_hooks)) {
|
|
1066
|
+
const [, claims] = mergeCopilotSettings(settingsFile(cfg, managed), managed.copilot_hooks,
|
|
1067
|
+
hookRunnerEntry());
|
|
1068
|
+
if (!deepEquals(claims, managed.copilot_hooks) && Object.keys(data).length > 0) {
|
|
1069
|
+
managed.copilot_hooks = claims;
|
|
1070
|
+
data.managed = managed;
|
|
1071
|
+
const tmp = path.join(cfg, `${GLOBAL_MANIFEST}.tmp`);
|
|
1072
|
+
try {
|
|
1073
|
+
writeText(tmp, `${jsonDumpsIndent(data)}\n`);
|
|
1074
|
+
renameSync(tmp, path.join(cfg, GLOBAL_MANIFEST));
|
|
1075
|
+
} catch { /* except OSError: pass */ }
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1055
1079
|
// `hookOpts` is the one argument `mergeClaudeSettings` will not default, and it is right
|
|
1056
1080
|
// not to: there is no computable fallback for the runner/entry the shim bakes. The
|
|
1057
1081
|
// emitter's own originator is imported rather than restated — a reactivate that wired a
|
|
@@ -1072,8 +1096,9 @@ function remergeClaudeHooks(cfg, root = cfg) {
|
|
|
1072
1096
|
// NOTHING excluded, so a reactivate can only ever restore a gate, never remove one.
|
|
1073
1097
|
const excluded = excludedRulesOfDir(root).length ? excludedRulesOfDir(root)
|
|
1074
1098
|
: excludedRulesOfDir(cfg);
|
|
1099
|
+
// `host` reaches the group builder: a Bob reactivate must re-wire Gemini-named groups.
|
|
1075
1100
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed), 'global',
|
|
1076
|
-
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded);
|
|
1101
|
+
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host);
|
|
1077
1102
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1078
1103
|
if (!deepEquals(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1079
1104
|
managed.settings_hooks = claims;
|
|
@@ -1107,10 +1132,12 @@ function claudeDeactivate(root, scope = 'global', host = 'claude') {
|
|
|
1107
1132
|
return { ok: false, failed, rolled_back: done.length };
|
|
1108
1133
|
}
|
|
1109
1134
|
unwireClaudeSettings(settingsFile(cfg, managed), managed.settings_hooks || []);
|
|
1135
|
+
unwireCopilotSettings(settingsFile(cfg, managed), managed.copilot_hooks || []);
|
|
1110
1136
|
unwireClaudeExcludes(settingsFile(cfg, managed), managed.settings_excludes || []);
|
|
1111
1137
|
// The same integrity check the uninstall path runs: a deactivate that silently failed to
|
|
1112
1138
|
// unwire would leave hooks firing in a repo the user believes is off.
|
|
1113
1139
|
settingsIntegrityCheck(settingsFile(cfg, managed), managed, 'absent');
|
|
1140
|
+
copilotIntegrityCheck(settingsFile(cfg, managed), managed.copilot_hooks || [], 'absent');
|
|
1114
1141
|
const cm = claudeMdPath(cfg, managed);
|
|
1115
1142
|
const block = managedBlockRead(cm);
|
|
1116
1143
|
mkdirSync(stash, { recursive: true }); // presence == disabled, even if nothing moved
|
|
@@ -1147,7 +1174,7 @@ function claudeReactivate(root, scope = 'global', host = 'claude') {
|
|
|
1147
1174
|
&& isFile(path.join(cfg, 'rules', 'geneseed.md'));
|
|
1148
1175
|
}
|
|
1149
1176
|
if (relive) {
|
|
1150
|
-
remergeClaudeHooks(cfg, root); // ensure the hooks are present (and the claims exact)
|
|
1177
|
+
remergeClaudeHooks(cfg, root, host); // ensure the hooks are present (and the claims exact)
|
|
1151
1178
|
cleanHostStash(cfg, host);
|
|
1152
1179
|
return { ok: true,
|
|
1153
1180
|
note: 'install was re-created while disabled; discarded the stashed snapshot' };
|
|
@@ -1165,7 +1192,7 @@ function claudeReactivate(root, scope = 'global', host = 'claude') {
|
|
|
1165
1192
|
if (existsSync(blockFile)) {
|
|
1166
1193
|
managedBlockWrite(claudeMdPath(cfg, managed), readText(blockFile));
|
|
1167
1194
|
}
|
|
1168
|
-
remergeClaudeHooks(cfg, root);
|
|
1195
|
+
remergeClaudeHooks(cfg, root, host);
|
|
1169
1196
|
wireClaudeExcludes(settingsFile(cfg, managed), managed.settings_excludes || []);
|
|
1170
1197
|
cleanHostStash(cfg, host);
|
|
1171
1198
|
return { ok: true, kind: host, moved };
|
package/js/maintain/update.mjs
CHANGED
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
*
|
|
34
34
|
* ALSO HERE, SINCE P8b: `syncSelf`/`cmdSyncSelf` and the bootstrap step runner
|
|
35
35
|
* (`cmdBootstrap`, `bootstrapPlain`) at the bottom of this file. Still NOT here: `main`, the
|
|
36
|
-
* launchers' self-heal entry.
|
|
37
|
-
*
|
|
36
|
+
* launchers' self-heal entry. Every web action row crosses — see `js/web/jobs.mjs`.
|
|
37
|
+
* `parseOrigin` is ported in full here even though only `upgrade`
|
|
38
38
|
* needs its fallback, because P8c's docs `about` kind needs the slug half and splitting one
|
|
39
39
|
* pure function across two phases is how a second classifier gets written.
|
|
40
40
|
*/
|
package/js/web/actions.mjs
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* ---------------------------------------------------------------------------------------
|
|
12
12
|
* `apiPickFolder` CROSSED 2026-08-27 — a user override of the permanent decline this section
|
|
13
|
-
* used to record
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* used to record (why a folder dialog looked permanently un-portable lives in git history,
|
|
14
|
+
* under `DECLINED_POST` in `js/web/routes.mjs`). See `apiPickFolder`'s own docblock, below,
|
|
15
|
+
* for the argv, the async-spawn reasoning, and why it dispatches inline from
|
|
16
|
+
* `js/web/handler.mjs` rather than through `POST_ROUTES`.
|
|
17
17
|
*
|
|
18
18
|
* ---------------------------------------------------------------------------------------
|
|
19
19
|
* `apiInstallToggle` is a thin endpoint over an engine that lives elsewhere on purpose.
|
|
@@ -223,10 +223,10 @@ function runPicker(cmd, args, done, resultOf) {
|
|
|
223
223
|
|
|
224
224
|
/**
|
|
225
225
|
* `/api/pick-folder` — the OS-NATIVE folder chooser, opened ON THE DAEMON HOST. `done` is
|
|
226
|
-
* called with `{path}` on OK, `{cancelled: true}` on cancel, or `{error}` — the shape
|
|
227
|
-
* `web/src/api/
|
|
228
|
-
* DECLINED (see this file's header
|
|
229
|
-
*
|
|
226
|
+
* called with `{path}` on OK, `{cancelled: true}` on cancel, or `{error}` — the shape the
|
|
227
|
+
* client's `pickFolder()` (in `web/src/api/index.js`) already expects, unchanged since the
|
|
228
|
+
* endpoint was DECLINED (see this file's header for that history, overridden by the user
|
|
229
|
+
* 2026-08-27).
|
|
230
230
|
*
|
|
231
231
|
* ASYNC, NEVER `spawnSync`: a folder dialog stays open until a human answers it — seconds to
|
|
232
232
|
* minutes — and a synchronous spawn would block this single-threaded server's event loop for
|
package/js/web/api.mjs
CHANGED
|
@@ -37,7 +37,6 @@ import { stripWhitespace, percentDecode } from '../lib/text.mjs';
|
|
|
37
37
|
import { readJsonc } from '../hosts/settings.mjs';
|
|
38
38
|
import { apiActivity, apiActivityDetail } from './activity.mjs';
|
|
39
39
|
import { apiMcp, apiRules } from './actions.mjs';
|
|
40
|
-
import { apiGraph } from './graph.mjs';
|
|
41
40
|
|
|
42
41
|
/** A requested catalog section or item that does not exist. */
|
|
43
42
|
export class NotFound extends Error {}
|
|
@@ -342,7 +341,7 @@ export const WIKI_FILE_CAP = 5000;
|
|
|
342
341
|
* (`js/hosts/hosts.mjs`'s docblock). None of them may let that escape: `js/web/handler.mjs`
|
|
343
342
|
* wraps the whole GET in a blanket `catch` → a JSON 500, so an unguarded refusal would turn
|
|
344
343
|
* ONE bad line in a file the user hand-edits into a dead Knowledge section. That is the same
|
|
345
|
-
* argument `sovereignBypass` makes in `js/hosts/
|
|
344
|
+
* argument `sovereignBypass` makes in `js/hosts/hosts.mjs` — contain the refusal per entry
|
|
346
345
|
* and take the site's OWN degrade — and each of the three already has one: `[]`, `continue`,
|
|
347
346
|
* `continue`-into-404.
|
|
348
347
|
*
|
|
@@ -498,7 +497,6 @@ function flatName(name) {
|
|
|
498
497
|
}
|
|
499
498
|
}
|
|
500
499
|
|
|
501
|
-
/** `js/web/graph.mjs` is a second reader of this pattern. */
|
|
502
500
|
export const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
503
501
|
|
|
504
502
|
/**
|
|
@@ -874,8 +872,8 @@ const RECENT_LIMIT = 8;
|
|
|
874
872
|
/**
|
|
875
873
|
* THE NEWEST FILE-BACKED ENTRIES ACROSS THE HARNESS — the "freshly grown" card's whole payload.
|
|
876
874
|
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
875
|
+
* The first GET added after the Python→Node port froze its reference surface. It exists
|
|
876
|
+
* because NOTHING else in this API carries a per-entry date: every
|
|
879
877
|
* catalog row is `{name, title, desc, source}` and the only two timestamps on the whole surface are
|
|
880
878
|
* `overview.build_time` and `doctor.checked_at`. A client cannot stat a path, so "what changed
|
|
881
879
|
* lately" was unanswerable without a server that looks.
|
|
@@ -954,13 +952,11 @@ export const PREFIX_ROUTES = [
|
|
|
954
952
|
/**
|
|
955
953
|
* A literal path to `api_X(state)`, table-keyed rather than a chain of `if`s — the reason
|
|
956
954
|
* it is a table is `tests/unit/web_server.test.mjs`'s cross-check, which requires this
|
|
957
|
-
* table plus `PREFIX_ROUTES
|
|
958
|
-
*
|
|
955
|
+
* table plus `PREFIX_ROUTES` and `GET_INLINE` to equal a surface list written out in the
|
|
956
|
+
* test, so a route cannot appear or vanish unenumerated.
|
|
959
957
|
*/
|
|
960
958
|
export const STATE_ROUTES = {
|
|
961
959
|
'/api/overview': apiOverview,
|
|
962
|
-
// Added after the reference surface was frozen — declared in `GET_BEYOND_REF`, which
|
|
963
|
-
// keeps the partition gate's set equality honest instead of forging `REF_GET`.
|
|
964
960
|
'/api/recent': apiRecent,
|
|
965
961
|
'/api/themes': apiThemes,
|
|
966
962
|
'/api/setup': apiSetup,
|
|
@@ -969,7 +965,6 @@ export const STATE_ROUTES = {
|
|
|
969
965
|
'/api/excludes': apiExcludes,
|
|
970
966
|
'/api/profile': apiProfile,
|
|
971
967
|
'/api/diff': apiDiff,
|
|
972
|
-
'/api/graph': apiGraph,
|
|
973
968
|
'/api/activity': apiActivity,
|
|
974
969
|
// `/api/rules` must exist as a PAIR with its POST: `apiRulesMutate` splices `parseRules`'
|
|
975
970
|
// line indices, and the fingerprint every mutation must send back is what this GET
|
package/js/web/docs.mjs
CHANGED
|
@@ -7,11 +7,9 @@
|
|
|
7
7
|
* the same file, so there is exactly one description of the CLI surface, not a second
|
|
8
8
|
* transcription of it.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* on. Same partition shape as the route table, which is why it is a table here too and not
|
|
14
|
-
* two `if`s.
|
|
10
|
+
* `tests/unit/web_server.test.mjs` holds `KINDS` against a written-out list of the five
|
|
11
|
+
* kinds this daemon dispatches on — same declaration-vs-dispatch shape as the route table,
|
|
12
|
+
* which is why it is a table here too and not five `if`s.
|
|
15
13
|
*
|
|
16
14
|
* THE `?harness=` QUERY PARAM IS the Docs selector, and it is the ONLY input to these
|
|
17
15
|
* endpoints that is not the checkout itself — the one thing a test can vary. Every
|
|
@@ -430,22 +428,6 @@ function about(state) {
|
|
|
430
428
|
};
|
|
431
429
|
}
|
|
432
430
|
|
|
433
|
-
/**
|
|
434
|
-
* EMPTY, AND KEPT DECLARED — the third partition in this codebase to reach that state,
|
|
435
|
-
* after `NOT_PORTED_POST` and `NOT_PORTED_ACTIONS`.
|
|
436
|
-
*
|
|
437
|
-
* `tests/unit/web_server.test.mjs` cross-checks this set against the kinds `apiDocsPage`
|
|
438
|
-
* dispatches on, and that check is at its STRONGEST when the set is empty: it says every
|
|
439
|
-
* kind this daemon can dispatch on is answered here. A sixth kind added later still cannot
|
|
440
|
-
* quietly fall through to the `NotFound` at the bottom, and it has a place to be declared
|
|
441
|
-
* rather than reading as a typo'd page id.
|
|
442
|
-
*
|
|
443
|
-
* THE 501 ARM HAS NO REACHABLE TARGET TODAY: with this set empty, no page names an
|
|
444
|
-
* unported kind. Its correctness rests on the declaration check above, not on any request
|
|
445
|
-
* that actually reaches it — worth knowing before "simplifying" it away as dead code.
|
|
446
|
-
*/
|
|
447
|
-
export const NOT_PORTED_KINDS = new Set();
|
|
448
|
-
|
|
449
431
|
export function apiDocs(state, harnessName = null) {
|
|
450
432
|
const hn = normHarness(harnessName, state);
|
|
451
433
|
const groups = visibleGroups(hn).map((g) => ({
|
|
@@ -466,20 +448,16 @@ export function apiDocsPage(state, pageId, harnessName = null) {
|
|
|
466
448
|
const hn = normHarness(harnessName, state);
|
|
467
449
|
const render = KIND_ROUTES[page.kind];
|
|
468
450
|
if (render !== undefined) return render(state, pageId, page, hn);
|
|
469
|
-
if (NOT_PORTED_KINDS.has(page.kind)) {
|
|
470
|
-
const e = new Error(`not ported yet: docs kind '${page.kind}'`);
|
|
471
|
-
e.notPorted = true;
|
|
472
|
-
throw e;
|
|
473
|
-
}
|
|
474
451
|
throw new NotFound(pageId);
|
|
475
452
|
}
|
|
476
453
|
|
|
477
454
|
/**
|
|
478
|
-
* The
|
|
455
|
+
* The five kinds, as a TABLE and not five `if`s.
|
|
479
456
|
*
|
|
480
|
-
* `tests/unit/web_server.test.mjs`
|
|
481
|
-
*
|
|
482
|
-
* consults, so a declaration-only check cannot see a dispatcher that stopped
|
|
457
|
+
* `tests/unit/web_server.test.mjs` holds `Object.keys(KIND_ROUTES)` against a written-out
|
|
458
|
+
* list of the kinds `apiDocsPage` must dispatch on — this table is what the dispatcher
|
|
459
|
+
* actually consults, so a declaration-only check cannot see a dispatcher that stopped
|
|
460
|
+
* using it.
|
|
483
461
|
*/
|
|
484
462
|
const KIND_ROUTES = {
|
|
485
463
|
markdown: (state, pageId, page, hn) => {
|
|
@@ -514,4 +492,4 @@ const KIND_ROUTES = {
|
|
|
514
492
|
}),
|
|
515
493
|
};
|
|
516
494
|
|
|
517
|
-
export const
|
|
495
|
+
export const KINDS = Object.keys(KIND_ROUTES);
|