geneseed 3.10.0 → 3.12.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 +96 -0
- package/DESIGN.md +1 -1
- package/README.md +27 -45
- package/SHIPPED.md +2 -2
- package/adapters/bob/README.md +1 -1
- package/adapters/claude-code/README.md +17 -12
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/README.md +4 -4
- package/adapters/opencode/plugins/geneseed-guard.js +157 -2
- package/bin/build-driver.mjs +2 -0
- package/bin/geneseed-cli.mjs +21 -0
- package/bin/geneseed-hook.mjs +2 -0
- package/docs/README.md +42 -0
- package/docs/_groups.json +18 -0
- package/docs/concepts/agents.md +44 -0
- package/docs/concepts/collaboration.md +62 -0
- package/docs/concepts/exclusions.md +55 -0
- package/docs/concepts/footprint.md +52 -0
- package/docs/concepts/foreman-mode.md +69 -0
- package/docs/concepts/hooks.md +101 -0
- package/docs/concepts/hosts.md +55 -0
- package/docs/concepts/loops.md +165 -0
- package/docs/concepts/lsp.md +50 -0
- package/docs/{web → concepts}/memory.md +2 -2
- package/docs/{web → concepts}/notebook.md +2 -2
- package/docs/concepts/rules.md +52 -0
- package/docs/concepts/skills.md +49 -0
- package/docs/concepts/themes.md +50 -0
- package/docs/extending.md +32 -21
- package/docs/guides/autostart.md +70 -0
- package/docs/guides/choose-your-setup.md +134 -0
- package/docs/guides/headless.md +22 -0
- package/docs/guides/install.md +161 -0
- package/docs/guides/mcp.md +195 -0
- package/docs/guides/migrate.md +41 -0
- package/docs/guides/project-context.md +87 -0
- package/docs/guides/run-anywhere.md +45 -0
- package/docs/guides/uninstall.md +51 -0
- package/docs/guides/upgrade.md +65 -0
- package/docs/guides/verify.md +65 -0
- package/docs/guides/wiki.md +69 -0
- package/docs/guides/worktree.md +47 -0
- package/docs/reference/about.md +7 -0
- package/docs/reference/cli.md +7 -0
- package/docs/reference/environment.md +99 -0
- package/docs/reference/glossary.md +37 -0
- package/docs/reference/opencode-plugins.md +133 -0
- package/docs/reference/troubleshooting.md +350 -0
- package/docs/understand/a-day.md +77 -0
- package/docs/understand/enforced-vs-asked.md +64 -0
- package/docs/understand/harness.md +36 -0
- package/docs/understand/on-your-machine.md +76 -0
- package/docs/web-ui.md +13 -2
- package/harness.config.json +1 -1
- package/js/README.md +27 -4
- package/js/build/driver.mjs +14 -8
- package/js/build/generate.mjs +22 -7
- package/js/build/render.mjs +5 -0
- package/js/build/source.mjs +11 -1
- package/js/cli-table.json +236 -1
- package/js/hosts/gitref.mjs +99 -0
- package/js/hosts/hooks.mjs +88 -1
- package/js/hosts/installs.mjs +21 -0
- package/js/hosts/mcp.mjs +1 -1
- package/js/hosts/settings.mjs +13 -1
- package/js/inspect/checks-authoring.mjs +15 -3
- package/js/inspect/diff.mjs +3 -0
- package/js/inspect/doctor.mjs +3 -2
- package/js/inspect/inventory.mjs +2 -1
- package/js/inspect/registry.mjs +20 -3
- package/js/inspect/status.mjs +8 -3
- package/js/lib/node-floor.mjs +30 -0
- package/js/loop/catalog.mjs +116 -0
- package/js/loop/cli.mjs +151 -0
- package/js/loop/graph.mjs +145 -0
- package/js/loop/score.mjs +143 -0
- package/js/loop/state.mjs +322 -0
- package/js/maintain/migrate.mjs +4 -2
- package/js/maintain/setup.mjs +109 -21
- package/js/maintain/update.mjs +4 -15
- package/js/ui/cli.mjs +2 -2
- package/js/web/actions.mjs +9 -3
- package/js/web/api.mjs +41 -2
- package/js/web/daemon.mjs +94 -4
- package/js/web/docs.mjs +149 -65
- package/js/web/handler.mjs +1 -0
- package/js/web/jobs.mjs +3 -1
- package/js/web/server.mjs +10 -5
- package/package.json +6 -4
- package/registry.json +21 -0
- package/src/AGENT.md.tmpl +6 -0
- package/src/bricks/README.md +32 -0
- package/src/bricks/_template.md +32 -0
- package/src/bricks/apply.md +17 -0
- package/src/bricks/baseline-green.md +17 -0
- package/src/bricks/bruno-test.md +16 -0
- package/src/bricks/ci-fix.md +17 -0
- package/src/bricks/deps-audit.md +14 -0
- package/src/bricks/docs-update.md +15 -0
- package/src/bricks/identify.md +20 -0
- package/src/bricks/lint.md +14 -0
- package/src/bricks/plan.md +17 -0
- package/src/bricks/reproduce.md +16 -0
- package/src/bricks/review.md +17 -0
- package/src/bricks/security-scan.md +17 -0
- package/src/bricks/test.md +14 -0
- package/src/doctrines/process.md +11 -8
- package/src/loops/bugfix.json +22 -0
- package/src/loops/feature.json +21 -0
- package/src/loops/refactor.json +20 -0
- package/src/postures/mentor.md +1 -0
- package/src/skills/brick-forge.md +46 -0
- package/src/skills/geneseed.md +1 -1
- package/src/skills/learn-mode.md +89 -0
- package/src/skills/loop.md +99 -0
- package/src/skills/pipeline.md +2 -2
- package/src/skills/quiz.md +2 -1
- package/src/skills/teach.md +2 -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-GaaZWXTY.js +1 -0
- package/web/dist/assets/ActivityDetail-4t7d45Du.js +1 -0
- package/web/dist/assets/Dashboard-CYn7Tp16.js +2 -0
- package/web/dist/assets/Docs-BeFKLg2T.js +5 -0
- package/web/dist/assets/{GreenhouseView-D0cbCJ5m.js → GreenhouseView-C6ibc8IE.js} +1 -1
- package/web/dist/assets/Installs-DRYjYnxs.js +2 -0
- package/web/dist/assets/JournalView-1HbU1mfg.js +1 -0
- package/web/dist/assets/Laws-DFoG8JWB.js +3 -0
- package/web/dist/assets/Library-DDjgNwz_.js +1 -0
- package/web/dist/assets/Loops-D9VEoO0F.js +1 -0
- package/web/dist/assets/{Markdown-CkFyfgNf.js → Markdown-qjM2_ofS.js} +5 -5
- package/web/dist/assets/{OperatorHudView-CiAkAkQL.js → OperatorHudView-Bs7paWmh.js} +1 -1
- package/web/dist/assets/Personal-BY5n4gTw.js +1 -0
- package/web/dist/assets/Tabs-ho-EYD8i.js +1 -0
- package/web/dist/assets/api-CyMRbU2x.js +1 -0
- package/web/dist/assets/drawable-CLS9drMb.js +1 -0
- package/web/dist/assets/index-CX_hXMi5.css +1 -0
- package/web/dist/assets/index-Cjr7XCA3.js +2 -0
- package/web/dist/index.html +3 -2
- package/QUICKSTART.md +0 -50
- package/SETUP.md +0 -1148
- package/docs/web/_groups.json +0 -45
- package/docs/web/about.md +0 -6
- package/docs/web/adapters-claude-code.md +0 -8
- package/docs/web/adapters-opencode-loads.md +0 -8
- package/docs/web/adapters-opencode-spec.md +0 -8
- package/docs/web/adapters-opencode.md +0 -8
- package/docs/web/agents.md +0 -8
- package/docs/web/authoring.md +0 -11
- package/docs/web/autostart.md +0 -9
- package/docs/web/claude-hooks.md +0 -37
- package/docs/web/cli.md +0 -6
- package/docs/web/collaboration.md +0 -27
- package/docs/web/configure-wiki.md +0 -9
- package/docs/web/design.md +0 -7
- package/docs/web/env-knobs.md +0 -9
- package/docs/web/exclusions.md +0 -26
- package/docs/web/first-session.md +0 -28
- package/docs/web/footprint.md +0 -38
- package/docs/web/foreman-mode.md +0 -33
- package/docs/web/glossary.md +0 -6
- package/docs/web/harnesses.md +0 -30
- package/docs/web/headless.md +0 -9
- package/docs/web/ingest-docs.md +0 -9
- package/docs/web/install-paths.md +0 -9
- package/docs/web/install-quick.md +0 -35
- package/docs/web/lsp-overview.md +0 -37
- package/docs/web/mcp-claude-code.md +0 -10
- package/docs/web/mcp-filesystem.md +0 -9
- package/docs/web/mcp-gitlab.md +0 -9
- package/docs/web/mcp-markitdown.md +0 -9
- package/docs/web/mcp-overview.md +0 -34
- package/docs/web/mcp-trouble.md +0 -9
- package/docs/web/mcp-verify.md +0 -9
- package/docs/web/migrate.md +0 -39
- package/docs/web/model.md +0 -20
- package/docs/web/pipelines.md +0 -24
- package/docs/web/plugin-activity.md +0 -23
- package/docs/web/plugin-context.md +0 -26
- package/docs/web/plugin-guard.md +0 -24
- package/docs/web/plugin-learn.md +0 -27
- package/docs/web/plugin-notify.md +0 -24
- package/docs/web/plugin-ponytail.md +0 -23
- package/docs/web/plugin-setup.md +0 -7
- package/docs/web/plugin-workflow.md +0 -25
- package/docs/web/plugins.md +0 -17
- package/docs/web/project-context.md +0 -9
- package/docs/web/rules.md +0 -30
- package/docs/web/run-anywhere.md +0 -9
- package/docs/web/self-improve.md +0 -8
- package/docs/web/setup-choices.md +0 -64
- package/docs/web/skills.md +0 -8
- package/docs/web/themes-author.md +0 -20
- package/docs/web/themes.md +0 -9
- package/docs/web/token-footprint.md +0 -7
- package/docs/web/trouble.md +0 -38
- package/docs/web/upgrade.md +0 -9
- package/docs/web/verify.md +0 -30
- package/docs/web/wiki.md +0 -7
- package/docs/web/worktree.md +0 -10
- package/docs/wiki.md +0 -43
- package/web/dist/assets/Activity-BjVp522q.js +0 -1
- package/web/dist/assets/ActivityDetail-8ep1bspW.js +0 -1
- package/web/dist/assets/Dashboard-OkC_PCH6.js +0 -2
- package/web/dist/assets/Docs-CKPWz00o.js +0 -2
- package/web/dist/assets/Installs-BtGGdoTt.js +0 -2
- package/web/dist/assets/JournalView-Ckl4dmzR.js +0 -1
- package/web/dist/assets/Laws-CLpdRRk8.js +0 -3
- package/web/dist/assets/Library-wPPuKQrI.js +0 -1
- package/web/dist/assets/Personal-D8CnDo9v.js +0 -1
- package/web/dist/assets/Tabs-B4HNrrCs.js +0 -1
- package/web/dist/assets/drawable-BiwaOo6x.js +0 -1
- package/web/dist/assets/index-C1yil5j1.js +0 -2
- package/web/dist/assets/index-CDPDk2vj.css +0 -1
package/js/hosts/hooks.mjs
CHANGED
|
@@ -56,6 +56,7 @@ import { NO_WINDOW } from '../lib/proc.mjs';
|
|
|
56
56
|
import {
|
|
57
57
|
GATE_LEDGER, resolveMemoryDir, resolvePath, sovereignBypass,
|
|
58
58
|
} from './hosts.mjs';
|
|
59
|
+
import { currentBranch, gitRootOf, loopLaunched } from './gitref.mjs';
|
|
59
60
|
|
|
60
61
|
const ROOT = path.resolve(import.meta.dirname, '../..');
|
|
61
62
|
|
|
@@ -599,6 +600,88 @@ const GIT_GATE_RE = /\bgit\b[^\n]*\b(?:commit|push)\b/;
|
|
|
599
600
|
const DESTRUCTIVE_GIT_RE =
|
|
600
601
|
/\bgit\b[^\n]*\b(?:reset\s+--hard|clean\s+-[a-zA-Z]*f|branch\s+-D|checkout\s+--\s|push\s+[^\n]*--force)/;
|
|
601
602
|
|
|
603
|
+
// `loopExempt` — a WHITELIST, not the blacklist this replaced. A false ask costs one prompt; a
|
|
604
|
+
// false allow publishes. So the exemption holds only when the ENTIRE command is built from
|
|
605
|
+
// forms read in full below, on a branch a loop was actually launched on (LOOP.md's state
|
|
606
|
+
// marker, left by `geneseed loop init` — not just a name starting with `loop/`, which anyone
|
|
607
|
+
// can type). `currentBranch`/`gitRootOf`/`loopLaunched` live in `./gitref.mjs` — the one
|
|
608
|
+
// fs-only reader this file shares with `js/loop/cli.mjs`'s commit-message writer.
|
|
609
|
+
|
|
610
|
+
// No newline, backtick, or `$(` substitution anywhere — these can smuggle a second command (or
|
|
611
|
+
// a shared-branch target) past every check below, so the command is refused before it is even
|
|
612
|
+
// split. `<`/`>` are refused too: a redirect can both hide a second effect and, as the review
|
|
613
|
+
// found, trail off the end of an otherwise-whitelisted push (`HEAD:main>/dev/null`). A single
|
|
614
|
+
// `&` — not doubled into `&&` — is refused too: a shell runs `a & b` as TWO commands just like
|
|
615
|
+
// `a && b`, and the review found it unparsed by the splitter below, which only recognised the
|
|
616
|
+
// doubled form. `(?<!&)&(?!&)` matches a lone `&` without also matching either half of `&&`.
|
|
617
|
+
const UNSAFE_CHARS_RE = /[\n`<>]|\$\(|(?<!&)&(?!&)/;
|
|
618
|
+
|
|
619
|
+
// The only three git shapes a loop's own automation ever needs: adding unquoted paths,
|
|
620
|
+
// committing via `-F`/`--file` (so the message text, which may contain anything, never has to
|
|
621
|
+
// be quoted on this command line at all — see `js/loop/cli.mjs`'s LOOP_COMMIT_MSG), and
|
|
622
|
+
// read-only inspection. The token class `[\w./:@^~=+,-]` excludes quotes, braces, `!`, `*`,
|
|
623
|
+
// `$` and `&` — a brace or glob expansion (`{a,b}`, `*`) is exactly how a shell can turn one
|
|
624
|
+
// whitelisted-looking token into several unknown ones, so no pattern's path/arg tokens may
|
|
625
|
+
// contain them. The SAME class is the `-F`/`--file` path, not the looser `[^\s'"]+` this once
|
|
626
|
+
// was: with that loose class, `git commit -F {m,--amend}` brace-expands to `git commit -F m
|
|
627
|
+
// --amend` and `git commit -F m*` glob-expands to whatever matches — either turns a commit of
|
|
628
|
+
// the loop's own message file into an amend of the last one. `ARG_RE` already accepts a drive
|
|
629
|
+
// letter (`C:/…`) via its `:`, so tightening it here costs nothing the loop's own writer uses.
|
|
630
|
+
const ARG_RE = '[\\w./:@^~=+,-]+';
|
|
631
|
+
const SEG_ADD_RE = new RegExp(`^git\\s+add(\\s+${ARG_RE})*$`);
|
|
632
|
+
const SEG_COMMIT_RE = new RegExp(`^git\\s+commit(\\s+-q)?\\s+(-F\\s+|--file[=\\s])${ARG_RE}(\\s+-q)?$`);
|
|
633
|
+
const SEG_READONLY_RE = new RegExp(`^git\\s+(status|diff|log|rev-parse|show)(\\s+${ARG_RE})*$`);
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* `git push`, exactly: `[-u|--set-upstream] <remote> HEAD:<branch>` or
|
|
637
|
+
* `HEAD:refs/heads/<branch>` — the ONLY exempt form. Bare `git push`, a remote alone, a plain
|
|
638
|
+
* branch name, or bare `HEAD` are no longer enough: every one of those is redirectable by
|
|
639
|
+
* `push.default`/`remote.*.push`/an upstream config this read cannot see, which is exactly how
|
|
640
|
+
* the review's re-review turned "looks like it only pushes `loop/x`" into "pushes wherever the
|
|
641
|
+
* repo's config says". An EXPLICIT `HEAD:<ref>` refspec is immune to both — git ignores
|
|
642
|
+
* `push.default` once a refspec is given, and a mirror/`+`/`:`-prefixed form refuses to
|
|
643
|
+
* coexist with one. The remote's first character must be alphanumeric so a flag (`-f`,
|
|
644
|
+
* `--all`, `--delete`, …) can never be read as a remote name.
|
|
645
|
+
*/
|
|
646
|
+
function pushSegmentOk(seg, branch) {
|
|
647
|
+
const m = /^git\s+push(?:\s+(.*))?$/.exec(seg);
|
|
648
|
+
if (!m) return false;
|
|
649
|
+
const rest = (m[1] || '').trim();
|
|
650
|
+
if (!rest) return false;
|
|
651
|
+
const tokens = rest.split(/\s+/);
|
|
652
|
+
let i = 0;
|
|
653
|
+
if (tokens[i] === '-u' || tokens[i] === '--set-upstream') i += 1;
|
|
654
|
+
const remote = tokens[i];
|
|
655
|
+
if (remote === undefined || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(remote)) return false;
|
|
656
|
+
i += 1;
|
|
657
|
+
const ref = tokens[i];
|
|
658
|
+
i += 1;
|
|
659
|
+
if (ref === undefined || i !== tokens.length) return false;
|
|
660
|
+
return ref === `HEAD:${branch}` || ref === `HEAD:refs/heads/${branch}`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Consent Before Push's loop/* exemption: true only when EVERY clause holds —
|
|
665
|
+
* (1) the command carries none of the characters that could smuggle a second effect past this
|
|
666
|
+
* read, (2)-(3) every `&&`/`;`/`||`/`|`-separated segment is one of the three whitelisted git
|
|
667
|
+
* shapes above, with every push segment also passing `pushSegmentOk`, and (4) the branch
|
|
668
|
+
* checked out in `cwd` starts with `loop/` AND its git root carries a launched LOOP.md. Splitting
|
|
669
|
+
* on the separators without respecting quotes is deliberate, not an oversight: (1) and the
|
|
670
|
+
* per-segment whitelist already refuse any segment a quoted separator could produce, because a
|
|
671
|
+
* quote character fails every one of the three segment patterns.
|
|
672
|
+
*/
|
|
673
|
+
export function loopExempt(command, cwd) {
|
|
674
|
+
if (typeof command !== 'string' || UNSAFE_CHARS_RE.test(command)) return false;
|
|
675
|
+
const branch = currentBranch(cwd);
|
|
676
|
+
if (!branch || !branch.startsWith('loop/')) return false;
|
|
677
|
+
const root = gitRootOf(cwd);
|
|
678
|
+
if (!root || !loopLaunched(root)) return false;
|
|
679
|
+
const segments = command.split(/&&|\|\||[;|]/).map((s) => s.trim()).filter(Boolean);
|
|
680
|
+
if (!segments.length) return false;
|
|
681
|
+
return segments.every((seg) => SEG_ADD_RE.test(seg) || SEG_COMMIT_RE.test(seg)
|
|
682
|
+
|| SEG_READONLY_RE.test(seg) || pushSegmentOk(seg, branch));
|
|
683
|
+
}
|
|
684
|
+
|
|
602
685
|
/** The `permissionDecision: "ask"` document, in Python's compact `json.dumps` spelling. */
|
|
603
686
|
function askDecision(reason) {
|
|
604
687
|
return `${jsonDumpsCompact({
|
|
@@ -739,8 +822,12 @@ function gitDecide(args, payload) {
|
|
|
739
822
|
+ 'discarding git act needs confirmation bound to this specific command');
|
|
740
823
|
}
|
|
741
824
|
// `--no-consent`: the process pack (or process 5 alone) is off, so the commit/push ask has
|
|
742
|
-
// no rule behind it — but Law IV above is universal and has already run.
|
|
825
|
+
// no rule behind it — but Law IV above is universal and has already run. Checked before
|
|
826
|
+
// `loopExempt` so a non-git command, or one with consent off, never pays for the whitelist.
|
|
743
827
|
if (args.noConsent || !GIT_GATE_RE.test(command)) return 0;
|
|
828
|
+
// Consent Before Push: a loop launched on its own `loop/*` branch is a named batch for a
|
|
829
|
+
// fixed set of commit/push forms on that branch only — never a merge into anything shared.
|
|
830
|
+
if (loopExempt(command, (payload && payload.cwd) || process.cwd())) return 0;
|
|
744
831
|
return ask(args, 'git-gate', 'process-5', 'Geneseed (Consent Before Push) \u2014 every git '
|
|
745
832
|
+ 'commit/push needs explicit approval; to see the change first, use the explain-changes skill');
|
|
746
833
|
}
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -230,6 +230,27 @@ function leadOfDir(d, names) {
|
|
|
230
230
|
export const postureOfDir = (d) => leadOfDir(d, discoverNames('postures', 'peer'));
|
|
231
231
|
export const modeOfDir = (d) => leadOfDir(d, discoverNames('modes', 'direct'));
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* The loop skill's default trust preset, read back off a deployed install — `null` when no
|
|
235
|
+
* loop skill is deployed there.
|
|
236
|
+
*
|
|
237
|
+
* CARRIED BY THE SKILL, NOT BY A CARRIER: `--trust` is the one build axis with zero always-on
|
|
238
|
+
* footprint, so it is not in AGENT.md to be found. The skill renders a literal
|
|
239
|
+
* `Default trust preset: **<Label>**` line, and this scans the places an emit lands it: the
|
|
240
|
+
* native layer (`skills/loop/SKILL.md`, in the root itself for a global install and under the
|
|
241
|
+
* host's `projectMarker` for a project one) and the plain bundle (`skills/loop.md`).
|
|
242
|
+
*/
|
|
243
|
+
const TRUST_RE = /^Default trust preset: \*\*(Prudent|Balanced|Aggressive)\*\*/m;
|
|
244
|
+
export function trustOfDir(d) {
|
|
245
|
+
for (const base of ['', ...HOSTS.map((h) => h.projectMarker)]) {
|
|
246
|
+
for (const rel of [['skills', 'loop', 'SKILL.md'], ['skills', 'loop.md']]) {
|
|
247
|
+
const m = TRUST_RE.exec(readMaybe(path.join(d, base, ...rel)) ?? '');
|
|
248
|
+
if (m) return m[1].toLowerCase();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
233
254
|
/** The `Active packs:` marker line the template emits, in one of the five carriers. */
|
|
234
255
|
const ACTIVE_PACKS_RE = /^Active packs:[ \t]*(.+?)[ \t]*$/m;
|
|
235
256
|
|
package/js/hosts/mcp.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export const MCP_PRESETS = {
|
|
|
68
68
|
+ 'install). Edit GITLAB_PERSONAL_ACCESS_TOKEN (scopes: api, read_repository) '
|
|
69
69
|
+ 'and GITLAB_API_URL before use. For a second instance (a self-hosted server), '
|
|
70
70
|
+ 'copy this block by hand under a new name and point it at the other URL — '
|
|
71
|
-
+ '
|
|
71
|
+
+ 'docs/guides/mcp.md has the pattern.',
|
|
72
72
|
block: {
|
|
73
73
|
type: 'local',
|
|
74
74
|
command: ['npx', '-y', '@zereight/mcp-gitlab'],
|
package/js/hosts/settings.mjs
CHANGED
|
@@ -483,6 +483,18 @@ export function hookShimPath(platform = process.platform) {
|
|
|
483
483
|
return path.join(shimHome(), ...shimRel(platform));
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
+
/**
|
|
487
|
+
* The paths the machine's hook shim names that are gone — `[]` when the shim is absent or live.
|
|
488
|
+
*
|
|
489
|
+
* Cheap on purpose: `status`, the doctor and EVERY CLI run call it (the run-time warning in
|
|
490
|
+
* `bin/geneseed-cli.mjs`), because a dead shim disables every hook of every Claude-shaped
|
|
491
|
+
* install and nothing else would say so. An absent shim is not dead — a checkout that has never
|
|
492
|
+
* emitted owns none — and an unreadable one is reported by the doctor, not here.
|
|
493
|
+
*/
|
|
494
|
+
export function shimDead() {
|
|
495
|
+
try { return shimDeadPaths(readText(hookShimPath())); } catch { return []; }
|
|
496
|
+
}
|
|
497
|
+
|
|
486
498
|
/**
|
|
487
499
|
* `_build_settings._hook_shim_body`, with the two volatile values INJECTED.
|
|
488
500
|
*
|
|
@@ -988,7 +1000,7 @@ export function migrateShape(commands, shimBody) {
|
|
|
988
1000
|
/**
|
|
989
1001
|
* `_build_settings._autostart_paths` — where a hand-written web-daemon autostart entry lives.
|
|
990
1002
|
*
|
|
991
|
-
* NOTHING IN THIS REPOSITORY HAS EVER WRITTEN ONE —
|
|
1003
|
+
* NOTHING IN THIS REPOSITORY HAS EVER WRITTEN ONE — the install guide tells the user to create them
|
|
992
1004
|
* by hand, and no .py/.mjs/.sh/.cmd in the tree contains `vbs`, `LaunchAgents` or `plist`. So
|
|
993
1005
|
* `migrate` REPORTS a stale one and never rewrites it, on the rule `settingsIntegrityCheck`
|
|
994
1006
|
* already states: an entry the manifest does not claim is "possibly user-authored; left alone".
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import {
|
|
16
|
-
CONFIG, PACK_ORDER, PLUGIN_SRC, RETIRED_RULE_IDS, ROOT, RULE_ID_RE, SRC, THEMES, canonFiles,
|
|
16
|
+
CONFIG, DOCS, DOC_FOLDERS, PACK_ORDER, PLUGIN_SRC, RETIRED_RULE_IDS, ROOT, RULE_ID_RE, SRC, THEMES, canonFiles,
|
|
17
17
|
knownRuleIds, ruleCanon, titleKey,
|
|
18
18
|
} from '../build/source.mjs';
|
|
19
19
|
import { themeFiles } from '../hosts/installs.mjs';
|
|
@@ -25,6 +25,7 @@ import { NO_WINDOW } from '../lib/proc.mjs';
|
|
|
25
25
|
import {
|
|
26
26
|
folderSkillProblems, registryProblems, secretProblems, vendorPinProblems,
|
|
27
27
|
} from './checks-repo.mjs';
|
|
28
|
+
import { catalogProblems } from '../loop/catalog.mjs';
|
|
28
29
|
import { LAW_CLASS, LAW_CLASSES, SKILL_CLASS } from './inventory.mjs';
|
|
29
30
|
import { NOTE, globSorted, has, isDir, isFile, rglob, srcStems } from './scan.mjs';
|
|
30
31
|
import { spawnSync } from 'node:child_process';
|
|
@@ -763,7 +764,7 @@ export function countTableProblems() {
|
|
|
763
764
|
|
|
764
765
|
// WHERE THE ONBOARDING COPY LIVES, AND WHY THIS READ MOVED. It used to open the one module
|
|
765
766
|
// that held the web console's onboarding prose. That prose has since moved into
|
|
766
|
-
// `docs
|
|
767
|
+
// `docs/<folder>/*.md`, and the counts in it render from `{N_LAWS}` / `{N_AGENTS}` / `{N_SKILLS}`
|
|
767
768
|
// — so the read was still succeeding against a file the sentences had left, all three arms
|
|
768
769
|
// below scored zero, and the check looked healthy while gating nothing. A templated count
|
|
769
770
|
// cannot drift; what these arms still catch is a maintainer typing the NUMBER into a page
|
|
@@ -773,7 +774,9 @@ export function countTableProblems() {
|
|
|
773
774
|
// byte moves. `web` stays fail-soft — a missing docs tree is not an authoring fault.
|
|
774
775
|
let web = '';
|
|
775
776
|
try {
|
|
776
|
-
web =
|
|
777
|
+
web = DOC_FOLDERS
|
|
778
|
+
.flatMap((f) => (isDir(path.join(DOCS, f))
|
|
779
|
+
? globSorted(path.join(DOCS, f), (n) => n.endsWith('.md')) : []))
|
|
777
780
|
.map(readText).join('\n');
|
|
778
781
|
} catch { web = ''; }
|
|
779
782
|
// ⚠ NOT FAIL-SOFT, UNLIKE `web` ABOVE, AND THE ASYMMETRY IS THE POINT. A missing `docs/web`
|
|
@@ -834,6 +837,15 @@ function loadLearnPromptHead() {
|
|
|
834
837
|
+ 'When in doubt, output exactly: NOTHING.';
|
|
835
838
|
}
|
|
836
839
|
|
|
840
|
+
/**
|
|
841
|
+
* The shipped loop catalogue parses and every shipped template passes the engine's own graph
|
|
842
|
+
* rules. Global and project bricks are deliberately out of scope: doctor judges what this
|
|
843
|
+
* checkout ships, not what a user wrote in their config.
|
|
844
|
+
*/
|
|
845
|
+
export function loopProblems() {
|
|
846
|
+
return catalogProblems({ projectRoot: null, globalLevel: false });
|
|
847
|
+
}
|
|
848
|
+
|
|
837
849
|
/**
|
|
838
850
|
* `_harness_build._authoring_problems` — author-time gates on the source specs and plugins.
|
|
839
851
|
*
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
} from '../hosts/hosts.mjs';
|
|
47
47
|
import {
|
|
48
48
|
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, footprintOfDir, modeOfDir, postureOfDir,
|
|
49
|
+
trustOfDir,
|
|
49
50
|
readJsonMaybe, readMaybe, themeOfDir,
|
|
50
51
|
} from '../hosts/installs.mjs';
|
|
51
52
|
import { unifiedDiff, splitLines } from '../lib/udiff.mjs';
|
|
@@ -175,6 +176,8 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
175
176
|
// `null` (no marker) means unknown and renders at all four, which `doctrinesForBuild`
|
|
176
177
|
// makes explicit rather than leaving to `makeCfg`'s parameter default two layers down.
|
|
177
178
|
posture: postureOfDir(dir), mode: modeOfDir(dir), doctrines: doctrinesForBuild(dir),
|
|
179
|
+
// The loop skill's preset, or its `Default trust preset:` line reads as edited.
|
|
180
|
+
trust: trustOfDir(dir),
|
|
178
181
|
}));
|
|
179
182
|
// The one place a render depends on WHERE it is installed: a global Bob emit writes each
|
|
180
183
|
// folder skill's absolute directory into its SKILL.md (see `writeNativeLayer`), so the
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -36,7 +36,7 @@ import { resolvePath } from '../hosts/hosts.mjs';
|
|
|
36
36
|
import { installedDefaults, themeFiles } from '../hosts/installs.mjs';
|
|
37
37
|
import { validateIsVendored } from '../hosts/native.mjs';
|
|
38
38
|
import { printOut } from '../lib/fs.mjs';
|
|
39
|
-
import { authoringProblems } from './checks-authoring.mjs';
|
|
39
|
+
import { authoringProblems, loopProblems } from './checks-authoring.mjs';
|
|
40
40
|
import {
|
|
41
41
|
checkBuild, colorThemeProblems, renderedProblems, themeParityProblems,
|
|
42
42
|
} from './checks-build.mjs';
|
|
@@ -217,6 +217,7 @@ export function doctorCollect({
|
|
|
217
217
|
problems = problems.concat(ran('parity', 'Theme parity', themeParityProblems()));
|
|
218
218
|
problems = problems.concat(ran('colors', 'Colour themes', colorThemeProblems()));
|
|
219
219
|
problems = problems.concat(ran('authoring', 'Authoring gates', authoringProblems()));
|
|
220
|
+
problems = problems.concat(ran('loops', 'Loop catalogue', loopProblems()));
|
|
220
221
|
problems = problems.concat(ran('shim', 'Hook shim', shimProbs));
|
|
221
222
|
problems = problems.concat(ran('map', 'Module map', moduleMapProblems()));
|
|
222
223
|
// P10c's `cli` check is GONE, and the reason is not that it stopped mattering. It hashed
|
|
@@ -283,7 +284,7 @@ export function cmdDoctor(args) {
|
|
|
283
284
|
// (`js/build/generate.mjs`), which forwards `--theme` and nothing else. So the front door
|
|
284
285
|
// answered `unrecognized arguments: --sync-themes` to anyone who followed doctor's own
|
|
285
286
|
// advice, for as long as the premise went unchecked. The flag belongs to the GENERATOR,
|
|
286
|
-
// whose binary is `geneseed-build` — which is what `README.md` and
|
|
287
|
+
// whose binary is `geneseed-build` — which is what `README.md` and the install guide have said
|
|
287
288
|
// all along. A hint is a command; if it is not runnable it is decoration.
|
|
288
289
|
printOut(' tip: a theme is missing a key another theme defines — run '
|
|
289
290
|
+ '`geneseed-build --sync-themes` to fill it from _TEMPLATE.json, '
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -91,6 +91,7 @@ export const LAW_CLASSES = ['security', 'process', 'verify', 'craft', 'context',
|
|
|
91
91
|
export const SKILL_CLASS = {
|
|
92
92
|
brainstorm: 'design', plan: 'design', council: 'design',
|
|
93
93
|
workflow: 'design', 'parallel-agents': 'design', pipeline: 'design',
|
|
94
|
+
loop: 'design', 'brick-forge': 'design',
|
|
94
95
|
'codebase-design': 'design', 'domain-modeling': 'design', wayfinder: 'design',
|
|
95
96
|
tickets: 'design',
|
|
96
97
|
develop: 'build', refactor: 'build', debug: 'build', migrate: 'build',
|
|
@@ -108,7 +109,7 @@ export const SKILL_CLASS = {
|
|
|
108
109
|
// `understand` before, which made that class a grab-bag.
|
|
109
110
|
wiki: 'harness', geneseed: 'harness', rule: 'harness', profile: 'harness',
|
|
110
111
|
herdr: 'harness', 'skill-forge': 'harness', 'consolidate-memory': 'harness',
|
|
111
|
-
teach: 'learn', quiz: 'learn',
|
|
112
|
+
teach: 'learn', quiz: 'learn', 'learn-mode': 'learn',
|
|
112
113
|
};
|
|
113
114
|
|
|
114
115
|
/** `k in dict` — own keys only, where `k in obj` also finds `Object.prototype` members. */
|
package/js/inspect/registry.mjs
CHANGED
|
@@ -17,10 +17,10 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync } from 'n
|
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
|
|
20
|
-
import { expanduser } from '../hosts/hosts.mjs';
|
|
20
|
+
import { expanduser, resolvePath } from '../hosts/hosts.mjs';
|
|
21
21
|
import { writeText, isFile, isDir } from '../lib/fs.mjs';
|
|
22
22
|
import { parseJson, jsonDumpsIndent } from '../lib/json.mjs';
|
|
23
|
-
import { toPlatformPath } from '../lib/paths.mjs';
|
|
23
|
+
import { toPlatformPath, within } from '../lib/paths.mjs';
|
|
24
24
|
|
|
25
25
|
/** `_install_registry._path()` — `$XDG_CONFIG_HOME/geneseed/installs.json`. */
|
|
26
26
|
function registryPath() {
|
|
@@ -28,6 +28,21 @@ function registryPath() {
|
|
|
28
28
|
return path.join(base, 'geneseed', 'installs.json');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Is `root` a scratch install — inside the OS temp dir while the registry is not?
|
|
33
|
+
*
|
|
34
|
+
* Agent sessions emit into scratchpads under `%TEMP%`; recorded, each one is rebuilt by every
|
|
35
|
+
* `rebuild-all` and listed by every `status` for as long as the folder survives. The second
|
|
36
|
+
* clause is what keeps every test and golden cell meaningful: their registry lives in a
|
|
37
|
+
* sandbox under the temp dir too, so their temp roots are not scratch RELATIVE TO IT.
|
|
38
|
+
* `within` is case-insensitive on win32, so `C:\Users\X\AppData\Local\Temp` and its
|
|
39
|
+
* lower-cased spelling are one directory.
|
|
40
|
+
*/
|
|
41
|
+
export function isScratchRoot(root, registryFile) {
|
|
42
|
+
const tmp = resolvePath(os.tmpdir());
|
|
43
|
+
return within(resolvePath(root), tmp) && !within(resolvePath(registryFile), tmp);
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
function registryLoad() {
|
|
32
47
|
try {
|
|
33
48
|
const data = parseJson(readFileSync(registryPath(), 'utf8'));
|
|
@@ -59,6 +74,7 @@ export function registryRecord(dir) {
|
|
|
59
74
|
try {
|
|
60
75
|
let root;
|
|
61
76
|
try { root = realpathSync.native(dir); } catch { root = path.resolve(dir); }
|
|
77
|
+
if (isScratchRoot(root, registryPath())) return;
|
|
62
78
|
const cur = registryLoad();
|
|
63
79
|
if (!cur.includes(root)) registrySave([...cur, root]);
|
|
64
80
|
} catch { /* a registry hiccup must never fail a build */ }
|
|
@@ -75,6 +91,7 @@ export function registryRecord(dir) {
|
|
|
75
91
|
*/
|
|
76
92
|
export function registryRoots() {
|
|
77
93
|
const original = registryLoad();
|
|
94
|
+
const file = registryPath();
|
|
78
95
|
const out = [];
|
|
79
96
|
const kept = [];
|
|
80
97
|
const seen = new Set();
|
|
@@ -96,7 +113,7 @@ export function registryRoots() {
|
|
|
96
113
|
let key;
|
|
97
114
|
try { key = existsSync(root) ? realpathSync.native(root) : root; } catch { key = root; }
|
|
98
115
|
if (seen.has(key)) continue;
|
|
99
|
-
if (isDir(root) && isFile(path.join(root, '.geneseed-emit'))) {
|
|
116
|
+
if (isDir(root) && isFile(path.join(root, '.geneseed-emit')) && !isScratchRoot(root, file)) {
|
|
100
117
|
seen.add(key);
|
|
101
118
|
kept.push(root);
|
|
102
119
|
out.push(root);
|
package/js/inspect/status.mjs
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
defaultTheme, installedDefaults, installTargets, manifestIsClaude, readJsonMaybe, readMaybe,
|
|
74
74
|
} from '../hosts/installs.mjs';
|
|
75
75
|
import { installProfile, rebuildCommand } from '../build/generate.mjs';
|
|
76
|
+
import { shimDead } from '../hosts/settings.mjs';
|
|
76
77
|
import { printOut } from '../lib/fs.mjs';
|
|
77
78
|
import { codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
78
79
|
|
|
@@ -208,7 +209,9 @@ export function gateSummary(cfgDirs) {
|
|
|
208
209
|
total += 1;
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
|
-
|
|
212
|
+
// `dead` is the machine shim's missing targets: every gate below is armed on paper and never
|
|
213
|
+
// runs, so it outranks `armed` in the row.
|
|
214
|
+
return { standing_down: standingDown, asks, total, dead: shimDead() };
|
|
212
215
|
}
|
|
213
216
|
|
|
214
217
|
/** `_harness_status._status_data`. */
|
|
@@ -350,7 +353,9 @@ export function statusLines(d, color = false) {
|
|
|
350
353
|
// panels carry no `gates` key and must render unchanged.
|
|
351
354
|
if (d.gates) {
|
|
352
355
|
const g = d.gates;
|
|
353
|
-
const armed = g.
|
|
356
|
+
const armed = g.dead?.length
|
|
357
|
+
? `DEAD — hook shim points at ${g.dead.join(', ')}; run: geneseed rebuild-all`
|
|
358
|
+
: g.standing_down.length
|
|
354
359
|
? `STANDING DOWN for this cwd (excludes.json in ${g.standing_down.join(', ')})`
|
|
355
360
|
: 'armed';
|
|
356
361
|
const byRule = Object.entries(g.asks).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
@@ -367,7 +372,7 @@ export function statusLines(d, color = false) {
|
|
|
367
372
|
}
|
|
368
373
|
const packs = p.doctrines.length ? p.doctrines.join(',') : 'no packs';
|
|
369
374
|
const excl = p.excludeRules?.length ? `excluding ${p.excludeRules.join(',')}` : 'no rules excluded';
|
|
370
|
-
rows.push([`${p.host}:${p.scope}`, [p.state, p.theme, p.footprint, `${p.posture}/${p.mode}`,
|
|
375
|
+
rows.push([`${p.host}:${p.scope}`, [p.state, p.theme, p.footprint, `${p.posture}/${p.mode}/${p.trust}`,
|
|
371
376
|
packs, excl].join(` ${DOT} `) + where]);
|
|
372
377
|
}
|
|
373
378
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node floor — the FIRST import of all three binaries, so an old Node gets one sentence
|
|
3
|
+
* instead of a stack trace from whichever API it lacks.
|
|
4
|
+
*
|
|
5
|
+
* `engines` in package.json only makes npm warn, and `npx geneseed`, a global install and the
|
|
6
|
+
* hook shim all run the binaries directly. ESM links the whole graph before any body runs, so
|
|
7
|
+
* this only helps while every file still PARSES on the old Node; it runs before any of them
|
|
8
|
+
* executes. The hook exits 0: a hook must never block a tool call, and the message on stderr
|
|
9
|
+
* is what the host surfaces. Nothing here may print to stdout — the hook's decision channel.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
export const NODE_FLOOR = '22.3.0';
|
|
14
|
+
|
|
15
|
+
/** Numeric, not string, comparison: '22.10.0' is above '22.3.0'. */
|
|
16
|
+
export function belowFloor(version, floor = NODE_FLOOR) {
|
|
17
|
+
const v = version.replace(/^v/, '').split('.').map(Number);
|
|
18
|
+
const f = floor.split('.').map(Number);
|
|
19
|
+
for (let i = 0; i < 3; i += 1) {
|
|
20
|
+
if ((v[i] || 0) !== f[i]) return (v[i] || 0) < f[i];
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (belowFloor(process.versions.node)) {
|
|
26
|
+
process.stderr.write(`geneseed needs Node 22.3 or newer; this is Node ${process.versions.node}. `
|
|
27
|
+
+ 'Install a current Node from https://nodejs.org (or your package manager), then run the '
|
|
28
|
+
+ 'command again.\n');
|
|
29
|
+
process.exit(path.basename(process.argv[1] || '').startsWith('geneseed-hook') ? 0 : 1);
|
|
30
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where bricks and loop templates come from, and which one wins.
|
|
3
|
+
*
|
|
4
|
+
* THREE ORIGINS, ONE LADDER. Shipped (this package's own `src/`), global (the user's, beside
|
|
5
|
+
* `installs.json` under XDG) and project (`<repo>/.geneseed/`, committed, shared with a team).
|
|
6
|
+
* A later origin replaces an earlier one BY NAME — that is the whole point: a team adapts the
|
|
7
|
+
* shipped `reproduce` to its stack without copying every template that uses it. The cost is
|
|
8
|
+
* that a fixed shipped brick stays masked by the override, so the override is reported
|
|
9
|
+
* (`overridden`), never silent.
|
|
10
|
+
*
|
|
11
|
+
* WHY SHIPPED BRICKS ARE READ FROM `src/`, NOT FROM AN INSTALL. The model never reads a brick
|
|
12
|
+
* file: `geneseed loop next` hands it the brick's prompt. So the CLI reads its own source and no
|
|
13
|
+
* host emit has to learn a new directory — four hosts get the catalogue for free and cannot drift.
|
|
14
|
+
*
|
|
15
|
+
* Templates are parsed with JSON.parse on purpose: they are read, never written back into a
|
|
16
|
+
* user's file, so the int/float distinction `parseJson` preserves has nowhere to matter.
|
|
17
|
+
*/
|
|
18
|
+
import { readdirSync } from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { readText, isFile, isDir } from '../lib/fs.mjs';
|
|
22
|
+
import { frontmatter } from '../hosts/hooks.mjs';
|
|
23
|
+
import { SRC } from '../build/source.mjs';
|
|
24
|
+
import { checkGraph } from './graph.mjs';
|
|
25
|
+
|
|
26
|
+
export function userLoopsDir() {
|
|
27
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
28
|
+
return path.join(base, 'geneseed');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const KEBAB = /^[a-z0-9][a-z0-9-]*$/;
|
|
32
|
+
|
|
33
|
+
export function parseBrick(text, origin) {
|
|
34
|
+
const [fm, body] = frontmatter(text);
|
|
35
|
+
const get = (k) => (fm.get(k) ?? '').trim();
|
|
36
|
+
const name = get('name');
|
|
37
|
+
const problems = [];
|
|
38
|
+
if (!KEBAB.test(name)) problems.push(`name ${JSON.stringify(name)} must be kebab-case`);
|
|
39
|
+
const description = get('description');
|
|
40
|
+
if (!description) problems.push(`${name}: description is empty`);
|
|
41
|
+
const effect = get('effect');
|
|
42
|
+
if (effect !== 'read' && effect !== 'mutate') {
|
|
43
|
+
problems.push(`${name}: effect must be read or mutate, not ${JSON.stringify(effect)}`);
|
|
44
|
+
}
|
|
45
|
+
const agent = get('agent') || null;
|
|
46
|
+
const skill = get('skill') || null;
|
|
47
|
+
if (!agent === !skill) problems.push(`${name}: exactly one of agent or skill`);
|
|
48
|
+
const outcomes = get('outcomes').split(',').map((s) => s.trim()).filter(Boolean);
|
|
49
|
+
if (!outcomes.length) problems.push(`${name}: outcomes is empty`);
|
|
50
|
+
return {
|
|
51
|
+
brick: { name, description, effect, agent, skill, outcomes, body: body.trim(), origin },
|
|
52
|
+
problems,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const listFiles = (dir, ext) => (isDir(dir)
|
|
57
|
+
? readdirSync(dir).filter((n) => n.endsWith(ext) && !n.startsWith('_') && n !== 'README.md').sort()
|
|
58
|
+
: []);
|
|
59
|
+
|
|
60
|
+
function availability(brick, srcRoot) {
|
|
61
|
+
if (brick.agent && !isFile(path.join(srcRoot, 'agents', `${brick.agent}.md`))) {
|
|
62
|
+
return { available: false, reason: `agent ${brick.agent} is not shipped` };
|
|
63
|
+
}
|
|
64
|
+
if (brick.skill && !isFile(path.join(srcRoot, 'skills', `${brick.skill}.md`))
|
|
65
|
+
&& !isDir(path.join(srcRoot, 'skills', brick.skill))) {
|
|
66
|
+
return { available: false, reason: `skill ${brick.skill} is not shipped` };
|
|
67
|
+
}
|
|
68
|
+
return { available: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function loadCatalog({
|
|
72
|
+
projectRoot = process.cwd(), srcRoot = SRC, globalLevel = true,
|
|
73
|
+
} = {}) {
|
|
74
|
+
const levels = [
|
|
75
|
+
['shipped', path.join(srcRoot, 'bricks'), path.join(srcRoot, 'loops')],
|
|
76
|
+
];
|
|
77
|
+
if (globalLevel) {
|
|
78
|
+
levels.push(['global', path.join(userLoopsDir(), 'bricks'), path.join(userLoopsDir(), 'loops')]);
|
|
79
|
+
}
|
|
80
|
+
if (projectRoot) {
|
|
81
|
+
levels.push(['project', path.join(projectRoot, '.geneseed', 'bricks'), path.join(projectRoot, '.geneseed', 'loops')]);
|
|
82
|
+
}
|
|
83
|
+
const bricks = new Map(); const templates = new Map(); const problems = []; const overridden = [];
|
|
84
|
+
for (const [origin, brickDir, loopDir] of levels) {
|
|
85
|
+
for (const f of listFiles(brickDir, '.md')) {
|
|
86
|
+
const { brick, problems: p } = parseBrick(readText(path.join(brickDir, f)), origin);
|
|
87
|
+
problems.push(...p.map((x) => `bricks/${f} (${origin}): ${x}`));
|
|
88
|
+
if (p.length) continue;
|
|
89
|
+
const prior = bricks.get(brick.name);
|
|
90
|
+
if (prior) overridden.push(`${brick.name} (${origin}, overrides ${prior.origin})`);
|
|
91
|
+
bricks.set(brick.name, { ...brick, ...availability(brick, srcRoot) });
|
|
92
|
+
}
|
|
93
|
+
for (const f of listFiles(loopDir, '.json')) {
|
|
94
|
+
let graph;
|
|
95
|
+
try { graph = JSON.parse(readText(path.join(loopDir, f))); } catch {
|
|
96
|
+
problems.push(`loops/${f}: not JSON`); continue;
|
|
97
|
+
}
|
|
98
|
+
const stem = f.slice(0, -'.json'.length);
|
|
99
|
+
if (graph.name !== stem) {
|
|
100
|
+
problems.push(`loops/${f}: name ${JSON.stringify(graph.name)} does not match the file name`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
templates.set(stem, { ...graph, origin });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { bricks, templates, problems, overridden: overridden.sort() };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function catalogProblems(opts = {}) {
|
|
110
|
+
const { bricks, templates, problems } = loadCatalog(opts);
|
|
111
|
+
const out = [...problems];
|
|
112
|
+
for (const [name, graph] of [...templates].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
113
|
+
out.push(...checkGraph(graph, bricks).map((p) => `loops/${name}: ${p}`));
|
|
114
|
+
}
|
|
115
|
+
return out.sort();
|
|
116
|
+
}
|