geneseed 3.12.0 → 3.14.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 +160 -0
- package/README.md +2 -2
- package/SHIPPED.md +1 -1
- package/adapters/bob/README.md +1 -1
- package/adapters/claude-code/README.md +1 -1
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/README.md +3 -3
- package/adapters/opencode/plugins/geneseed-learn.js +10 -1
- package/bin/geneseed-cli.mjs +10 -14
- package/bin/geneseed-hook.mjs +22 -15
- package/docs/README.md +41 -22
- package/docs/concepts/agents.md +3 -1
- package/docs/concepts/collaboration.md +3 -1
- package/docs/concepts/exclusions.md +4 -2
- package/docs/concepts/footprint.md +3 -1
- package/docs/concepts/foreman-mode.md +3 -1
- package/docs/concepts/hook-gates.md +61 -0
- package/docs/concepts/hooks.md +8 -56
- package/docs/concepts/host-matrix.md +38 -0
- package/docs/concepts/hosts.md +6 -32
- package/docs/concepts/loop-branch.md +66 -0
- package/docs/concepts/loop-bricks.md +48 -0
- package/docs/concepts/loop-contracts.md +33 -0
- package/docs/concepts/loop-gates.md +30 -0
- package/docs/concepts/loop-notes.md +43 -0
- package/docs/concepts/loop-risk.md +36 -0
- package/docs/concepts/loop-templates.md +50 -0
- package/docs/concepts/loops.md +24 -116
- package/docs/concepts/lsp.md +2 -0
- package/docs/concepts/memory.md +3 -1
- package/docs/concepts/notebook.md +3 -1
- package/docs/concepts/rules.md +4 -2
- package/docs/concepts/skills.md +3 -1
- package/docs/concepts/themes.md +3 -1
- package/docs/extending.md +28 -13
- package/docs/guides/autostart.md +3 -1
- package/docs/guides/choose-your-setup.md +5 -80
- package/docs/guides/headless.md +5 -3
- package/docs/guides/install-agent-md.md +37 -0
- package/docs/guides/install-bob.md +20 -0
- package/docs/guides/install-claude-code.md +22 -0
- package/docs/guides/install-clone.md +34 -0
- package/docs/guides/install-openclaude.md +25 -0
- package/docs/guides/install-opencode.md +35 -0
- package/docs/guides/install-scripted.md +21 -0
- package/docs/guides/install.md +9 -118
- package/docs/guides/loop-finish.md +54 -0
- package/docs/guides/loop-running.md +66 -0
- package/docs/guides/loop-which-template.md +35 -0
- package/docs/guides/mcp-filesystem.md +41 -0
- package/docs/guides/mcp-gitlab.md +66 -0
- package/docs/guides/mcp-markitdown.md +36 -0
- package/docs/guides/mcp.md +8 -137
- package/docs/guides/migrate.md +3 -1
- package/docs/guides/project-context.md +4 -2
- package/docs/guides/read-non-markdown.md +17 -0
- package/docs/guides/run-a-loop.md +64 -0
- package/docs/guides/run-anywhere.md +2 -0
- package/docs/guides/setup-doctrines.md +55 -0
- package/docs/guides/setup-footprint.md +48 -0
- package/docs/guides/uninstall.md +3 -1
- package/docs/guides/upgrade.md +3 -1
- package/docs/guides/verify.md +3 -1
- package/docs/guides/wiki.md +3 -1
- package/docs/guides/worktree.md +3 -1
- package/docs/reference/about.md +3 -1
- package/docs/reference/cli.md +2 -0
- package/docs/reference/env-context.md +40 -0
- package/docs/reference/environment.md +10 -59
- package/docs/reference/glossary.md +3 -1
- package/docs/reference/loop-actions.md +20 -0
- package/docs/reference/opencode-plugins.md +14 -118
- package/docs/reference/plugin-activity.md +20 -0
- package/docs/reference/plugin-context.md +27 -0
- package/docs/reference/plugin-guard.md +27 -0
- package/docs/reference/plugin-learn.md +27 -0
- package/docs/reference/plugin-notify.md +20 -0
- package/docs/reference/plugin-ponytail.md +19 -0
- package/docs/reference/plugin-workflow.md +25 -0
- package/docs/reference/troubleshoot-doctor.md +50 -0
- package/docs/reference/troubleshoot-hooks.md +99 -0
- package/docs/reference/troubleshoot-install.md +108 -0
- package/docs/reference/troubleshoot-tools.md +56 -0
- package/docs/reference/troubleshoot-update.md +23 -0
- package/docs/reference/troubleshoot-web.md +66 -0
- package/docs/reference/troubleshooting.md +13 -337
- package/docs/understand/a-day.md +3 -1
- package/docs/understand/enforced-vs-asked.md +3 -1
- package/docs/understand/harness.md +2 -0
- package/docs/understand/machine-bob.md +17 -0
- package/docs/understand/machine-claude.md +35 -0
- package/docs/understand/machine-opencode.md +36 -0
- package/docs/understand/on-your-machine.md +12 -55
- package/docs/understand/take-it-out.md +13 -0
- package/docs/web-ui.md +35 -15
- package/harness.config.json +1 -1
- package/js/README.md +30 -13
- package/js/build/args.mjs +374 -0
- package/js/build/driver.mjs +137 -553
- package/js/build/emit-claude.mjs +2 -4
- package/js/build/generate.mjs +6 -3
- package/js/build/render.mjs +5 -9
- package/js/build/source.mjs +3 -11
- package/js/cli-table.json +27 -1
- package/js/hosts/hooks-context.mjs +431 -0
- package/js/hosts/hooks-learn.mjs +275 -0
- package/js/hosts/hooks-prims.mjs +108 -0
- package/js/hosts/hooks.mjs +50 -977
- package/js/hosts/hosts.mjs +12 -4
- package/js/hosts/installs.mjs +5 -6
- package/js/hosts/mcp.mjs +13 -49
- package/js/hosts/memory-files.mjs +181 -0
- package/js/hosts/native.mjs +43 -5
- package/js/hosts/settings.mjs +135 -414
- package/js/hosts/shim.mjs +373 -0
- package/js/inspect/checks-authoring.mjs +21 -828
- package/js/inspect/checks-build.mjs +32 -4
- package/js/inspect/checks-constitution.mjs +493 -0
- package/js/inspect/checks-counts.mjs +242 -0
- package/js/inspect/checks-repo.mjs +17 -8
- package/js/inspect/diff.mjs +4 -1
- package/js/inspect/doctor.mjs +2 -2
- package/js/inspect/inventory.mjs +4 -4
- package/js/inspect/scan.mjs +1 -1
- package/js/inspect/status.mjs +1 -1
- package/js/inspect/validate.mjs +48 -35
- package/js/loop/catalog.mjs +17 -4
- package/js/loop/cli.mjs +52 -16
- package/js/loop/graph.mjs +19 -1
- package/js/loop/registry.mjs +191 -0
- package/js/loop/score.mjs +46 -7
- package/js/loop/state.mjs +157 -19
- package/js/maintain/memory.mjs +1 -1
- package/js/maintain/migrate.mjs +18 -30
- package/js/maintain/setup.mjs +1 -1
- package/js/maintain/uninstall.mjs +47 -88
- package/js/ui/cli.mjs +12 -12
- package/js/web/actions.mjs +39 -4
- package/js/web/api.mjs +34 -21
- package/js/web/docs.mjs +87 -78
- package/js/web/routes.mjs +5 -1
- package/package.json +1 -1
- package/src/agents/explorer.md +2 -2
- package/src/agents/researcher.md +3 -3
- package/src/bricks/README.md +3 -1
- package/src/bricks/adr-challenge.md +20 -0
- package/src/bricks/adr-draft.md +15 -0
- package/src/bricks/characterize.md +17 -0
- package/src/bricks/ci-fix.md +2 -1
- package/src/bricks/ci-triage.md +20 -0
- package/src/bricks/compat-check.md +16 -0
- package/src/bricks/done-check.md +16 -0
- package/src/bricks/fitness-define.md +22 -0
- package/src/bricks/flake-check.md +17 -0
- package/src/bricks/flake-repro.md +18 -0
- package/src/bricks/identify.md +6 -3
- package/src/bricks/migration-plan.md +21 -0
- package/src/bricks/mutation-check.md +18 -0
- package/src/bricks/plan.md +6 -6
- package/src/bricks/red-test.md +16 -0
- package/src/bricks/review.md +5 -3
- package/src/bricks/spec.md +18 -0
- package/src/bricks/upgrade-scout.md +16 -0
- package/src/loops/api-endpoint.json +31 -0
- package/src/loops/architecture-decision.json +22 -0
- package/src/loops/bugfix.json +1 -0
- package/src/loops/ci-repair.json +19 -0
- package/src/loops/deps-upgrade.json +29 -0
- package/src/loops/enforce-architecture-rule.json +28 -0
- package/src/loops/feature.json +8 -5
- package/src/loops/fix-flaky-tests.json +30 -0
- package/src/loops/legacy-refactor.json +26 -0
- package/src/loops/legacy-tests.json +29 -0
- package/src/loops/refactor.json +5 -1
- package/src/loops/remove-dead-code.json +30 -0
- package/src/loops/spec-first-feature.json +29 -0
- package/src/loops/tdd.json +25 -0
- package/src/loops/update-contract.json +33 -0
- package/src/skills/brick-forge.md +3 -1
- package/src/skills/bruno-collection-generator.md +3 -3
- package/src/skills/bruno-test-writer.md +3 -3
- package/src/skills/ci-fix.md +3 -3
- package/src/skills/consolidate-memory.md +4 -4
- package/src/skills/council.md +1 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/git-archaeology.md +1 -1
- package/src/skills/git-rescue.md +4 -3
- package/src/skills/handoff.md +1 -1
- package/src/skills/herdr.md +1 -1
- package/src/skills/learn-mode.md +2 -2
- package/src/skills/loop.md +22 -10
- package/src/skills/opencode-theme.md +3 -3
- package/src/skills/ponytail.md +1 -1
- package/src/skills/profile.md +3 -4
- package/src/skills/prose.md +2 -3
- package/src/skills/quiz.md +1 -1
- package/src/skills/react-view-transitions/SKILL.md +1 -1
- package/src/skills/react-view-transitions/VENDOR.md +8 -3
- package/src/skills/research.md +3 -3
- package/src/skills/review-response.md +2 -2
- package/src/skills/rule.md +3 -3
- package/src/skills/security-audit.md +4 -4
- package/src/skills/teach.md +1 -1
- package/src/skills/wiki.md +3 -3
- package/src/skills/workflow.md +4 -4
- package/src/skills/worktree.md +2 -2
- package/web/dist/assets/Activity-CjRqZnr-.js +1 -0
- package/web/dist/assets/ActivityDetail-OQTxKBj2.js +1 -0
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +2 -0
- package/web/dist/assets/Docs-CvOZ8akL.js +5 -0
- package/web/dist/assets/{GreenhouseView-C6ibc8IE.js → GreenhouseView-6KDjX8da.js} +1 -1
- package/web/dist/assets/{Installs-DRYjYnxs.js → Installs-MWuhNjSm.js} +2 -2
- package/web/dist/assets/JournalView-C8MrOz2c.js +1 -0
- package/web/dist/assets/Laws-Du0-Zyp2.js +3 -0
- package/web/dist/assets/Library-DOkO0TtB.js +1 -0
- package/web/dist/assets/Loops-Dv_aLByJ.js +2 -0
- package/web/dist/assets/{Markdown-qjM2_ofS.js → Markdown-X2a3qvOh.js} +14 -14
- package/web/dist/assets/{OperatorHudView-Bs7paWmh.js → OperatorHudView-BPP1dtLz.js} +1 -1
- package/web/dist/assets/{Personal-BY5n4gTw.js → Personal-26VPYkK0.js} +1 -1
- package/web/dist/assets/Tabs-BzKuFskD.js +1 -0
- package/web/dist/assets/{usePoll-O0sJEw1Y.js → activity-Dr7ZMBI3.js} +1 -1
- package/web/dist/assets/{api-CyMRbU2x.js → api-D_ixdyEY.js} +1 -1
- package/web/dist/assets/drawable-zVVDbsyw.js +1 -0
- package/web/dist/assets/index-Ctp-4poP.css +1 -0
- package/web/dist/assets/index-KHt3wE_K.js +2 -0
- package/web/dist/assets/usePoll-DD8VjtSg.js +1 -0
- package/web/dist/index.html +3 -3
- package/web/src/pages/Laws.jsx +0 -1
- package/web/dist/assets/Activity-GaaZWXTY.js +0 -1
- package/web/dist/assets/ActivityDetail-4t7d45Du.js +0 -1
- package/web/dist/assets/Dashboard-CYn7Tp16.js +0 -2
- package/web/dist/assets/Docs-BeFKLg2T.js +0 -5
- package/web/dist/assets/JournalView-1HbU1mfg.js +0 -1
- package/web/dist/assets/Laws-DFoG8JWB.js +0 -3
- package/web/dist/assets/Library-DDjgNwz_.js +0 -1
- package/web/dist/assets/Loops-D9VEoO0F.js +0 -1
- package/web/dist/assets/Tabs-ho-EYD8i.js +0 -1
- package/web/dist/assets/drawable-CLS9drMb.js +0 -1
- package/web/dist/assets/index-CX_hXMi5.css +0 -1
- package/web/dist/assets/index-Cjr7XCA3.js +0 -2
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The COUNT gates — the hand-written tables, badges and prose that restate how many laws,
|
|
3
|
+
* agents, skills, themes and plugins ship, held against `src/`.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `checks-authoring.mjs`, which still aggregates these (and re-exports them for
|
|
6
|
+
* its existing importers). `proseMirrorProblems` is pure over its inputs; `countTableProblems`
|
|
7
|
+
* is the reader that feeds it.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { DOCS, DOC_FOLDERS, PLUGIN_SRC, ROOT, SRC, ruleCanon } from '../build/source.mjs';
|
|
11
|
+
import { themeFiles } from '../hosts/installs.mjs';
|
|
12
|
+
import { readText } from '../lib/fs.mjs';
|
|
13
|
+
import { formatRepr } from '../lib/json.mjs';
|
|
14
|
+
import { lawMetaProblems } from './checks-constitution.mjs';
|
|
15
|
+
import { LAW_CLASS, LAW_CLASSES, SKILL_CLASS } from './inventory.mjs';
|
|
16
|
+
import { globSorted, has, isDir, isFile, srcStems } from './scan.mjs';
|
|
17
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `_harness_build._prose_mirror_problems` — the human-readable count mirrors, the ones the
|
|
21
|
+
* badge regex never sees.
|
|
22
|
+
*
|
|
23
|
+
* The README "What you get" table and the web onboarding copy each restate the law / agent /
|
|
24
|
+
* skill counts in prose, and the README enumerates the skills by name. Nothing renders these,
|
|
25
|
+
* so they drift silently — they already had. Pure over its inputs, so the corpus can feed it
|
|
26
|
+
* crafted drift without touching the tree.
|
|
27
|
+
*/
|
|
28
|
+
export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '') {
|
|
29
|
+
const problems = [];
|
|
30
|
+
const { laws, agents, skills, plugins } = counts;
|
|
31
|
+
|
|
32
|
+
for (const m of readme.matchAll(/(\d+) universal laws/g)) {
|
|
33
|
+
if (Number(m[1]) !== laws) {
|
|
34
|
+
problems.push(`[authoring] README prose says '${m[1]} universal laws' but src has ${laws}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// The README plugin count. It had drifted — the overview sentence said six while seven
|
|
38
|
+
// shipped — because the project's other plugin count ({N_PLUGINS} in the web docs) reads
|
|
39
|
+
// a DEPLOYED plugins dir that a markdown file in this repository never has. `plugins` is
|
|
40
|
+
// optional in `counts` (Python's `.get`, undefined here) because the corpus predates it.
|
|
41
|
+
if (plugins !== undefined) {
|
|
42
|
+
for (const m of readme.matchAll(/(\d+) \*{0,2}plugins/g)) {
|
|
43
|
+
if (Number(m[1]) !== plugins) {
|
|
44
|
+
problems.push(`[authoring] README prose says '${m[1]} plugins' but `
|
|
45
|
+
+ `adapters/opencode/plugins has ${plugins}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const [label, want] of [['Agents', agents], ['Skills', skills]]) {
|
|
50
|
+
const m = new RegExp(`${label}\\*\\*\\s*\\((\\d+)\\)`).exec(readme);
|
|
51
|
+
if (m && Number(m[1]) !== want) {
|
|
52
|
+
problems.push(`[authoring] README '${label} (${m[1]})' but src has ${want}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// The README Skills row must enumerate EXACTLY the source skills — a dropped name is
|
|
56
|
+
// invisible to the (N) count alone, which is the off-by-one this gate forbids.
|
|
57
|
+
const row = readme.split('\n').find((ln) => ln.includes('🛠') && ln.includes('Skills')) ?? '';
|
|
58
|
+
if (row.includes('workflows:')) {
|
|
59
|
+
const listed = new Set(row.split('workflows:').slice(1).join('workflows:').split('·')
|
|
60
|
+
.map((seg) => seg.replace(/[^a-z0-9-]/g, '')));
|
|
61
|
+
listed.delete('');
|
|
62
|
+
for (const missing of [...skillStems].filter((s) => !listed.has(s)).sort()) {
|
|
63
|
+
problems.push(`[authoring] README skills list omits '${missing}'`);
|
|
64
|
+
}
|
|
65
|
+
for (const orphan of [...listed].filter((s) => !skillStems.has(s)).sort()) {
|
|
66
|
+
problems.push(`[authoring] README skills list names '${orphan}' but no `
|
|
67
|
+
+ `skills/${orphan}.md exists`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const m of web.matchAll(/(\d+) universal (?:laws|Rules)/g)) {
|
|
72
|
+
if (Number(m[1]) !== laws) {
|
|
73
|
+
problems.push(`[authoring] docs/ prose says '${m[1]} universal laws/Rules' but `
|
|
74
|
+
+ `src has ${laws}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const m of web.matchAll(/(\d+) capability specialists/g)) {
|
|
78
|
+
if (Number(m[1]) !== agents) {
|
|
79
|
+
problems.push(`[authoring] docs/ prose says '${m[1]} capability specialists' but `
|
|
80
|
+
+ `src has ${agents}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// N is a curated-subset size, not the total, so it is gated against the wikilinks it
|
|
84
|
+
// introduces rather than against the skill count. `[\s\S]` is Python's `re.S` dot.
|
|
85
|
+
const m = /(\d+) repeatable workflows the agent can invoke by name([\s\S]*?)playbook under/
|
|
86
|
+
.exec(web);
|
|
87
|
+
if (m) {
|
|
88
|
+
const listedN = (m[2].match(/\[\[[^\]]+\]\]/g) ?? []).length;
|
|
89
|
+
if (Number(m[1]) !== listedN) {
|
|
90
|
+
problems.push(`[authoring] docs/ says '${m[1]} repeatable workflows' `
|
|
91
|
+
+ `but its wikilink list has ${listedN}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ⚠ THE ABSENCE OF THIS TRIPLE IS CHECKED BY THE CALLER, NOT HERE. This arm is satisfiable by
|
|
96
|
+
// DELETING THE SENTENCE — no match, no problem, green — which is a gate failing open. The
|
|
97
|
+
// presence check lives in `countTableProblems`, beside the read that already knows whether
|
|
98
|
+
// the file could be opened at all; an empty `shipped` here means "nothing to compare".
|
|
99
|
+
const s = /(\d+) laws, (\d+) agents, (\d+) skills/.exec(shipped);
|
|
100
|
+
if (s) {
|
|
101
|
+
for (const [got, want, label] of [[s[1], laws, 'laws'], [s[2], agents, 'agents'],
|
|
102
|
+
[s[3], skills, 'skills']]) {
|
|
103
|
+
if (Number(got) !== want) {
|
|
104
|
+
problems.push(`[authoring] SHIPPED.md says '${got} ${label}' but src has ${want}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return problems;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `_build_render._TMPL_SPEC_RE`, as the count gate spells it. */
|
|
112
|
+
const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `_harness_build._count_table_problems` — the hand-authored AGENT.md tables and the README
|
|
116
|
+
* badges, against `src/`.
|
|
117
|
+
*
|
|
118
|
+
* The tables must list EXACTLY the spec files (no dead row, no orphaned spec) and each
|
|
119
|
+
* `agents`/`skills`/`laws`/`themes` badge must equal the real count. This is the
|
|
120
|
+
* authoring-time guarantee that lets tables, badges and prose stay hand-written without
|
|
121
|
+
* silently drifting from the source tree.
|
|
122
|
+
*/
|
|
123
|
+
export function countTableProblems() {
|
|
124
|
+
const problems = [];
|
|
125
|
+
let ttext;
|
|
126
|
+
try { ttext = readText(path.join(SRC, 'AGENT.md.tmpl')); } catch (e) {
|
|
127
|
+
return [`[authoring] AGENT.md.tmpl unreadable: ${e.message}`];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const linked = { agents: new Set(), skills: new Set() };
|
|
131
|
+
for (const m of ttext.matchAll(TMPL_SPEC_RE)) {
|
|
132
|
+
if (m[2] !== '_template') linked[m[1] === 'AGENTS' ? 'agents' : 'skills'].add(m[2]);
|
|
133
|
+
}
|
|
134
|
+
for (const folder of ['agents', 'skills']) {
|
|
135
|
+
const files = srcStems(folder);
|
|
136
|
+
for (const missing of [...linked[folder]].filter((x) => !files.has(x)).sort()) {
|
|
137
|
+
problems.push(`[authoring] AGENT.md links ${folder}/${missing}.md but no such spec exists`);
|
|
138
|
+
}
|
|
139
|
+
for (const orphan of [...files].filter((x) => !linked[folder].has(x)).sort()) {
|
|
140
|
+
problems.push(`[authoring] ${folder}/${orphan}.md exists but the AGENT.md table omits it`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const skillFiles = srcStems('skills');
|
|
145
|
+
for (const missing of [...skillFiles].filter((s) => !has(SKILL_CLASS, s)).sort()) {
|
|
146
|
+
problems.push(`[authoring] skills/${missing}.md has no category in SKILL_CLASS`);
|
|
147
|
+
}
|
|
148
|
+
for (const stale of Object.keys(SKILL_CLASS).filter((s) => !skillFiles.has(s)).sort()) {
|
|
149
|
+
problems.push(`[authoring] SKILL_CLASS lists '${stale}' but no skills/${stale}.md exists`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The numerals the laws RENDER with — positional, off the canon — because LAW_CLASS and the
|
|
153
|
+
// console's LAW_META are keyed the way the rendered catalogue numbers them.
|
|
154
|
+
const lawRules = ruleCanon(SRC).rules.filter((r) => r.kind === 'LAW');
|
|
155
|
+
const lawNums = lawRules.map((r) => r.label);
|
|
156
|
+
for (const num of lawNums) {
|
|
157
|
+
if (!has(LAW_CLASS, num)) {
|
|
158
|
+
problems.push(`[authoring] laws/universal.md rule ${num} has no class in LAW_CLASS`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Sorted by the Roman numeral as a STRING; `doctorCollect` re-sorts every message anyway.
|
|
162
|
+
for (const num of Object.keys(LAW_CLASS).sort()) {
|
|
163
|
+
if (!LAW_CLASSES.includes(LAW_CLASS[num])) {
|
|
164
|
+
problems.push(`[authoring] LAW_CLASS['${num}'] = '${LAW_CLASS[num]}' is not a known `
|
|
165
|
+
+ `class ${formatRepr(LAW_CLASSES)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
problems.push(...lawMetaProblems(lawRules, LAW_CLASS, LAW_CLASSES));
|
|
169
|
+
|
|
170
|
+
// WHAT SHIPS, NOT WHAT IS FLAT. A folder skill (`src/skills/<name>/SKILL.md`, vendored with
|
|
171
|
+
// its scripts or references) ships exactly like a flat one, so the badge, the README list and
|
|
172
|
+
// the SHIPPED.md triple count it. It is added HERE rather than in `srcStems`, whose other
|
|
173
|
+
// readers above — the AGENT.md table links and SKILL_CLASS — are about flat specs only.
|
|
174
|
+
const skillsDir = path.join(SRC, 'skills');
|
|
175
|
+
const shippedSkills = new Set([...skillFiles, ...(isDir(skillsDir) ? readdirSync(skillsDir) : [])
|
|
176
|
+
.filter((d) => !d.startsWith('_') && isFile(path.join(skillsDir, d, 'SKILL.md')))]);
|
|
177
|
+
|
|
178
|
+
const counts = {
|
|
179
|
+
agents: srcStems('agents').size,
|
|
180
|
+
skills: shippedSkills.size,
|
|
181
|
+
laws: lawNums.length,
|
|
182
|
+
themes: themeFiles().length,
|
|
183
|
+
plugins: existsSync(PLUGIN_SRC)
|
|
184
|
+
? readdirSync(PLUGIN_SRC).filter((f) => f.startsWith('geneseed-') && f.endsWith('.js')).length
|
|
185
|
+
: 0,
|
|
186
|
+
};
|
|
187
|
+
let readme;
|
|
188
|
+
try { readme = readText(path.join(ROOT, 'README.md')); } catch { return problems; }
|
|
189
|
+
// ⚠ PRESENCE FIRST, THEN THE VALUE — the same absence arm the SHIPPED triple needed, for the
|
|
190
|
+
// same reason: `if (m && …)` is green when there is no badge, so deleting a badge silences its
|
|
191
|
+
// own gate.
|
|
192
|
+
for (const [key, n] of Object.entries(counts)) {
|
|
193
|
+
const m = new RegExp(`badge/${key}-(\\d+)`).exec(readme);
|
|
194
|
+
if (!m) {
|
|
195
|
+
problems.push(`[authoring] README has no ${key} badge — deleting one is how this gate `
|
|
196
|
+
+ 'goes green while the reader is told nothing');
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (Number(m[1]) !== n) {
|
|
200
|
+
problems.push(`[authoring] README ${key} badge says ${m[1]} but src has ${n}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// WHERE THE ONBOARDING COPY LIVES, AND WHY THIS READ MOVED. It used to open the one module
|
|
205
|
+
// that held the web console's onboarding prose. That prose has since moved into
|
|
206
|
+
// `docs/<folder>/*.md`, and the counts in it render from `{N_LAWS}` / `{N_AGENTS}` / `{N_SKILLS}`
|
|
207
|
+
// — so the read was still succeeding against a file the sentences had left, all three arms
|
|
208
|
+
// below scored zero, and the check looked healthy while gating nothing. A templated count
|
|
209
|
+
// cannot drift; what these arms still catch is a maintainer typing the NUMBER into a page
|
|
210
|
+
// instead of the token, which is the drift that reaches a reader.
|
|
211
|
+
//
|
|
212
|
+
// `web` stays fail-soft — a missing docs tree is not an authoring fault.
|
|
213
|
+
let web = '';
|
|
214
|
+
try {
|
|
215
|
+
web = DOC_FOLDERS
|
|
216
|
+
.flatMap((f) => (isDir(path.join(DOCS, f))
|
|
217
|
+
? globSorted(path.join(DOCS, f), (n) => n.endsWith('.md')) : []))
|
|
218
|
+
.map(readText).join('\n');
|
|
219
|
+
} catch { web = ''; }
|
|
220
|
+
// ⚠ NOT FAIL-SOFT, UNLIKE `web` ABOVE, AND THE ASYMMETRY IS THE POINT. A missing docs tree
|
|
221
|
+
// is not an authoring fault — a checkout can legitimately lack it. SHIPPED.md is a tracked,
|
|
222
|
+
// shipped file whose CONTENT this function asserts, so an unreadable one was
|
|
223
|
+
// indistinguishable from a deleted claim: swallowing it into `''` handed the triple arm an
|
|
224
|
+
// empty string and turned a hard read failure into a silent pass.
|
|
225
|
+
//
|
|
226
|
+
// AND THE PRESENCE OF THE TRIPLE IS CHECKED HERE rather than inside `proseMirrorProblems`:
|
|
227
|
+
// this is the caller that already owns the question "could the file be read".
|
|
228
|
+
let shipped;
|
|
229
|
+
try {
|
|
230
|
+
shipped = readText(path.join(ROOT, 'SHIPPED.md'));
|
|
231
|
+
} catch (e) {
|
|
232
|
+
problems.push(`[authoring] SHIPPED.md is unreadable (${e.message}) — the counts it carries `
|
|
233
|
+
+ 'cannot be checked, and a gate that cannot read its subject must say so');
|
|
234
|
+
shipped = '';
|
|
235
|
+
}
|
|
236
|
+
if (shipped && !/(\d+) laws, (\d+) agents, (\d+) skills/.test(shipped)) {
|
|
237
|
+
problems.push('[authoring] SHIPPED.md carries no \'N laws, N agents, N skills\' line — '
|
|
238
|
+
+ 'deleting it is how that gate goes green while the count drifts');
|
|
239
|
+
}
|
|
240
|
+
problems.push(...proseMirrorProblems(readme, web, counts, shippedSkills, shipped));
|
|
241
|
+
return problems;
|
|
242
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { PLUGIN_SRC, ROOT, SRC, THEMES, WORKFLOW_SRC } from '../build/source.mjs';
|
|
11
11
|
import { VENDORED_SKILL_DIRS } from '../hosts/native.mjs';
|
|
12
|
-
import { hookShimPath, shimDeadPaths } from '../hosts/
|
|
12
|
+
import { hookShimPath, shimDeadPaths } from '../hosts/shim.mjs';
|
|
13
13
|
import { readText } from '../lib/fs.mjs';
|
|
14
14
|
import { formatRepr, formatValue } from '../lib/json.mjs';
|
|
15
15
|
import { splitLines } from '../lib/udiff.mjs';
|
|
@@ -313,11 +313,20 @@ export function moduleMapProblems() {
|
|
|
313
313
|
+ 'checks-repo.mjs asserts it describes every module under js/'];
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
// A row is `| `name.mjs` | …
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
316
|
+
// A row is `| `name.mjs` | … |`, and it documents `<folder>/name.mjs` for the folder whose
|
|
317
|
+
// `## `js/<folder>/`` heading it sits under. Keyed by basename alone, a row answered for every
|
|
318
|
+
// folder holding that name — catalog.mjs, cli.mjs and registry.mjs each live in two — so a
|
|
319
|
+
// deleted module stayed "documented" by its namesake's row. Reading the CODE SPAN and not the
|
|
320
|
+
// whole line keeps a module named in a "Before editing" paragraph from counting as a row.
|
|
321
|
+
const documented = new Set();
|
|
322
|
+
let folder = null;
|
|
323
|
+
for (const line of text.split('\n')) {
|
|
324
|
+
const heading = /^## `js\/([a-z0-9-]+)\/`/.exec(line);
|
|
325
|
+
if (heading) { folder = heading[1]; continue; }
|
|
326
|
+
if (line.startsWith('## ')) { folder = null; continue; }
|
|
327
|
+
const row = /^\|\s*`([a-z0-9-]+\.mjs)`\s*\|/.exec(line);
|
|
328
|
+
if (row && folder) documented.add(`${folder}/${row[1]}`);
|
|
329
|
+
}
|
|
321
330
|
|
|
322
331
|
const onDisk = new Set();
|
|
323
332
|
const jsDir = path.join(String(ROOT), 'js');
|
|
@@ -328,7 +337,7 @@ export function moduleMapProblems() {
|
|
|
328
337
|
// that only knew `/` finds ONE part on Windows and therefore zero modules — which is what
|
|
329
338
|
// the vacuity guard below caught the first time this ran.
|
|
330
339
|
const parts = path.relative(jsDir, rel).replaceAll('\\', '/').split('/');
|
|
331
|
-
if (parts.length === 2) onDisk.add(parts
|
|
340
|
+
if (parts.length === 2) onDisk.add(parts.join('/'));
|
|
332
341
|
}
|
|
333
342
|
|
|
334
343
|
if (onDisk.size < 30) {
|
|
@@ -344,7 +353,7 @@ export function moduleMapProblems() {
|
|
|
344
353
|
}
|
|
345
354
|
for (const mod of [...documented].sort()) {
|
|
346
355
|
if (!onDisk.has(mod)) {
|
|
347
|
-
problems.push(`[authoring] js/README.md has a row for
|
|
356
|
+
problems.push(`[authoring] js/README.md has a row for js/${mod}, which does not exist — the map `
|
|
348
357
|
+ 'sends a reader to a file that is not there');
|
|
349
358
|
}
|
|
350
359
|
}
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
46
46
|
} from '../hosts/hosts.mjs';
|
|
47
47
|
import {
|
|
48
|
-
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, footprintOfDir, modeOfDir, postureOfDir,
|
|
48
|
+
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, excludedRulesOfDir, footprintOfDir, modeOfDir, postureOfDir,
|
|
49
49
|
trustOfDir,
|
|
50
50
|
readJsonMaybe, readMaybe, themeOfDir,
|
|
51
51
|
} from '../hosts/installs.mjs';
|
|
@@ -178,6 +178,9 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
178
178
|
posture: postureOfDir(dir), mode: modeOfDir(dir), doctrines: doctrinesForBuild(dir),
|
|
179
179
|
// The loop skill's preset, or its `Default trust preset:` line reads as edited.
|
|
180
180
|
trust: trustOfDir(dir),
|
|
181
|
+
// The excluded rules, or an install built `--exclude-rules "process 5"` reads its
|
|
182
|
+
// carrier as drift: `expected` would re-state the rule the deployment took away.
|
|
183
|
+
excludeRules: excludedRulesOfDir(dir),
|
|
181
184
|
}));
|
|
182
185
|
// The one place a render depends on WHERE it is installed: a global Bob emit writes each
|
|
183
186
|
// folder skill's absolute directory into its SKILL.md (see `writeNativeLayer`), so the
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import path from 'node:path';
|
|
33
33
|
import { emitGlobalInto, emitProjectInto, main as driverMain } from '../build/driver.mjs';
|
|
34
34
|
import { ROOT } from '../build/source.mjs';
|
|
35
|
-
import { resolvePath } from '../hosts/hosts.mjs';
|
|
35
|
+
import { CLAUDE_STYLE, 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';
|
|
@@ -143,7 +143,7 @@ export function globalEmitProblems(themeName) {
|
|
|
143
143
|
* skills one level deeper than a bundle does.
|
|
144
144
|
*/
|
|
145
145
|
export function claudeBobEmitProblems(themeName) {
|
|
146
|
-
return emitProblemsOver(
|
|
146
|
+
return emitProblemsOver(CLAUDE_STYLE, (tmp, label) => {
|
|
147
147
|
const root = path.join(tmp, 'root');
|
|
148
148
|
return {
|
|
149
149
|
label: `${themeName} ${label}`,
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -34,10 +34,10 @@ import { parseJson } from '../lib/json.mjs';
|
|
|
34
34
|
* `_harness_tui.LAW_HEADING_RE` — a heading in the RENDERED laws file, e.g.
|
|
35
35
|
* `### Rule IV — Deletion Is Deliberate`.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* The rendered side only: the unrendered source declares a rule as `{{LAW:<id>}} <Name>`, and
|
|
38
|
+
* `ruleCanon` in `js/build/source.mjs` is what reads that.
|
|
39
39
|
*/
|
|
40
|
-
|
|
40
|
+
const LAW_HEADING_RE = /^###\s+\S+\s+([IVXLCDM]+)\s+[—-]\s+(.+?)\s*$/;
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* A heading in a RENDERED doctrine pack, e.g. `### Doctrine craft 1 — Automate Repetition`.
|
|
@@ -52,7 +52,7 @@ export const LAW_HEADING_RE = /^###\s+\S+\s+([IVXLCDM]+)\s+[—-]\s+(.+?)\s*$/;
|
|
|
52
52
|
* `([a-z]+)` refuses `Rule I`'s uppercase numeral, and `([IVXLCDM]+)` refuses a lowercase pack
|
|
53
53
|
* id. Measured across all fourteen themes — zero cross-matches either way.
|
|
54
54
|
*/
|
|
55
|
-
|
|
55
|
+
const DOCTRINE_HEADING_RE = /^###\s+\S+\s+([a-z]+)\s+(\d+)\s+[—-]\s+(.+?)\s*$/;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* A section heading in the RENDERED ontology, e.g. `#### Telos`.
|
package/js/inspect/scan.mjs
CHANGED
|
@@ -36,7 +36,7 @@ const FENCE_RE = /```[\s\S]*?```/g;
|
|
|
36
36
|
const INLINE_CODE_RE = /`[^`]*`/g;
|
|
37
37
|
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
38
38
|
|
|
39
|
-
/** `_harness_core.strip_code` — fenced blocks,
|
|
39
|
+
/** `_harness_core.strip_code` — fenced blocks, then HTML comments, then inline code, in that order. */
|
|
40
40
|
export function stripCode(text) {
|
|
41
41
|
return text.replace(FENCE_RE, '').replace(COMMENT_RE, '').replace(INLINE_CODE_RE, '');
|
|
42
42
|
}
|
package/js/inspect/status.mjs
CHANGED
|
@@ -73,7 +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/
|
|
76
|
+
import { shimDead } from '../hosts/shim.mjs';
|
|
77
77
|
import { printOut } from '../lib/fs.mjs';
|
|
78
78
|
import { codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
79
79
|
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -9,48 +9,36 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { buildInto, emitGlobalInto, emitProjectInto } from '../build/driver.mjs';
|
|
11
11
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
12
|
+
import {
|
|
13
|
+
doctrinesOfDir, excludedRulesOfDir, modeOfDir, postureOfDir,
|
|
14
|
+
} from '../hosts/installs.mjs';
|
|
12
15
|
import { validateIsVendored } from '../hosts/native.mjs';
|
|
13
|
-
import { printOut, printErr
|
|
16
|
+
import { printOut, printErr } from '../lib/fs.mjs';
|
|
14
17
|
import { comparePaths } from '../lib/paths.mjs';
|
|
15
18
|
import { stripWhitespace } from '../lib/text.mjs';
|
|
16
|
-
import {
|
|
19
|
+
import { checkBuild } from './checks-build.mjs';
|
|
17
20
|
import { cmdDoctor } from './doctor.mjs';
|
|
18
|
-
import {
|
|
21
|
+
import { isDir, isFile, rglob, withTempDir, within } from './scan.mjs';
|
|
19
22
|
|
|
20
23
|
// --------------------------------------------------------------------------------------
|
|
21
24
|
// validate — the generator's `--validate-only`, on this binary because it runs the doctor
|
|
22
25
|
// --------------------------------------------------------------------------------------
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* A NEAR-TWIN OF `checkBuild` ABOVE, AND DELIBERATELY NOT IT, because the reference is two
|
|
29
|
-
* functions and they differ in three observable ways: the token list is `sorted(set(...))`
|
|
30
|
-
* here and bare `set(...)` there, the message carries no `[theme]` prefix (the caller adds
|
|
31
|
-
* `[emit]` instead), and an unreadable file is SKIPPED rather than raised. Python duplicates
|
|
32
|
-
* the loop because `build.py` cannot import the harness tree; here the two live in one file
|
|
33
|
-
* and the two now sit one file apart (`checkBuild` is in `checks-build.mjs`); the
|
|
34
|
-
* duplication is eight lines — every primitive underneath (`rglob`, `stripCode`,
|
|
35
|
-
* `linkProblems`, `readText`) is the shared one, which is the half that could actually drift.
|
|
28
|
+
* `validate --help`. Its own text, because the parser it borrows is the generator's and that
|
|
29
|
+
* parser's `-h` prints `usage: geneseed-build …` — naming `--sync-themes`/`--config-dir`, which
|
|
30
|
+
* this verb refuses, under a program name the user did not type.
|
|
36
31
|
*/
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const tok of sortedUnique(text.match(TOKEN_RE) ?? [])) {
|
|
48
|
-
problems.push(`unresolved token ${tok} in ${rel}`);
|
|
49
|
-
}
|
|
50
|
-
problems.push(...linkProblems(md, text, out, rel));
|
|
51
|
-
}
|
|
52
|
-
return problems;
|
|
53
|
-
}
|
|
32
|
+
export const VALIDATE_USAGE = [
|
|
33
|
+
'usage: geneseed validate [-h] [--theme THEME] [--emit EMIT] [--out OUT] [--root ROOT]',
|
|
34
|
+
' [--footprint {lean,full}] [--posture POSTURE] [--mode MODE]',
|
|
35
|
+
' [--trust TRUST] [--doctrines DOCTRINES] [--exclude-rules RULES] [-v]',
|
|
36
|
+
'',
|
|
37
|
+
"Render and emit into a throwaway sandbox, run the doctor's checks, and write nothing real.",
|
|
38
|
+
"Takes the generator's render flags (see geneseed-build --help) except --sync-themes and",
|
|
39
|
+
'--config-dir.',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
54
42
|
|
|
55
43
|
/**
|
|
56
44
|
* `subprocess.run(..., capture_output=True, text=True)` around an IN-PROCESS call.
|
|
@@ -109,6 +97,17 @@ function captureStreams(fn) {
|
|
|
109
97
|
* contract absolutely — this verb is deliberately outside the verb table, so nothing else does.
|
|
110
98
|
*/
|
|
111
99
|
export function cmdValidate(args) {
|
|
100
|
+
// Two generator flags with no meaning in a dry run, refused rather than silently ignored:
|
|
101
|
+
// `--sync-themes` WRITES into the live themes dir, and `--config-dir` names a real install dir
|
|
102
|
+
// while every render here goes to a sandbox — accepting either would claim a check of
|
|
103
|
+
// something this verb never touches.
|
|
104
|
+
for (const [flag, set] of [['--sync-themes', args.syncThemes], ['--config-dir', args.cfgDir]]) {
|
|
105
|
+
if (set) {
|
|
106
|
+
printErr(`geneseed: error: validate does not take ${flag} — it renders into a sandbox `
|
|
107
|
+
+ 'and writes nothing real; run `geneseed validate --help` for what it takes\n');
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
112
111
|
const problems = [];
|
|
113
112
|
const scan = withTempDir((tmpRaw) => {
|
|
114
113
|
// `.resolve()` mirrors `_check_build`'s: on Windows a temp dir can come back in 8.3
|
|
@@ -122,7 +121,13 @@ export function cmdValidate(args) {
|
|
|
122
121
|
const sandbox = args.root ? path.join(root, 'bundle') : root;
|
|
123
122
|
const cfgDir = path.join(tmp, 'cfg');
|
|
124
123
|
const emit = args.emit;
|
|
125
|
-
|
|
124
|
+
// THE FIVE RENDER AXES TRAVEL WITH THE DRY RUN. They were parsed and then dropped, so
|
|
125
|
+
// `validate --doctrines craft` checked an all-packs render — a pass that said nothing
|
|
126
|
+
// about the build the operator was about to run.
|
|
127
|
+
const opts = {
|
|
128
|
+
theme: args.theme, footprint: args.footprint, posture: args.posture, mode: args.mode,
|
|
129
|
+
trust: args.trust, doctrines: args.doctrines, excludeRules: args.excludeRules,
|
|
130
|
+
};
|
|
126
131
|
let scanDirs;
|
|
127
132
|
try {
|
|
128
133
|
if (emit.endsWith('-global')) {
|
|
@@ -158,6 +163,14 @@ export function cmdValidate(args) {
|
|
|
158
163
|
.sort(comparePaths);
|
|
159
164
|
printOut(`[validate-only] theme=${args.theme} emit=${emit} `
|
|
160
165
|
+ `footprint=${args.footprint}\n`);
|
|
166
|
+
// READ BACK OFF THE RENDER, not echoed from `args`: this line is the proof the axes reached
|
|
167
|
+
// the sandbox, which an echo of the flags would print just as happily if they had not.
|
|
168
|
+
const at = scanDirs[0];
|
|
169
|
+
const packs = doctrinesOfDir(at);
|
|
170
|
+
const excluded = excludedRulesOfDir(at);
|
|
171
|
+
printOut(`[validate-only] rendered at posture=${postureOfDir(at) ?? '?'} `
|
|
172
|
+
+ `mode=${modeOfDir(at) ?? '?'} packs=${packs ? packs.join(',') || 'none' : '?'} `
|
|
173
|
+
+ `excluded=${excluded.map((id) => id.replace('.', ' ')).join(', ') || 'none'}\n`);
|
|
161
174
|
printOut(`[validate-only] would write ${written.length} file(s) under ${args.out}`
|
|
162
175
|
+ (args.root ? ` (root ${args.root})` : '')
|
|
163
176
|
+ ' — nothing was actually written (sandboxed).\n');
|
|
@@ -170,9 +183,9 @@ export function cmdValidate(args) {
|
|
|
170
183
|
printOut(` would write: ${path.relative(base, p)}\n`);
|
|
171
184
|
}
|
|
172
185
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
186
|
+
// The doctor's own per-tree scan, prefixed with the emit instead of a theme, and with the
|
|
187
|
+
// vendored shape every host depth can produce — a sandbox may hold a per-repo native layer.
|
|
188
|
+
for (const d of scanDirs) problems.push(...checkBuild(emit, d, validateIsVendored));
|
|
176
189
|
return scanDirs;
|
|
177
190
|
}, 'geneseed-validate-');
|
|
178
191
|
if (scan === null) return 1;
|
package/js/loop/catalog.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { readdirSync } from 'node:fs';
|
|
|
19
19
|
import os from 'node:os';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { readText, isFile, isDir } from '../lib/fs.mjs';
|
|
22
|
-
import { frontmatter } from '../hosts/
|
|
22
|
+
import { frontmatter } from '../hosts/memory-files.mjs';
|
|
23
23
|
import { SRC } from '../build/source.mjs';
|
|
24
24
|
import { checkGraph } from './graph.mjs';
|
|
25
25
|
|
|
@@ -47,8 +47,19 @@ export function parseBrick(text, origin) {
|
|
|
47
47
|
if (!agent === !skill) problems.push(`${name}: exactly one of agent or skill`);
|
|
48
48
|
const outcomes = get('outcomes').split(',').map((s) => s.trim()).filter(Boolean);
|
|
49
49
|
if (!outcomes.length) problems.push(`${name}: outcomes is empty`);
|
|
50
|
+
// `gate: human` holds the transition after this brick until the user answers (state.mjs).
|
|
51
|
+
// Present on the brick only when declared, so an ungated brick's shape is unchanged.
|
|
52
|
+
const gate = get('gate');
|
|
53
|
+
if (gate && gate !== 'human') problems.push(`${name}: gate must be human, not ${JSON.stringify(gate)}`);
|
|
54
|
+
// `gateOn` narrows the gate to the outcomes it lists; any other outcome follows its edge ungated.
|
|
55
|
+
const gateOn = get('gateOn').split(',').map((s) => s.trim()).filter(Boolean);
|
|
56
|
+
if (gateOn.length && gate !== 'human') problems.push(`${name}: gateOn needs gate: human`);
|
|
57
|
+
for (const o of gateOn) if (!outcomes.includes(o)) problems.push(`${name}: gateOn ${o} is not one of its outcomes`);
|
|
50
58
|
return {
|
|
51
|
-
brick: {
|
|
59
|
+
brick: {
|
|
60
|
+
name, description, effect, agent, skill, outcomes, ...(gate ? { gate } : {}),
|
|
61
|
+
...(gateOn.length ? { gateOn } : {}), body: body.trim(), origin,
|
|
62
|
+
},
|
|
52
63
|
problems,
|
|
53
64
|
};
|
|
54
65
|
}
|
|
@@ -106,8 +117,10 @@ export function loadCatalog({
|
|
|
106
117
|
return { bricks, templates, problems, overridden: overridden.sort() };
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
/** `catalog`: an already-loaded `loadCatalog(opts)`, for a caller that needs the catalogue
|
|
121
|
+
* itself too — passing it saves reading every brick and template twice. */
|
|
122
|
+
export function catalogProblems(opts = {}, catalog = loadCatalog(opts)) {
|
|
123
|
+
const { bricks, templates, problems } = catalog;
|
|
111
124
|
const out = [...problems];
|
|
112
125
|
for (const [name, graph] of [...templates].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
113
126
|
out.push(...checkGraph(graph, bricks).map((p) => `loops/${name}: ${p}`));
|