geneseed 3.1.3 → 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 +180 -0
- package/README.md +24 -14
- 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 +143 -89
- package/bin/geneseed-cli.mjs +63 -106
- package/bin/geneseed-hook.mjs +26 -16
- 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 -15
- package/js/build/emit-claude.mjs +26 -12
- package/js/build/emit-common.mjs +63 -114
- package/js/build/emit-opencode.mjs +74 -66
- package/js/build/generate.mjs +4 -6
- package/js/build/render.mjs +50 -32
- package/js/build/source.mjs +29 -8
- package/js/build/stubs.mjs +25 -33
- package/js/build/themes.mjs +1 -1
- package/js/build/version.mjs +1 -3
- package/js/cli-table.json +84 -0
- package/js/hosts/hooks.mjs +243 -81
- package/js/hosts/hosts.mjs +57 -5
- package/js/hosts/installs.mjs +35 -28
- package/js/hosts/link.mjs +2 -2
- package/js/hosts/mcp.mjs +3 -1
- package/js/hosts/native.mjs +54 -19
- package/js/hosts/opencode.mjs +39 -28
- package/js/hosts/settings.mjs +206 -115
- package/js/inspect/checks-authoring.mjs +5 -4
- package/js/inspect/checks-build.mjs +1 -1
- package/js/inspect/diff.mjs +1 -3
- package/js/inspect/doctor.mjs +59 -44
- package/js/inspect/excludes.mjs +5 -11
- package/js/inspect/inventory.mjs +38 -56
- package/js/inspect/registry.mjs +4 -7
- package/js/inspect/scan.mjs +8 -15
- package/js/inspect/status.mjs +51 -3
- package/js/lib/fs.mjs +34 -0
- package/js/lib/json.mjs +28 -1
- package/js/lib/paths.mjs +0 -10
- package/js/lib/udiff.mjs +11 -16
- package/js/maintain/memory.mjs +2 -4
- package/js/maintain/migrate.mjs +3 -3
- package/js/maintain/uninstall.mjs +127 -88
- package/js/maintain/update.mjs +4 -4
- package/js/ui/anim.mjs +1 -1
- package/js/ui/menu.mjs +1 -1
- package/js/ui/tui.mjs +3 -20
- package/js/web/actions.mjs +230 -217
- package/js/web/activity.mjs +71 -89
- package/js/web/api.mjs +130 -226
- package/js/web/daemon.mjs +92 -110
- package/js/web/docs.mjs +59 -126
- package/js/web/handler.mjs +94 -123
- package/js/web/jobs.mjs +116 -178
- package/js/web/routes.mjs +47 -192
- package/js/web/server.mjs +47 -82
- package/package.json +1 -1
- package/registry.json +14 -0
- package/src/AGENT.md.tmpl +55 -16
- package/src/agents/explorer.md +2 -1
- package/src/doctrines/README.md +3 -3
- package/src/doctrines/process.md +4 -2
- package/src/doctrines/rigor.md +13 -0
- package/src/laws/universal.md +133 -43
- 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/clarify.md +1 -1
- 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 +2 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/herdr.md +1 -0
- package/src/skills/ingest.md +2 -1
- package/src/skills/learning-path.md +1 -0
- package/src/skills/opencode-theme.md +1 -0
- package/src/skills/parallel-agents.md +1 -1
- package/src/skills/ponytail.md +2 -2
- package/src/skills/research.md +4 -2
- package/themes/_TEMPLATE.json +5 -0
- package/themes/biker.json +5 -0
- package/themes/commentator.json +5 -0
- package/themes/cyberpunk.json +5 -0
- package/themes/gamer.json +5 -0
- package/themes/imperial.json +5 -0
- package/themes/joker.json +5 -0
- package/themes/marvin.json +5 -0
- package/themes/mean.json +5 -0
- package/themes/military.json +5 -0
- package/themes/neutral.json +5 -0
- package/themes/pirate.json +5 -0
- package/themes/sports.json +5 -0
- package/themes/verstappen.json +5 -0
- package/themes/wizard.json +5 -0
- package/web/dist/assets/Activity-BfVcLrda.js +1 -0
- package/web/dist/assets/ActivityDetail-CQXccGPl.js +1 -0
- package/web/dist/assets/CatalogRow-DALEQImZ.js +1 -0
- package/web/dist/assets/Diff-BIjxXx46.js +5 -0
- package/web/dist/assets/Doctor-CJLbgsrw.js +1 -0
- package/web/dist/assets/FilterInput-CkUCk_lY.js +1 -0
- package/web/dist/assets/GreenhouseView-DfC5IILJ.js +2 -0
- package/web/dist/assets/Harness-Dm5CCU-v.js +1 -0
- 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/{marked.esm-JKvyS0Pp.js → Markdown-C__nuWX5.js} +15 -15
- package/web/dist/assets/OperatorHudView-DEpCtAjP.js +2 -0
- package/web/dist/assets/Profile-BlXtUlh2.js +1 -0
- package/web/dist/assets/{Rules-CfsV1EVy.js → Rules-DIre6gxR.js} +1 -1
- package/web/dist/assets/Skills-rY13ZPhM.js +1 -0
- package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-wW_RS_my.js} +1 -1
- package/web/dist/assets/{activity-Cc0n2jwx.js → activity-DpYPKNrA.js} +1 -1
- package/web/dist/assets/index-BEZvPvpF.js +1 -0
- package/web/dist/assets/index-DNQE5ydy.js +10 -0
- package/web/dist/assets/index-DzdQjvM1.css +1 -0
- package/web/dist/assets/index-PmQGRS1X.js +2 -0
- 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 +119 -130
- package/js/web/graph.mjs +0 -0
- package/web/dist/assets/Activity-B49fE_ug.js +0 -1
- package/web/dist/assets/ActivityDetail-abt7hzD2.js +0 -1
- package/web/dist/assets/Diff-D3_bVkih.js +0 -5
- package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
- package/web/dist/assets/Graph-DvYMtlvl.js +0 -1
- package/web/dist/assets/Harness-BIWei7N5.js +0 -1
- package/web/dist/assets/Laws-CxmnRdwt.js +0 -3
- package/web/dist/assets/Library-MjbOkHIW.js +0 -1
- package/web/dist/assets/Markdown-Dx_w0faR.js +0 -1
- package/web/dist/assets/Profile-BRFvv2gr.js +0 -1
- package/web/dist/assets/Skills-DArA6IHS.js +0 -1
- package/web/dist/assets/index-Bx3oxZ1o.css +0 -1
- package/web/dist/assets/index-DC3-GlCY.js +0 -1
- package/web/dist/assets/index-DP035wPa.js +0 -12
- package/web/dist/assets/index-dHi-7M-F.js +0 -2
- package/web/dist/assets/roman-DkYN0LX9.js +0 -1
package/js/build/render.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { readText } from '../lib/fs.mjs';
|
|
|
34
34
|
import { parseJson } from '../lib/json.mjs';
|
|
35
35
|
import { normcase, comparePaths } from '../lib/paths.mjs';
|
|
36
36
|
// `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
|
|
37
|
-
import { PACK_ORDER } from './source.mjs';
|
|
37
|
+
import { PACK_ORDER, readPackText } from './source.mjs';
|
|
38
38
|
|
|
39
39
|
/** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
|
|
40
40
|
export const STRUCTURE = {
|
|
@@ -69,6 +69,16 @@ const TOKEN_RE = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g;
|
|
|
69
69
|
const INCLUDE_RE = /^[ \t]*<!--[ \t]*INCLUDE:[ \t]*(?<path>[^ \t]+)[ \t]*-->[ \t]*$/gm;
|
|
70
70
|
const CATALOG_BLOCK_RE =
|
|
71
71
|
/[ \t]*<!-- CATALOG:begin -->\n(?<table>[\s\S]*?)[ \t]*<!-- CATALOG:else -->\n(?<pointer>[\s\S]*?)[ \t]*<!-- CATALOG:end -->\n/g;
|
|
72
|
+
// The footprint twin of CATALOG. Before it existed, `lean` could only reach the two
|
|
73
|
+
// include-driven corpora (`laws/universal.md` and the doctrine packs, both truncated
|
|
74
|
+
// mechanically to heading + first sentence); every other section of the root template was
|
|
75
|
+
// byte-identical across footprints, which is how ~1.5k tokens of §-level prose rode into
|
|
76
|
+
// every lean install. A LEAN block ships a HAND-WRITTEN condensation instead of a
|
|
77
|
+
// truncation — the objection `docs/token-footprint.md` raised against terse-ing the
|
|
78
|
+
// Ontology ("four orphan sentences") is answered by authoring the short form, not
|
|
79
|
+
// generating it. Same marker grammar as CATALOG so the two stay learnable as one idea.
|
|
80
|
+
const LEAN_BLOCK_RE =
|
|
81
|
+
/[ \t]*<!-- LEAN:begin -->\n(?<full>[\s\S]*?)[ \t]*<!-- LEAN:else -->\n(?<lean>[\s\S]*?)[ \t]*<!-- LEAN:end -->\n/g;
|
|
72
82
|
|
|
73
83
|
/**
|
|
74
84
|
* `PurePath.suffix`. Not `path.extname`: Python returns '' for a name whose only dot is
|
|
@@ -81,17 +91,17 @@ function suffixOf(name) {
|
|
|
81
91
|
return 0 < i && i < name.length - 1 ? name.slice(i) : '';
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* `Path.rglob("*")` restricted to files, then `sorted()`.
|
|
96
|
+
*
|
|
97
|
+
* `recursive: true, withFileTypes: true` replaces the hand-rolled walk — the order it
|
|
98
|
+
* yields is not the walk's order, but the explicit `.sort(comparePaths)` below is what
|
|
99
|
+
* every caller actually observes, so a different collection order changes nothing.
|
|
100
|
+
*/
|
|
85
101
|
function sortedSourceFiles(src) {
|
|
86
|
-
const out =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const full = path.join(dir, name);
|
|
90
|
-
if (name === '__pycache__') continue; // `"__pycache__" in path.parts`
|
|
91
|
-
if (statSync(full).isDirectory()) walk(full); // rglob yields dirs; render_all skips them
|
|
92
|
-
else out.push(full);
|
|
93
|
-
}
|
|
94
|
-
})(src);
|
|
102
|
+
const out = readdirSync(src, { recursive: true, withFileTypes: true })
|
|
103
|
+
.filter((d) => d.isFile() && !d.parentPath.split(path.sep).includes('__pycache__'))
|
|
104
|
+
.map((d) => path.join(d.parentPath, d.name));
|
|
95
105
|
return out.sort(comparePaths); // `sorted()` — case-folded on Windows only
|
|
96
106
|
}
|
|
97
107
|
|
|
@@ -126,7 +136,7 @@ export function loadTheme(cfg, name) {
|
|
|
126
136
|
/** `_build_render.substitute`. */
|
|
127
137
|
export function substitute(text, theme) {
|
|
128
138
|
return text.replace(TOKEN_RE, (whole, key) => {
|
|
129
|
-
if (!Object.
|
|
139
|
+
if (!Object.hasOwn(theme, key)) return whole; // visible for debugging
|
|
130
140
|
const v = theme[key];
|
|
131
141
|
// Python renders `str(theme[key])`, which agrees with JS only for strings —
|
|
132
142
|
// `str(True)`/`String(true)` and `str({...})`/`String({...})` do not. Every one of
|
|
@@ -143,10 +153,15 @@ export function substitute(text, theme) {
|
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
/** `_build_render._resolve_catalogs`. */
|
|
146
|
-
|
|
156
|
+
function resolveCatalogs(text, nativeCatalog) {
|
|
147
157
|
return text.replace(CATALOG_BLOCK_RE, (_m, table, pointer) => (nativeCatalog ? pointer : table));
|
|
148
158
|
}
|
|
149
159
|
|
|
160
|
+
/** LEAN blocks — full text or the hand-written condensation, by footprint. */
|
|
161
|
+
function resolveLean(text, footprint) {
|
|
162
|
+
return text.replace(LEAN_BLOCK_RE, (_m, full, lean) => (footprint === 'lean' ? lean : full));
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
/**
|
|
151
166
|
* `re.split(r"(?m)^(?=### )", text)`.
|
|
152
167
|
*
|
|
@@ -168,15 +183,15 @@ function splitAtLawHeadings(text) {
|
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
/**
|
|
171
|
-
*
|
|
186
|
+
* Heading + first sentence, per `### ` block. DOCTRINES ONLY, now.
|
|
172
187
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
188
|
+
* The laws used to go through this too, and it made a law's lean footprint a function of its
|
|
189
|
+
* punctuation: Law II shipped as one 36-character sentence and lost its stop-and-ask
|
|
190
|
+
* mechanism, while a law written as one colon-chained run-on shipped nearly whole. The laws
|
|
191
|
+
* now carry authored LEAN blocks in `src/laws/universal.md`, the way the ontology already did,
|
|
192
|
+
* and `resolveLean` picks the half. Doctrine rules stay machine-cut: there are ~23 of them
|
|
193
|
+
* across four packs, and a pack is a practice catalogue whose first sentence is written to
|
|
194
|
+
* stand alone.
|
|
180
195
|
*/
|
|
181
196
|
function terseBlocks(text) {
|
|
182
197
|
const blocks = splitAtLawHeadings(text);
|
|
@@ -197,16 +212,14 @@ function terseBlocks(text) {
|
|
|
197
212
|
return out.join('\n\n');
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
/**
|
|
201
|
-
export function
|
|
215
|
+
/** The pointer under §1 at lean. The laws' lean text itself is authored in LEAN blocks. */
|
|
216
|
+
export function lawsPointer(theme, lawsPrefix = '') {
|
|
202
217
|
const law = theme.LAW ?? 'Law';
|
|
203
218
|
const lawsDir = theme.DIR_LAWS ?? 'laws';
|
|
204
|
-
|
|
205
|
-
`> Each ${law} above is given in brief — the rule, not its reasoning. The `
|
|
219
|
+
return `> Each ${law} above is given in brief — the rule, not its reasoning. The `
|
|
206
220
|
+ `complete, binding text of every ${law} is in \`${lawsPrefix}${lawsDir}/universal.md\`; `
|
|
207
221
|
+ `read it whenever a ${law}'s application is unclear, and before any act touching `
|
|
208
|
-
+
|
|
209
|
-
return terseBlocks(text) + '\n\n' + pointer;
|
|
222
|
+
+ 'secrets, deletion, git history, scope, or untrusted content.';
|
|
210
223
|
}
|
|
211
224
|
|
|
212
225
|
/** `_build_render.render_file`. */
|
|
@@ -225,12 +238,13 @@ export function renderFile(cfg, filePath, theme, footprint = 'full', lawsPrefix
|
|
|
225
238
|
new Set(visiting).add(normcase(here)), nativeCatalog)
|
|
226
239
|
.replace(/\n+$/, ''); // `.rstrip("\n")`
|
|
227
240
|
if (footprint === 'lean' && rel === 'laws/universal.md') {
|
|
228
|
-
inner =
|
|
241
|
+
inner = `${inner}\n\n${lawsPointer(theme, lawsPrefix)}`;
|
|
229
242
|
}
|
|
230
243
|
return inner;
|
|
231
244
|
});
|
|
232
245
|
|
|
233
246
|
text = resolveCatalogs(text, nativeCatalog);
|
|
247
|
+
text = resolveLean(text, footprint);
|
|
234
248
|
return substitute(text, theme);
|
|
235
249
|
}
|
|
236
250
|
|
|
@@ -291,7 +305,7 @@ function activeDoctrines(cfg) {
|
|
|
291
305
|
// `Active packs:` marker a later reader parses back — agrees with what was rendered.
|
|
292
306
|
const excluded = new Set(cfg.excludeRules ?? []);
|
|
293
307
|
const survives = (pack) => {
|
|
294
|
-
const ids = [...
|
|
308
|
+
const ids = [...readPackText(path.join(dir, `${pack}.md`))
|
|
295
309
|
.matchAll(/^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/gm)]
|
|
296
310
|
.map((m) => `${m[1]}.${Number(m[2])}`);
|
|
297
311
|
return ids.some((id) => !excluded.has(id));
|
|
@@ -340,7 +354,7 @@ function doctrinesBody(cfg, theme, active, footprint, lawsPrefix) {
|
|
|
340
354
|
if (!active.length) return '';
|
|
341
355
|
const excluded = new Set(cfg.excludeRules ?? []);
|
|
342
356
|
const parts = active.map((name) => {
|
|
343
|
-
const raw = dropExcludedRules(
|
|
357
|
+
const raw = dropExcludedRules(readPackText(path.join(cfg.src, 'doctrines', `${name}.md`)),
|
|
344
358
|
name, excluded);
|
|
345
359
|
const body = substitute(raw, theme).trim();
|
|
346
360
|
return footprint === 'lean' ? terseBlocks(body) : body;
|
|
@@ -397,7 +411,7 @@ export function effectiveTheme(cfg, themeName, { footprint = 'full', lawsPrefix
|
|
|
397
411
|
/** `_build_render.themed_rel` — rename the top-level folder of an output path per theme. */
|
|
398
412
|
export function themedRel(rel, theme) {
|
|
399
413
|
const parts = rel.split(path.sep);
|
|
400
|
-
if (parts.length && Object.
|
|
414
|
+
if (parts.length && Object.hasOwn(SRC_DIR_TOKENS, parts[0])) {
|
|
401
415
|
parts[0] = theme[SRC_DIR_TOKENS[parts[0]]] ?? parts[0];
|
|
402
416
|
}
|
|
403
417
|
return parts.join(path.sep);
|
|
@@ -440,8 +454,12 @@ export function renderAll(cfg, themeName, {
|
|
|
440
454
|
for (const file of sortedSourceFiles(cfg.src)) {
|
|
441
455
|
const rel = path.relative(cfg.src, file);
|
|
442
456
|
const outRel = destRel(themedRel(rel, theme)).split(path.sep).join('/'); // as_posix()
|
|
457
|
+
// The on-disk `laws/` and `ontology/` are the "complete, binding text" the lean pointer
|
|
458
|
+
// promises. They render at full whatever the footprint; only what AGENT.md INLINES leans.
|
|
459
|
+
// Keyed on the SOURCE rel, not the themed one, so a DIR_* rename cannot un-exempt them.
|
|
460
|
+
const fp = /^(?:laws|ontology)[\\/]/.test(rel) ? 'full' : footprint;
|
|
443
461
|
items.push(TEXT_SUFFIXES.has(suffixOf(path.basename(file)))
|
|
444
|
-
? { rel: outRel, text: renderFile(cfg, file, theme,
|
|
462
|
+
? { rel: outRel, text: renderFile(cfg, file, theme, fp, lawsPrefix, new Set(), nativeCatalog), src: file }
|
|
445
463
|
: { rel: outRel, text: null, src: file });
|
|
446
464
|
}
|
|
447
465
|
return { theme, items };
|
package/js/build/source.mjs
CHANGED
|
@@ -12,19 +12,40 @@
|
|
|
12
12
|
* `build.render_all(theme)`. So the CLI has to build the same `cfg` the driver builds, and
|
|
13
13
|
* until this move only the driver could.
|
|
14
14
|
*
|
|
15
|
-
* THE
|
|
16
|
-
* `_build_core.js_cfg()` always
|
|
17
|
-
* originals
|
|
18
|
-
* A Node process has no Python module to mutate, so it sends
|
|
15
|
+
* THE ABSENT KEY IS STILL ABSENT, and the reason is bin/build-driver.mjs's, unchanged:
|
|
16
|
+
* `_build_core.js_cfg()` always sent `structure` and `capabilityLinkRe` because the Python
|
|
17
|
+
* originals were module-level names TESTS MUTATE (`_OWNED` membership asked one level out).
|
|
18
|
+
* A Node process has no Python module to mutate, so it sends no `structure`, and
|
|
19
19
|
* `js/build/render.mjs`'s `cfg.structure ?? STRUCTURE` takes its right-hand branch.
|
|
20
|
+
* `capabilityLinkRe`'s equivalent override in `stripCapabilityLinks` had no producer either
|
|
21
|
+
* and was deleted outright in the over-engineering cleanup, `cfg` param and all.
|
|
20
22
|
*
|
|
21
23
|
* The move is safe for the reason P5c's was: `tests/golden.py` drives `bin/build-driver.mjs`
|
|
22
24
|
* over 259 cells and compares the tree byte-for-byte, and every one of them builds a cfg
|
|
23
25
|
* from these paths. A depth error or a renamed key fails 259 cells, not zero.
|
|
24
26
|
*/
|
|
25
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
27
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
26
28
|
import path from 'node:path';
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One doctrine pack file used to be read three times per render — the survives gate and the
|
|
32
|
+
* body in `js/build/render.mjs`, plus `knownRuleIds` below — and the golden matrix multiplies
|
|
33
|
+
* that by 261 cells. Keyed on mtime, not just path, because the one long-lived process (the
|
|
34
|
+
* web daemon) renders previews of a `src/` the user may be editing between requests; a
|
|
35
|
+
* path-only memo would serve yesterday's pack forever. Newlines come back folded to LF, the
|
|
36
|
+
* same normalisation `readText` gave the render callers — `knownRuleIds` matched identically
|
|
37
|
+
* on raw or folded text, so one behaviour serves both.
|
|
38
|
+
*/
|
|
39
|
+
const _packTexts = new Map();
|
|
40
|
+
export function readPackText(file) {
|
|
41
|
+
const key = path.resolve(file);
|
|
42
|
+
const mtime = statSync(file).mtimeMs;
|
|
43
|
+
const hit = _packTexts.get(key);
|
|
44
|
+
if (hit && hit.mtime === mtime) return hit.text;
|
|
45
|
+
const text = readFileSync(file, 'utf8').replace(/\r\n?/g, '\n');
|
|
46
|
+
_packTexts.set(key, { mtime, text });
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
28
49
|
|
|
29
50
|
/**
|
|
30
51
|
* `_build_core.ROOT` — the checkout, from this file's own location (`js/..`).
|
|
@@ -37,7 +58,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
37
58
|
* which is what makes them comparable and what makes them unfenceable. See the
|
|
38
59
|
* status/version section of `tests/harness_golden.py`.
|
|
39
60
|
*/
|
|
40
|
-
export const ROOT = path.
|
|
61
|
+
export const ROOT = path.resolve(import.meta.dirname, '../..');
|
|
41
62
|
export const SRC = path.join(ROOT, 'src');
|
|
42
63
|
export const CONFIG = path.join(ROOT, 'harness.config.json');
|
|
43
64
|
export const THEMES = path.join(ROOT, 'themes');
|
|
@@ -103,7 +124,7 @@ export function knownRuleIds() {
|
|
|
103
124
|
for (const pack of PACK_ORDER) {
|
|
104
125
|
const file = path.join(SRC, 'doctrines', `${pack}.md`);
|
|
105
126
|
let text = '';
|
|
106
|
-
try { text =
|
|
127
|
+
try { text = readPackText(file); } catch { continue; }
|
|
107
128
|
for (const m of text.matchAll(/^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/gm)) {
|
|
108
129
|
if (m[1] === pack) out.push(`${pack}.${Number(m[2])}`);
|
|
109
130
|
}
|
package/js/build/stubs.mjs
CHANGED
|
@@ -198,59 +198,51 @@ agent-overrides.json
|
|
|
198
198
|
// Seeded stubs — write-once, never overwrite
|
|
199
199
|
// ---------------------------------------------------------------------------
|
|
200
200
|
|
|
201
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* `_build_render`'s eight `ensure_*_stub` functions all wrote this same guard out by hand:
|
|
203
|
+
* write `t` to `p` only when nothing is there yet. The STUB BODIES above are what a recorded
|
|
204
|
+
* corpus compares byte for byte — this only touches the write-once CHECK every one of them
|
|
205
|
+
* repeated, never the text.
|
|
206
|
+
*/
|
|
207
|
+
const seed = (p, t) => { if (!existsSync(p)) writeText(p, t); };
|
|
208
|
+
|
|
209
|
+
/** A stub whose destination is a fixed `out`-relative name and a fixed body. */
|
|
210
|
+
const stubWriter = (file, stub) => (out) => seed(path.join(out, file), stub);
|
|
211
|
+
|
|
212
|
+
/** `_build_render.ensure_context_stub` — the one stub whose body is computed, not literal. */
|
|
202
213
|
export function ensureContextStub(out) {
|
|
203
|
-
|
|
204
|
-
if (!existsSync(dest)) {
|
|
205
|
-
writeText(dest, `${jsonDumpsIndent(CONTEXT_STUB, { ensureAscii: false })}\n`);
|
|
206
|
-
}
|
|
214
|
+
seed(path.join(out, 'context.json'), `${jsonDumpsIndent(CONTEXT_STUB, { ensureAscii: false })}\n`);
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
/** `_build_render.ensure_wiki_stub` — a legacy `wiki.json` counts as present. */
|
|
210
218
|
export function ensureWikiStub(out) {
|
|
211
|
-
|
|
212
|
-
if (!existsSync(dest) && !existsSync(path.join(out, 'wiki.json'))) writeText(dest, WIKI_STUB);
|
|
219
|
+
if (!existsSync(path.join(out, 'wiki.json'))) seed(path.join(out, 'wiki.jsonc'), WIKI_STUB);
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
/** `_build_render.ensure_rules_stub`. */
|
|
216
|
-
export
|
|
217
|
-
const dest = path.join(out, RULES_FILE);
|
|
218
|
-
if (!existsSync(dest)) writeText(dest, RULES_STUB);
|
|
219
|
-
}
|
|
223
|
+
export const ensureRulesStub = stubWriter(RULES_FILE, RULES_STUB);
|
|
220
224
|
|
|
221
225
|
/** `_build_render.ensure_profile_stub`. */
|
|
222
|
-
export
|
|
223
|
-
const dest = path.join(out, PROFILE_FILE);
|
|
224
|
-
if (!existsSync(dest)) writeText(dest, PROFILE_STUB);
|
|
225
|
-
}
|
|
226
|
+
export const ensureProfileStub = stubWriter(PROFILE_FILE, PROFILE_STUB);
|
|
226
227
|
|
|
227
228
|
/**
|
|
228
229
|
* `_build_render.ensure_excludes_stub` — the sovereign-repo list, seeded once and NEVER
|
|
229
230
|
* overwritten. Reachable only from the Claude-shaped emits, which is why it arrived with
|
|
230
231
|
* `emitClaudeRender` rather than with the bundle stubs beside it.
|
|
231
232
|
*/
|
|
232
|
-
export
|
|
233
|
-
const dest = path.join(out, EXCLUDES_FILE);
|
|
234
|
-
if (!existsSync(dest)) writeText(dest, EXCLUDES_STUB);
|
|
235
|
-
}
|
|
233
|
+
export const ensureExcludesStub = stubWriter(EXCLUDES_FILE, EXCLUDES_STUB);
|
|
236
234
|
|
|
237
235
|
/** `_build_render.ensure_bundle_gitignore`. */
|
|
238
|
-
export
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
236
|
+
export const ensureBundleGitignore = stubWriter('.gitignore', BUNDLE_GITIGNORE);
|
|
237
|
+
|
|
238
|
+
/** A stub INSIDE an already-existing store dir — a memory/notebook index, only ever seeded
|
|
239
|
+
* once the store itself exists (`ensureMemoryIndex`/`ensureNotebookIndex` are both called
|
|
240
|
+
* right after the store dir is created, but never invent the dir themselves). */
|
|
241
|
+
const storeIndexWriter = (file, header) => (dir) => { if (isDir(dir)) seed(path.join(dir, file), header); };
|
|
242
242
|
|
|
243
243
|
/** `_build_render.ensure_memory_index` — only inside an EXISTING store dir. */
|
|
244
|
-
export
|
|
245
|
-
if (!isDir(memDir)) return;
|
|
246
|
-
const idx = path.join(memDir, 'MEMORY.md');
|
|
247
|
-
if (!existsSync(idx)) writeText(idx, '# Memory Index\n');
|
|
248
|
-
}
|
|
244
|
+
export const ensureMemoryIndex = storeIndexWriter('MEMORY.md', '# Memory Index\n');
|
|
249
245
|
|
|
250
246
|
/** `_build_render.ensure_notebook_index`. */
|
|
251
|
-
export
|
|
252
|
-
if (!isDir(nbDir)) return;
|
|
253
|
-
const idx = path.join(nbDir, 'NOTEBOOK.md');
|
|
254
|
-
if (!existsSync(idx)) writeText(idx, '# Notebook Index\n');
|
|
255
|
-
}
|
|
247
|
+
export const ensureNotebookIndex = storeIndexWriter('NOTEBOOK.md', '# Notebook Index\n');
|
|
256
248
|
|
package/js/build/themes.mjs
CHANGED
|
@@ -66,7 +66,7 @@ import { stripWhitespaceStart, stripWhitespaceEnd, stripWhitespace } from '../li
|
|
|
66
66
|
* for containers — a `===` here would return `null` every time and silently route every
|
|
67
67
|
* theme through the re-dump fallback, i.e. rewrite all 14 files in full.
|
|
68
68
|
*/
|
|
69
|
-
|
|
69
|
+
function insertThemeKeys(raw, theme, tmpl, tmplKeys, missing) {
|
|
70
70
|
const lines = splitLines(raw);
|
|
71
71
|
if (!lines.length || stripWhitespace(lines[0]) !== '{') return null;
|
|
72
72
|
|
package/js/build/version.mjs
CHANGED
|
@@ -145,9 +145,7 @@ export function writeVersion(cfg, out) {
|
|
|
145
145
|
// Mutating it away stays green here for a reason no cell can remove — freezing the
|
|
146
146
|
// clock across a process boundary is not something the harness can do — which is
|
|
147
147
|
// exactly why the choice is spelled out instead of relying on the comparison.
|
|
148
|
-
const
|
|
149
|
-
const today = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-`
|
|
150
|
-
+ `${String(d.getDate()).padStart(2, '0')}`;
|
|
148
|
+
const today = new Date().toLocaleDateString('sv-SE');
|
|
151
149
|
writeText(path.join(out, VERSION_MARKER),
|
|
152
150
|
`${fp} (built ${today}) [release ${release}]\n`);
|
|
153
151
|
return fp;
|
package/js/cli-table.json
CHANGED
|
@@ -155,6 +155,21 @@
|
|
|
155
155
|
"is_flag": false,
|
|
156
156
|
"type": null,
|
|
157
157
|
"hidden": false
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"names": [
|
|
161
|
+
"--host"
|
|
162
|
+
],
|
|
163
|
+
"dest": "host",
|
|
164
|
+
"metavar": null,
|
|
165
|
+
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'copilot' has no ask tier, so Laws I/IV block and everything else is a stderr warning",
|
|
166
|
+
"choices": null,
|
|
167
|
+
"default": null,
|
|
168
|
+
"required": false,
|
|
169
|
+
"nargs": null,
|
|
170
|
+
"is_flag": false,
|
|
171
|
+
"type": null,
|
|
172
|
+
"hidden": false
|
|
158
173
|
}
|
|
159
174
|
],
|
|
160
175
|
"mutex": []
|
|
@@ -356,6 +371,21 @@
|
|
|
356
371
|
"is_flag": false,
|
|
357
372
|
"type": null,
|
|
358
373
|
"hidden": false
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"names": [
|
|
377
|
+
"--host"
|
|
378
|
+
],
|
|
379
|
+
"dest": "host",
|
|
380
|
+
"metavar": null,
|
|
381
|
+
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'copilot' has no ask tier, so Laws I/IV block and everything else is a stderr warning",
|
|
382
|
+
"choices": null,
|
|
383
|
+
"default": null,
|
|
384
|
+
"required": false,
|
|
385
|
+
"nargs": null,
|
|
386
|
+
"is_flag": false,
|
|
387
|
+
"type": null,
|
|
388
|
+
"hidden": false
|
|
359
389
|
}
|
|
360
390
|
],
|
|
361
391
|
"mutex": []
|
|
@@ -604,6 +634,21 @@
|
|
|
604
634
|
"is_flag": false,
|
|
605
635
|
"type": null,
|
|
606
636
|
"hidden": false
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
"names": [
|
|
640
|
+
"--host"
|
|
641
|
+
],
|
|
642
|
+
"dest": "host",
|
|
643
|
+
"metavar": null,
|
|
644
|
+
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'copilot' has no ask tier, so Laws I/IV block and everything else is a stderr warning",
|
|
645
|
+
"choices": null,
|
|
646
|
+
"default": null,
|
|
647
|
+
"required": false,
|
|
648
|
+
"nargs": null,
|
|
649
|
+
"is_flag": false,
|
|
650
|
+
"type": null,
|
|
651
|
+
"hidden": false
|
|
607
652
|
}
|
|
608
653
|
],
|
|
609
654
|
"mutex": []
|
|
@@ -764,6 +809,45 @@
|
|
|
764
809
|
]
|
|
765
810
|
]
|
|
766
811
|
},
|
|
812
|
+
{
|
|
813
|
+
"name": "tool-gate",
|
|
814
|
+
"help": "toolCall hook (Copilot): git-gate and rule-gate fused behind one command, dispatched on the payload",
|
|
815
|
+
"description": "",
|
|
816
|
+
"positionals": [],
|
|
817
|
+
"options": [
|
|
818
|
+
{
|
|
819
|
+
"names": [
|
|
820
|
+
"--root"
|
|
821
|
+
],
|
|
822
|
+
"dest": "root",
|
|
823
|
+
"metavar": null,
|
|
824
|
+
"help": "the install's own dir (set by the emitted hook); enables the sovereign-repo bypass from <root>/excludes.json",
|
|
825
|
+
"choices": null,
|
|
826
|
+
"default": null,
|
|
827
|
+
"required": false,
|
|
828
|
+
"nargs": null,
|
|
829
|
+
"is_flag": false,
|
|
830
|
+
"type": null,
|
|
831
|
+
"hidden": false
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
"names": [
|
|
835
|
+
"--host"
|
|
836
|
+
],
|
|
837
|
+
"dest": "host",
|
|
838
|
+
"metavar": null,
|
|
839
|
+
"help": "the host that reads the verdict (set by the emitted hook): 'claude' (default) asks; 'copilot' has no ask tier, so Laws I/IV block and everything else is a stderr warning",
|
|
840
|
+
"choices": null,
|
|
841
|
+
"default": null,
|
|
842
|
+
"required": false,
|
|
843
|
+
"nargs": null,
|
|
844
|
+
"is_flag": false,
|
|
845
|
+
"type": null,
|
|
846
|
+
"hidden": false
|
|
847
|
+
}
|
|
848
|
+
],
|
|
849
|
+
"mutex": []
|
|
850
|
+
},
|
|
767
851
|
{
|
|
768
852
|
"name": "tui",
|
|
769
853
|
"help": "print the harness at a glance (interactive terminals only)",
|