geneseed 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/DESIGN.md +31 -20
- package/README.md +2 -2
- package/SETUP.md +30 -3
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +88 -42
- package/bin/geneseed-cli.mjs +33 -32
- package/bin/geneseed-hook.mjs +7 -7
- package/docs/design-history.md +40 -40
- package/docs/extending.md +113 -85
- package/docs/web/rules.md +2 -0
- package/docs/web/setup-choices.md +13 -1
- package/harness.config.json +1 -1
- package/js/README.md +203 -0
- package/js/build/bundle.mjs +231 -0
- package/js/{catalog.mjs → build/catalog.mjs} +7 -7
- package/js/build/emit-claude.mjs +287 -0
- package/js/build/emit-common.mjs +255 -0
- package/js/build/emit-opencode.mjs +214 -0
- package/js/{generate.mjs → build/generate.mjs} +45 -31
- package/js/{render.mjs → build/render.mjs} +72 -11
- package/js/{checkout.mjs → build/source.mjs} +52 -11
- package/js/build/stubs.mjs +256 -0
- package/js/{themes.mjs → build/themes.mjs} +24 -24
- package/js/build/version.mjs +155 -0
- package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
- package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
- package/js/{installs.mjs → hosts/installs.mjs} +68 -16
- package/js/{link.mjs → hosts/link.mjs} +29 -28
- package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
- package/js/{native.mjs → hosts/native.mjs} +24 -22
- package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
- package/js/{settings.mjs → hosts/settings.mjs} +94 -69
- package/js/inspect/checks-authoring.mjs +761 -0
- package/js/inspect/checks-build.mjs +231 -0
- package/js/inspect/checks-repo.mjs +308 -0
- package/js/{diff.mjs → inspect/diff.mjs} +34 -31
- package/js/inspect/doctor.mjs +286 -0
- package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
- package/js/{inventory.mjs → inspect/inventory.mjs} +34 -20
- package/js/{registry.mjs → inspect/registry.mjs} +12 -10
- package/js/inspect/scan.mjs +125 -0
- package/js/{status.mjs → inspect/status.mjs} +27 -25
- package/js/inspect/validate.mjs +199 -0
- package/js/lib/fs.mjs +37 -759
- package/js/lib/json.mjs +344 -0
- package/js/lib/paths.mjs +242 -0
- package/js/lib/proc.mjs +3 -3
- package/js/lib/text.mjs +181 -0
- package/js/lib/udiff.mjs +1 -1
- package/js/{memory.mjs → maintain/memory.mjs} +10 -10
- package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
- package/js/{setup.mjs → maintain/setup.mjs} +49 -37
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +62 -58
- package/js/{update.mjs → maintain/update.mjs} +51 -51
- package/js/{anim.mjs → ui/anim.mjs} +30 -28
- package/js/{cli.mjs → ui/cli.mjs} +10 -10
- package/js/{menu.mjs → ui/menu.mjs} +10 -9
- package/js/{tui.mjs → ui/tui.mjs} +36 -34
- package/js/web/actions.mjs +124 -89
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +88 -58
- package/js/web/daemon.mjs +319 -0
- package/js/web/docs.mjs +24 -21
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +442 -0
- package/js/web/jobs.mjs +29 -21
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/AGENT.md.tmpl +1 -1
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-_MnJcwb_.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-DpjqO-FR.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Bvrjv8oW.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-DZ0IehBV.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-cu4krooO.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-DUOPY65j.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-B5Osg-BF.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/Laws-BK07NKw9.js +3 -0
- package/web/dist/assets/{Library-8y69I3_z.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-Ds5GS1OD.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-7Jxa7ZUX.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-UZ0jWkjg.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-Bgkl0xLd.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-Cx4caF_2.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-De8az7Sv.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-VDAdVQ3q.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-BKrjBRV4.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-wNiNzFFQ.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +135 -69
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1712
- package/js/emit.mjs +0 -1410
- package/web/dist/assets/Laws-D8DJgTBp.js +0 -3
- package/web/dist/assets/index-1pPSz7Kl.css +0 -1
- package/web/dist/assets/index-C-gb-q-r.js +0 -12
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* `js_cfg()` (_build_core.py:199) always sends `structure` and `capabilityLinkRe`, because
|
|
21
21
|
* the Python originals are module-level names that TESTS MUTATE — `_OWNED` membership
|
|
22
22
|
* asked one level out. This driver has no Python module to mutate, so it sends neither and
|
|
23
|
-
* `js/render.mjs:215`'s `cfg.structure ?? STRUCTURE` and `js/
|
|
23
|
+
* `js/build/render.mjs:215`'s `cfg.structure ?? STRUCTURE` and `js/build/bundle.mjs`'s
|
|
24
24
|
* `cfg.capabilityLinkRe ? ... : <default>` take their right-hand branches for the first
|
|
25
25
|
* time. Those two fallbacks have been dead code since they were written — always
|
|
26
26
|
* overridden by the driver that always supplies them. P4 is what makes them live.
|
|
@@ -36,33 +36,36 @@ import {
|
|
|
36
36
|
} from 'node:fs';
|
|
37
37
|
import path from 'node:path';
|
|
38
38
|
import { fileURLToPath } from 'node:url';
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
} from '../js/emit.mjs';
|
|
42
|
-
import { settingsIntegrityCheck } from '../js/settings.mjs';
|
|
43
|
-
import { writeText,
|
|
39
|
+
import { build, phaseLog } from '../js/build/bundle.mjs';
|
|
40
|
+
import { emitClaudeRender } from '../js/build/emit-claude.mjs';
|
|
41
|
+
import { emitOpencodeRender, emitOpencodeGlobalRender } from '../js/build/emit-opencode.mjs';
|
|
42
|
+
import { settingsIntegrityCheck } from '../js/hosts/settings.mjs';
|
|
43
|
+
import { writeText, withPlatformNewlines } from '../js/lib/fs.mjs';
|
|
44
|
+
import { parseJson, jsonDumpsIndent } from '../js/lib/json.mjs';
|
|
44
45
|
// P5c moved these out of this file: `bin/geneseed-cli.mjs` needs the same four resolvers to
|
|
45
46
|
// find a global install, and a resolver that decides WHERE a driver writes is the last thing
|
|
46
47
|
// that should exist twice. golden.py's 259 cells are what made the move safe to attempt.
|
|
47
48
|
import {
|
|
48
|
-
GLOBAL_MANIFEST,
|
|
49
|
+
GLOBAL_MANIFEST, resolvePath, opencodeConfigDir, claudeConfigDir, bobConfigDir,
|
|
49
50
|
copilotConfigDir,
|
|
50
|
-
} from '../js/hosts.mjs';
|
|
51
|
+
} from '../js/hosts/hosts.mjs';
|
|
51
52
|
|
|
52
53
|
// P5d moved these out of this file for the reason P5c moved the host resolvers: `harness
|
|
53
54
|
// status` renders to count, so `bin/geneseed-cli.mjs` needs the same checkout paths and the
|
|
54
55
|
// same cfg. golden.py's 259 cells all build one, which is what made the move safe.
|
|
55
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
ROOT, CONFIG, discoverNames, makeCfg, PACK_ORDER, knownRuleIds,
|
|
58
|
+
} from '../js/build/source.mjs';
|
|
56
59
|
|
|
57
60
|
// P5f moved these, for the same arithmetic a third time: `harness rebuild-all` reads the
|
|
58
61
|
// registry to find every install it must re-emit, and a CLI verb may not reach into a driver
|
|
59
|
-
// for a reader. See js/registry.mjs.
|
|
60
|
-
import { registryRecord, registryRoots } from '../js/registry.mjs';
|
|
62
|
+
// for a reader. See js/inspect/registry.mjs.
|
|
63
|
+
import { registryRecord, registryRoots } from '../js/inspect/registry.mjs';
|
|
61
64
|
|
|
62
65
|
// P2. `--sync-themes` crossed into its own module rather than into this file: it is 90 lines
|
|
63
66
|
// of textual surgery over committed files with a corpus of its own, and it is the half of the
|
|
64
67
|
// maintainer pair that needs nothing this driver is banned from having.
|
|
65
|
-
import { syncThemes } from '../js/themes.mjs';
|
|
68
|
+
import { syncThemes } from '../js/build/themes.mjs';
|
|
66
69
|
|
|
67
70
|
/** The nine `--emit` choices, in build.py:337-338's order. */
|
|
68
71
|
const EMITS = ['files', 'opencode', 'opencode-global', 'claude', 'claude-global',
|
|
@@ -93,24 +96,24 @@ const NATIVE_CATALOG = { opencode: true, claude: true, bob: false, copilot: fals
|
|
|
93
96
|
/** `_build_render.resolve_out` — absolute, or relative to the CURRENT WORKING DIRECTORY
|
|
94
97
|
* (not to ROOT), so the harness renders straight into any repository.
|
|
95
98
|
*
|
|
96
|
-
* EXPORTED IN P5i because `js/setup.mjs` became its second caller: `_setup_summary_lines`
|
|
99
|
+
* EXPORTED IN P5i because `js/maintain/setup.mjs` became its second caller: `_setup_summary_lines`
|
|
97
100
|
* names the AGENT.md a bundle emit just wrote, and it names it by the same resolution the
|
|
98
101
|
* emit used. Copying the one line would be a second owner of the rule that a bare `Harness`
|
|
99
102
|
* follows the invocation and not the checkout — which is what
|
|
100
103
|
* `build/the-bundle-follows-cwd-not-the-checkout` exists to gate.
|
|
101
104
|
*
|
|
102
|
-
* `
|
|
105
|
+
* `resolvePath` AND NOT `path.resolve`, and the difference is the whole reason `resolvePath`
|
|
103
106
|
* exists: the reference ends in `.resolve()`, which canonicalises — 8.3 short names expanded
|
|
104
107
|
* on Windows, symlinks followed on POSIX, the filesystem's own casing — where `path.resolve`
|
|
105
|
-
* only normalises `.`/`..`. Nine `
|
|
108
|
+
* only normalises `.`/`..`. Nine `resolvePath` call sites had the rule and this one did not.
|
|
106
109
|
* It is invisible on a machine whose paths are already canonical, which is every machine
|
|
107
110
|
* this ran on until GitHub's Windows runner handed it a `C:\\Users\\RUNNER~1\\...` and the
|
|
108
111
|
* two CLIs printed the same directory under two different names — a 70-byte difference in a
|
|
109
112
|
* line compared BYTE for byte by `test_the_two_entry_points_agree_on_stdout_BYTES`. The
|
|
110
|
-
* argument to `
|
|
113
|
+
* argument to `resolvePath` is made absolute first so its `expanduser` cannot fire: the
|
|
111
114
|
* reference does not expand `~` here, and a bare `~` is a legal directory name. */
|
|
112
115
|
export function resolveOut(raw) {
|
|
113
|
-
return
|
|
116
|
+
return resolvePath(path.resolve(process.cwd(), raw));
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
/**
|
|
@@ -124,6 +127,7 @@ export function resolveOut(raw) {
|
|
|
124
127
|
function configDefaults() {
|
|
125
128
|
const d = {
|
|
126
129
|
theme: 'neutral', posture: 'peer', mode: 'direct', doctrines: [...PACK_ORDER],
|
|
130
|
+
excludeRules: [],
|
|
127
131
|
};
|
|
128
132
|
if (!existsSync(CONFIG)) return d;
|
|
129
133
|
try {
|
|
@@ -146,6 +150,19 @@ function configDefaults() {
|
|
|
146
150
|
+ 'list of known pack names — using all packs.\n');
|
|
147
151
|
}
|
|
148
152
|
}
|
|
153
|
+
// The second axis, read the same forgiving way and defaulting the OTHER direction: a
|
|
154
|
+
// bad value excludes NOTHING, because the safe answer for a list of things to take
|
|
155
|
+
// away is to take nothing away. Same reasoning as `doctrines` defaulting to all.
|
|
156
|
+
if (data.excludeRules !== undefined) {
|
|
157
|
+
if (Array.isArray(data.excludeRules)
|
|
158
|
+
&& data.excludeRules.every((x) => typeof x === 'string')) {
|
|
159
|
+
d.excludeRules = [...new Set(data.excludeRules.map((x) => x.replace(/[ \t]+/, '.')))]
|
|
160
|
+
.sort();
|
|
161
|
+
} else {
|
|
162
|
+
process.stderr.write(`[geneseed] WARN: ${path.basename(CONFIG)} "excludeRules" is `
|
|
163
|
+
+ 'not a list of rule addresses — excluding nothing.\n');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
149
166
|
}
|
|
150
167
|
} catch {
|
|
151
168
|
process.stderr.write(`[geneseed] WARN: ${path.basename(CONFIG)} is unreadable — `
|
|
@@ -162,7 +179,7 @@ function configDefaults() {
|
|
|
162
179
|
*/
|
|
163
180
|
const VALUED = {
|
|
164
181
|
'--theme': 'theme', '--posture': 'posture', '--mode': 'mode',
|
|
165
|
-
'--doctrines': 'doctrines',
|
|
182
|
+
'--doctrines': 'doctrines', '--exclude-rules': 'excludeRules',
|
|
166
183
|
'--out': 'out', '--target': 'out', '--emit': 'emit',
|
|
167
184
|
'--footprint': 'footprint', '--root': 'root',
|
|
168
185
|
};
|
|
@@ -202,8 +219,39 @@ function choicesFor() {
|
|
|
202
219
|
* carrier, and a marker whose contents depend on argument order is one that compares unequal
|
|
203
220
|
* to itself. Validation is against DISCOVERY, not `PACK_ORDER`, so the refusal names the
|
|
204
221
|
* packs the checkout actually has; a discovered pack missing from `PACK_ORDER` is a
|
|
205
|
-
* different fault and `js/render.mjs` refuses the whole build for it.
|
|
222
|
+
* different fault and `js/build/render.mjs` refuses the whole build for it.
|
|
223
|
+
*/
|
|
224
|
+
/**
|
|
225
|
+
* `--exclude-rules "process 7,craft 3"` (or `process.7`) -> the cfg's exclusion array.
|
|
226
|
+
*
|
|
227
|
+
* BOTH SPELLINGS ACCEPTED ON THE WAY IN, ONE ON THE WAY OUT. The carrier's marker line and
|
|
228
|
+
* every citation in the corpus spell a rule `process 7`; the console's deep link and the
|
|
229
|
+
* catalogue spell it `process.7`. A flag that took only one of them would be wrong for
|
|
230
|
+
* whichever half of the documentation the reader had open, so it takes either and normalises
|
|
231
|
+
* to the dotted form the code compares.
|
|
232
|
+
*
|
|
233
|
+
* ⚠ VALIDATED AGAINST THE PACK FILES, NOT A RANGE. `process 9` and `craft 0` are both
|
|
234
|
+
* refusals, and so is a pack this checkout does not ship — an exclusion that names nothing is
|
|
235
|
+
* a typo that would otherwise take away nothing in silence, which is the failure this flag
|
|
236
|
+
* can least afford: the user asked for a rule to STOP binding and it would keep binding.
|
|
237
|
+
* Sorted, deduped, for the same marker-stability reason `--doctrines` is.
|
|
206
238
|
*/
|
|
239
|
+
function parseExcludeRules(value) {
|
|
240
|
+
const s = value.trim();
|
|
241
|
+
if (s === 'none' || s === '') return [];
|
|
242
|
+
const known = knownRuleIds();
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const raw of s.split(',').map((x) => x.trim()).filter(Boolean)) {
|
|
245
|
+
const id = raw.replace(/[ \t]+/, '.');
|
|
246
|
+
if (!known.includes(id)) {
|
|
247
|
+
die(2, `argument --exclude-rules: invalid choice: '${raw}' (choose from `
|
|
248
|
+
+ `${known.map((c) => `'${c.replace('.', ' ')}'`).join(', ')}, or 'none')`);
|
|
249
|
+
}
|
|
250
|
+
out.push(id);
|
|
251
|
+
}
|
|
252
|
+
return [...new Set(out)].sort();
|
|
253
|
+
}
|
|
254
|
+
|
|
207
255
|
function parseDoctrines(value) {
|
|
208
256
|
const s = value.trim();
|
|
209
257
|
const known = discoverNames('doctrines', PACK_ORDER[0]);
|
|
@@ -264,6 +312,7 @@ function parseArgs(argv, defaults) {
|
|
|
264
312
|
const args = {
|
|
265
313
|
theme: defaults.theme, posture: defaults.posture, mode: defaults.mode,
|
|
266
314
|
doctrines: defaults.doctrines,
|
|
315
|
+
excludeRules: defaults.excludeRules,
|
|
267
316
|
out: 'Harness', emit: 'files', footprint: 'lean', root: null,
|
|
268
317
|
syncThemes: false, validateOnly: false, verbose: false,
|
|
269
318
|
};
|
|
@@ -296,6 +345,7 @@ function parseArgs(argv, defaults) {
|
|
|
296
345
|
// Only when the flag was actually PASSED is this a string; left at its default it is
|
|
297
346
|
// already the array `configDefaults` built, and re-parsing an array would stringify it.
|
|
298
347
|
if (typeof args.doctrines === 'string') args.doctrines = parseDoctrines(args.doctrines);
|
|
348
|
+
if (typeof args.excludeRules === 'string') args.excludeRules = parseExcludeRules(args.excludeRules);
|
|
299
349
|
return args;
|
|
300
350
|
}
|
|
301
351
|
|
|
@@ -308,11 +358,11 @@ function parseArgs(argv, defaults) {
|
|
|
308
358
|
* hand-rolled copy would have its own `--target` alias, its own `choices` lists and its own
|
|
309
359
|
* `-h`, and `test_the_help_text_names_every_flag_the_reference_takes` gates only this one.
|
|
310
360
|
*
|
|
311
|
-
* `
|
|
361
|
+
* `withPlatformNewlines` because the refusal and `-h` paths WRITE: called from `bin/build-driver.mjs`
|
|
312
362
|
* they sit inside `main`'s funnel, and called from the CLI binary they would not.
|
|
313
363
|
*/
|
|
314
364
|
export function parseDriverArgs(argv) {
|
|
315
|
-
return
|
|
365
|
+
return withPlatformNewlines(() => parseArgs(argv, configDefaults()));
|
|
316
366
|
}
|
|
317
367
|
|
|
318
368
|
function choice(flag, value, allowed) {
|
|
@@ -392,7 +442,7 @@ function die(code, msg) {
|
|
|
392
442
|
* re-emit, which is P10's migration arriving five phases early.
|
|
393
443
|
*
|
|
394
444
|
* EXPORTED SINCE P6i, because the emitter is no longer its only caller.
|
|
395
|
-
* `js/uninstall.mjs`'s `remergeClaudeHooks` re-merges the canonical hooks when a disabled
|
|
445
|
+
* `js/maintain/uninstall.mjs`'s `remergeClaudeHooks` re-merges the canonical hooks when a disabled
|
|
396
446
|
* Claude install is turned back on, and `mergeClaudeSettings` refuses to guess the pair —
|
|
397
447
|
* correctly. There is exactly one right answer on this side and it is this function, so the
|
|
398
448
|
* second caller imports it rather than restating two lines that must never drift: a
|
|
@@ -415,14 +465,14 @@ export function hookRunnerEntry() {
|
|
|
415
465
|
* through `_build_core._claude_config_dir`, an `_OWNED` name precisely because the suite
|
|
416
466
|
* redirects it at a sandbox, and a redirect that stops at a subprocess half-works in
|
|
417
467
|
* silence. The child must therefore never derive it — but this file is the PARENT, so
|
|
418
|
-
* deriving it is exactly this file's job, and `
|
|
468
|
+
* deriving it is exactly this file's job, and `the host emit` receives the answer.
|
|
419
469
|
*
|
|
420
470
|
* POSIX spelling, per the original: `claudeMdExcludes` entries are glob patterns, where a
|
|
421
471
|
* backslash is an escape, so the Windows-native form risks never matching.
|
|
422
472
|
*/
|
|
423
473
|
function preambleExclude(claudeMd) {
|
|
424
474
|
if (path.basename(claudeMd) !== 'CLAUDE.md') return null;
|
|
425
|
-
return
|
|
475
|
+
return resolvePath(path.join(claudeConfigDir(), 'CLAUDE.md')).split(path.sep).join('/');
|
|
426
476
|
}
|
|
427
477
|
|
|
428
478
|
/**
|
|
@@ -517,10 +567,6 @@ function pruneOwned(oc, oldOwned, owned) {
|
|
|
517
567
|
}
|
|
518
568
|
}
|
|
519
569
|
|
|
520
|
-
function isDir(p) {
|
|
521
|
-
try { return statSync(p).isDirectory(); } catch { return false; }
|
|
522
|
-
}
|
|
523
|
-
|
|
524
570
|
/**
|
|
525
571
|
* `_build_global._project_survivors` — registered per-repo PROJECT installs of `emitName`.
|
|
526
572
|
*
|
|
@@ -687,7 +733,7 @@ function emitOpencodeGlobal(cfg, args, out) {
|
|
|
687
733
|
* `_build_global._emit_claude_core` — the shared engine behind six emits.
|
|
688
734
|
*
|
|
689
735
|
* Only the two Copilot ones run through it so far, and that is a deliberate split rather
|
|
690
|
-
* than an arbitrary stopping point: `js/emit.mjs
|
|
736
|
+
* than an arbitrary stopping point: `js/build/emit-claude.mjs's wire` gates the entire settings/hook path
|
|
691
737
|
* behind `if (!isCopilot)`, so a Copilot emit never reaches `hookPrefix` and therefore never
|
|
692
738
|
* needs `hookOpts` — the pair of machine-absolute values (`sys.executable` and the harness
|
|
693
739
|
* entry point) that a Node driver has no way to produce. Copilot crossing first isolates
|
|
@@ -958,7 +1004,7 @@ export function emitProjectInto(host, { theme, out, root, footprint = 'full', do
|
|
|
958
1004
|
// same reason: a drift reader that renders `expected` at the DEFAULT pack set reports the
|
|
959
1005
|
// whole doctrines section as edited on every narrowed install. `null` means "no opinion" and
|
|
960
1006
|
// `makeCfg` renders all four, which is what every validation caller wants.
|
|
961
|
-
return
|
|
1007
|
+
return withPlatformNewlines(() => emit(makeCfg(doctrines ? { doctrines } : {}),
|
|
962
1008
|
{ theme, footprint, root }, out));
|
|
963
1009
|
}
|
|
964
1010
|
|
|
@@ -971,7 +1017,7 @@ export function emitProjectInto(host, { theme, out, root, footprint = 'full', do
|
|
|
971
1017
|
* exactly as `run` inherits it below.
|
|
972
1018
|
*/
|
|
973
1019
|
export function buildInto({ theme, out, footprint = 'lean' }) {
|
|
974
|
-
return
|
|
1020
|
+
return withPlatformNewlines(
|
|
975
1021
|
() => build(makeCfg(), theme, out, { footprint, nativeCatalog: false }),
|
|
976
1022
|
);
|
|
977
1023
|
}
|
|
@@ -993,7 +1039,7 @@ export function emitGlobalInto(host, {
|
|
|
993
1039
|
// rendering at `peer`/`direct`; the drift readers pass what the deployment actually says,
|
|
994
1040
|
// because rendering `expected` at the defaults reports AGENT.md as edited on every install
|
|
995
1041
|
// that chose a register — the same scar the footprint left in `diffCollect`, one axis over.
|
|
996
|
-
return
|
|
1042
|
+
return withPlatformNewlines(
|
|
997
1043
|
() => emit(
|
|
998
1044
|
// THE PACK SELECTION RIDES THE SAME RAIL, with one difference: `doctrinesOfDir` answers
|
|
999
1045
|
// `null` for "no marker / undetectable", and that must render at ALL packs rather than
|
|
@@ -1013,16 +1059,16 @@ export function emitGlobalInto(host, {
|
|
|
1013
1059
|
* `_harness_build.cmd_build` is `run([sys.executable, BUILD, *extra]).returncode` — Python
|
|
1014
1060
|
* needs a second process because `build.py` is a different PROGRAM. Here it is a module in
|
|
1015
1061
|
* the same one, and `bin/geneseed-cli.mjs` is under a transitive `child_process` ban, so
|
|
1016
|
-
* `js/generate.mjs` calls this directly. The export is what makes the auto-run below need a
|
|
1062
|
+
* `js/build/generate.mjs` calls this directly. The export is what makes the auto-run below need a
|
|
1017
1063
|
* guard: without one, importing this file to reach `main` would RUN the generator with the
|
|
1018
1064
|
* CLI's argv.
|
|
1019
1065
|
*/
|
|
1020
1066
|
export function main(argv) {
|
|
1021
|
-
// `
|
|
1067
|
+
// `withPlatformNewlines` is what makes this driver's bytes Python's bytes on Windows, for every
|
|
1022
1068
|
// print site in the whole render tree at once — see its docblock for why a funnel and not
|
|
1023
|
-
// 25 calls to `
|
|
1069
|
+
// 25 calls to `printOut`. It wraps the `catch` as well as `run`, because `die` writes its
|
|
1024
1070
|
// line on the way out.
|
|
1025
|
-
return
|
|
1071
|
+
return withPlatformNewlines(() => {
|
|
1026
1072
|
try {
|
|
1027
1073
|
return run(argv);
|
|
1028
1074
|
} catch (e) {
|
|
@@ -1043,12 +1089,12 @@ function run(argv) {
|
|
|
1043
1089
|
// different places because the transitive `child_process` ban splits them.
|
|
1044
1090
|
//
|
|
1045
1091
|
// `--sync-themes` is HERE, because it needs nothing this driver may not have: it reads
|
|
1046
|
-
// `themes/`, rewrites the files textually, and prints. `js/themes.mjs` carries it.
|
|
1092
|
+
// `themes/`, rewrites the files textually, and prints. `js/build/themes.mjs` carries it.
|
|
1047
1093
|
// Non-zero when files were CHANGED (0 == already in sync), so CI can run it as a drift
|
|
1048
1094
|
// check — build.py:393-397's mapping, and a refusal (exit 2) if the template is unreadable.
|
|
1049
1095
|
if (args.syncThemes) return syncThemes() ? 1 : 0;
|
|
1050
1096
|
// `--validate-only` is NOT, and cannot be: its source-tree half is the doctor, and
|
|
1051
|
-
// `js/
|
|
1097
|
+
// `js/inspect/checks-authoring.mjs` starts a process (`node --check` over the OpenCode plugins). This driver
|
|
1052
1098
|
// is under a transitive ban on reaching any such module, gated twice — by a source grep in
|
|
1053
1099
|
// `tests/test_node_cli_parity.py` and by an import walk in `tests/test_hook_cli_parity.py`.
|
|
1054
1100
|
// So the tool crossed onto the CLI binary, which already carries the doctor, and this flag
|
|
@@ -1123,10 +1169,10 @@ function run(argv) {
|
|
|
1123
1169
|
}
|
|
1124
1170
|
|
|
1125
1171
|
/**
|
|
1126
|
-
* Run only when this file IS the entry point, not when `js/generate.mjs` imports it.
|
|
1172
|
+
* Run only when this file IS the entry point, not when `js/build/generate.mjs` imports it.
|
|
1127
1173
|
*
|
|
1128
1174
|
* `import.meta.main` would say this in one word and is Node 24; the spec's `engines` floor
|
|
1129
|
-
* is still open and this machine runs v22, the
|
|
1175
|
+
* is still open and this machine runs v22, which is why the comparison is spelled out rather than using it. So the
|
|
1130
1176
|
* check is the manual one, through `realpathSync` because `process.argv[1]` is whatever the
|
|
1131
1177
|
* caller typed and `import.meta.url` is always the resolved file — an `npx`-generated shim
|
|
1132
1178
|
* or a `geneseed link` symlink makes those differ as strings while naming the same file.
|
|
@@ -1134,9 +1180,9 @@ function run(argv) {
|
|
|
1134
1180
|
* BOTH ways of getting this wrong are gated, which is why it is a guard and not a comment,
|
|
1135
1181
|
* and both were measured rather than argued:
|
|
1136
1182
|
*
|
|
1137
|
-
* * stuck FALSE — `node bin/
|
|
1183
|
+
* * stuck FALSE — `node bin/build-driver.mjs` becomes a silent no-op: 259 golden cells.
|
|
1138
1184
|
* * stuck TRUE — importing this file runs the generator on the IMPORTER's argv, so it is
|
|
1139
|
-
* not only `build` that breaks. `bin/geneseed-cli.mjs` reaches `js/generate.mjs` at
|
|
1185
|
+
* not only `build` that breaks. `bin/geneseed-cli.mjs` reaches `js/build/generate.mjs` at
|
|
1140
1186
|
* module load, so every CLI verb dies in the driver's flag parser: **62 of the 166
|
|
1141
1187
|
* acceptance cells**, `exclude` and `status` and `version` among them.
|
|
1142
1188
|
*/
|
package/bin/geneseed-cli.mjs
CHANGED
|
@@ -4,24 +4,24 @@
|
|
|
4
4
|
* invokes. `bin/geneseed-hook.mjs` is the twin of the four it does.
|
|
5
5
|
*
|
|
6
6
|
* WHY A THIRD BINARY. P5c's whole question, and the three candidates were: grow the hook
|
|
7
|
-
* entry, put verbs in `bin/
|
|
7
|
+
* entry, put verbs in `bin/build-driver.mjs`, or this. Each of the other two costs something
|
|
8
8
|
* specific.
|
|
9
9
|
*
|
|
10
10
|
* * GROWING `bin/geneseed-hook.mjs` weakens the gate the design now rests on.
|
|
11
11
|
* `test_the_entry_carries_exactly_the_verbs_the_emitter_wires` asserts EQUALITY between
|
|
12
|
-
* that file's table and the hooks `js/settings.mjs` wires, and P5b made it load-bearing:
|
|
12
|
+
* that file's table and the hooks `js/hosts/settings.mjs` wires, and P5b made it load-bearing:
|
|
13
13
|
* the shim is machine-wide (`~/.geneseed/bin/geneseed-hook[.cmd]`, no per-install
|
|
14
14
|
* component) and last-writer-wins, so a Node emit owns the hooks of installs Python
|
|
15
15
|
* wrote, and the verb sets matching is what makes that safe. Relaxing it to a subset
|
|
16
16
|
* relation to admit `exclude` would be the single most damaging edit available here.
|
|
17
17
|
* Two more reasons point the same way, and the second is not in that file's own
|
|
18
|
-
* argument: `exclude`'s writer imports `js/settings.mjs` and `
|
|
18
|
+
* argument: `exclude`'s writer imports `js/hosts/settings.mjs` and `the emit modules`, ~2,100 lines
|
|
19
19
|
* that would then load on EVERY PreToolUse call — the spec's hook-latency budget says
|
|
20
20
|
* the shim must exec a minimal entry, not the CLI. And the entry's refusal list, which
|
|
21
21
|
* names every verb it does not carry, is what makes "silently accepted `doctor` and did
|
|
22
22
|
* nothing" impossible; every Geneseed hook exits 0 and signals through stdout, so a
|
|
23
23
|
* no-op and a success are one observation.
|
|
24
|
-
* * `bin/
|
|
24
|
+
* * `bin/build-driver.mjs` IS `build.py`'s `main()` — a different Python program from
|
|
25
25
|
* `rituals/harness.py`, not a different half of the same one. It parses generator FLAGS
|
|
26
26
|
* with a partition asserted over them (`test_the_node_driver_classifies_every_emit`),
|
|
27
27
|
* `tests/golden.py` drives it 259 times per run, and it is under a hard `child_process`
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*
|
|
32
32
|
* What this costs is what the P5b handoff predicted: a second acceptance matrix. It is paid
|
|
33
33
|
* in `tests/harness_golden.py`, which now carries a `bin` per cell and takes `--new-cli`
|
|
34
|
-
* beside `--new` — and it would have been paid identically by the `bin/
|
|
34
|
+
* beside `--new` — and it would have been paid identically by the `bin/build-driver.mjs` option,
|
|
35
35
|
* so it is not a discriminator between the two.
|
|
36
36
|
*
|
|
37
37
|
* THE VERB SET REFUSES THE REST BY NAME, exactly as the hook entry does. It carries 22 of the
|
|
@@ -46,24 +46,25 @@
|
|
|
46
46
|
* for the file as a whole is that its transitive imports now reach two modules that spawn,
|
|
47
47
|
* which `_ALLOWED_SPAWNS` in `tests/test_hook_cli_parity.py` declares argv by argv.
|
|
48
48
|
*/
|
|
49
|
-
import { cmdCatalog } from '../js/catalog.mjs';
|
|
50
|
-
import { cliSpec, printHelp } from '../js/cli.mjs';
|
|
51
|
-
import { cmdDiff } from '../js/diff.mjs';
|
|
52
|
-
import { cmdDoctor
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
49
|
+
import { cmdCatalog } from '../js/build/catalog.mjs';
|
|
50
|
+
import { cliSpec, printHelp } from '../js/ui/cli.mjs';
|
|
51
|
+
import { cmdDiff } from '../js/inspect/diff.mjs';
|
|
52
|
+
import { cmdDoctor } from '../js/inspect/doctor.mjs';
|
|
53
|
+
import { cmdValidate } from '../js/inspect/validate.mjs';
|
|
54
|
+
import { parseDriverArgs } from './build-driver.mjs';
|
|
55
|
+
import { cmdExclude } from '../js/inspect/excludes.mjs';
|
|
56
|
+
import { cmdBuild, cmdPrompt, cmdRebuildAll, cmdTheme } from '../js/build/generate.mjs';
|
|
57
|
+
import { cmdMigrate } from '../js/maintain/migrate.mjs';
|
|
58
|
+
import { parseIntStrict } from '../js/lib/text.mjs';
|
|
59
|
+
import { cmdLink, cmdUnlink } from '../js/hosts/link.mjs';
|
|
60
|
+
import { cmdMcp } from '../js/hosts/mcp.mjs';
|
|
61
|
+
import { cmdMemory } from '../js/maintain/memory.mjs';
|
|
62
|
+
import { cmdHome, cmdMenu } from '../js/ui/menu.mjs';
|
|
63
|
+
import { cmdTui } from '../js/ui/tui.mjs';
|
|
64
|
+
import { cmdSetup } from '../js/maintain/setup.mjs';
|
|
65
|
+
import { cmdStatus, cmdVersion } from '../js/inspect/status.mjs';
|
|
66
|
+
import { cmdUninstall } from '../js/maintain/uninstall.mjs';
|
|
67
|
+
import { cmdBootstrap, cmdSyncSelf, cmdUpgrade } from '../js/maintain/update.mjs';
|
|
67
68
|
import { cmdWeb } from '../js/web/server.mjs';
|
|
68
69
|
|
|
69
70
|
/**
|
|
@@ -74,7 +75,7 @@ import { cmdWeb } from '../js/web/server.mjs';
|
|
|
74
75
|
* `add_argument` calls, gated against the subparsers by NAME only. When
|
|
75
76
|
* `_web_docs._cli_reference` stopped walking the parser and started reading the CLI table,
|
|
76
77
|
* keeping that would have made THREE transcriptions of one parser — the parser, the file, and
|
|
77
|
-
* here. So the argument surface comes from `js/cli.mjs`'s `cliSpec()`, derived from the same
|
|
78
|
+
* here. So the argument surface comes from `js/ui/cli.mjs`'s `cliSpec()`, derived from the same
|
|
78
79
|
* file the docs page serves, and what is left in a row is the one thing a data file cannot
|
|
79
80
|
* hold: the FUNCTION.
|
|
80
81
|
*
|
|
@@ -150,14 +151,14 @@ const VERBS = {
|
|
|
150
151
|
// Points at `cmdSyncSelf` rather than at `cmdUpgrade`, and that is not tidiness:
|
|
151
152
|
// `sync-self` DROPS its `ref` before `upgrade` ever sees it, where `upgrade` both WARNS
|
|
152
153
|
// about one and re-reads it as a theme — so the two verbs answer `sync-self cyberpunk`
|
|
153
|
-
// and `sync-self v1.2.3` differently. See `js/update.mjs`'s `syncSelf` for both halves.
|
|
154
|
+
// and `sync-self v1.2.3` differently. See `js/maintain/update.mjs`'s `syncSelf` for both halves.
|
|
154
155
|
fn: cmdSyncSelf,
|
|
155
156
|
},
|
|
156
157
|
bootstrap: {
|
|
157
158
|
fn: cmdBootstrap,
|
|
158
159
|
},
|
|
159
160
|
// P7a. Both are DISPATCHERS whose off-TTY arm is the whole of what a cell can reach, and
|
|
160
|
-
// `menu`'s on-TTY arm falls back rather than opening a panel — `js/menu.mjs`'s header
|
|
161
|
+
// `menu`'s on-TTY arm falls back rather than opening a panel — `js/ui/menu.mjs`'s header
|
|
161
162
|
// argues both.
|
|
162
163
|
menu: {
|
|
163
164
|
fn: cmdMenu,
|
|
@@ -168,7 +169,7 @@ const VERBS = {
|
|
|
168
169
|
// P7b, and it is the twenty-fifth and last. `cmd_tui`'s FIRST arm is `if not sys.stdin.
|
|
169
170
|
// isatty()`, so off a TTY — which is every cell there is — the verb is one line and an
|
|
170
171
|
// exit code, and that arm crosses byte for byte. The panel behind it is P7c's;
|
|
171
|
-
// `js/tui.mjs`'s header argues why this entry falls back to `cmd_tui`'s own
|
|
172
|
+
// `js/ui/tui.mjs`'s header argues why this entry falls back to `cmd_tui`'s own
|
|
172
173
|
// panel-unavailable line rather than inventing a second full-screen UI, and
|
|
173
174
|
// `tests/test_tui_boundary.py` asserts that the arm it declares is genuinely unreachable
|
|
174
175
|
// here rather than merely untested.
|
|
@@ -201,7 +202,7 @@ const VERBS = {
|
|
|
201
202
|
};
|
|
202
203
|
|
|
203
204
|
function die(code, msg) {
|
|
204
|
-
// CRLF on Windows, for the same reason `js/hooks.mjs`'s funnels translate: argparse writes
|
|
205
|
+
// CRLF on Windows, for the same reason `js/hosts/hooks.mjs`'s funnels translate: argparse writes
|
|
205
206
|
// this line through `sys.stderr`, which does.
|
|
206
207
|
process.stderr.write(`geneseed: error: ${msg}${process.platform === 'win32' ? '\r\n' : '\n'}`);
|
|
207
208
|
return code;
|
|
@@ -211,7 +212,7 @@ function die(code, msg) {
|
|
|
211
212
|
* `argparse`'s surface for the verbs here, and only theirs: ordered positionals, some
|
|
212
213
|
* optional, some with `choices`.
|
|
213
214
|
*
|
|
214
|
-
* `spec` comes from `js/cli.mjs`'s `cliSpec()` since P10c — derived from `js/cli-table.json`,
|
|
215
|
+
* `spec` comes from `js/ui/cli.mjs`'s `cliSpec()` since P10c — derived from `js/cli-table.json`,
|
|
215
216
|
* which P2 made the owned document. This function's rules are unchanged; what
|
|
216
217
|
* changed is that the table they run over is no longer written out by hand beside them. Every
|
|
217
218
|
* argparse feature named below is a row of that derivation.
|
|
@@ -274,7 +275,7 @@ function parse(spec, argv) {
|
|
|
274
275
|
// `type=int`. argparse's own wording, minus the usage block it prints around it — the
|
|
275
276
|
// same rule the rest of this parser follows. The VALUE stays a string in `args`; the
|
|
276
277
|
// command converts, and this is what guarantees the conversion cannot fail there.
|
|
277
|
-
if ((spec.ints ?? []).includes(flag) &&
|
|
278
|
+
if ((spec.ints ?? []).includes(flag) && parseIntStrict(value) === null) {
|
|
278
279
|
return { error: `argument ${flag}: invalid int value: '${value}'` };
|
|
279
280
|
}
|
|
280
281
|
args[dest] = value;
|
|
@@ -347,7 +348,7 @@ async function main(argv) {
|
|
|
347
348
|
//
|
|
348
349
|
// So a row would have meant amending two gates and recording a corpus half blind. It takes
|
|
349
350
|
// the generator's own parser (see `parseDriverArgs`) because its flags are the generator's,
|
|
350
|
-
// and it is on THIS binary — not on `bin/
|
|
351
|
+
// and it is on THIS binary — not on `bin/build-driver.mjs` — because it runs the doctor, which
|
|
351
352
|
// starts a process the driver is banned from reaching.
|
|
352
353
|
if (verb === 'validate') {
|
|
353
354
|
try {
|
|
@@ -397,7 +398,7 @@ async function main(argv) {
|
|
|
397
398
|
return await spec.fn(parsed.args);
|
|
398
399
|
} catch (e) {
|
|
399
400
|
// `e.exitCode` is the generator's existing marker for a DELIBERATE refusal that has
|
|
400
|
-
// already explained itself on stderr — `js/emit.mjs
|
|
401
|
+
// already explained itself on stderr — `js/build/emit-claude.mjs` reads the same flag, and it is
|
|
401
402
|
// `sys.exit(<message>)` on the Python side. Anything without it is a crash and keeps its
|
|
402
403
|
// stack: Python prints a traceback for an unhandled exception, and turning one into a
|
|
403
404
|
// tidy one-liner here would hide a bug behind a refusal's clothing.
|
package/bin/geneseed-hook.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* The hook entry point — a Node twin of `rituals/harness.py` for the four verbs the
|
|
4
4
|
* emitted `settings.json` actually invokes.
|
|
5
5
|
*
|
|
6
|
-
* WHY IT IS A SECOND BINARY AND NOT A SUBCOMMAND OF `bin/
|
|
6
|
+
* WHY IT IS A SECOND BINARY AND NOT A SUBCOMMAND OF `bin/build-driver.mjs`. Three reasons,
|
|
7
7
|
* and each alone would be enough:
|
|
8
8
|
*
|
|
9
9
|
* * The generator driver is under a hard `child_process` ban
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* * The hook shim bakes ONE entry and forwards `%*` to it. `harness.py` is that entry on
|
|
15
15
|
* the Python side; a Node twin needs its own, invoked as `<node> <this> context
|
|
16
16
|
* --root "<cfg>"`.
|
|
17
|
-
* * `bin/
|
|
17
|
+
* * `bin/build-driver.mjs` parses generator FLAGS (`--emit`, `--theme`, `--footprint`), and
|
|
18
18
|
* `test_the_node_driver_classifies_every_emit` asserts a partition over them. Verbs
|
|
19
19
|
* are a different shape and would sit awkwardly inside that.
|
|
20
20
|
*
|
|
21
|
-
* WHAT BAKES THIS FILE, SINCE P5b. `bin/
|
|
21
|
+
* WHAT BAKES THIS FILE, SINCE P5b. `bin/build-driver.mjs` writes `<node> <checkout>/bin/
|
|
22
22
|
* geneseed-hook.mjs` into the machine-wide shim, so an install this driver emitted needs no
|
|
23
23
|
* Python for its hooks. The exit-4 refusal it used to raise when no interpreter was
|
|
24
24
|
* discoverable is gone with the discovery that fed it.
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
* every Geneseed hook returns 0 and signals through stdout — so "did nothing" and "worked"
|
|
38
38
|
* are the same observation.
|
|
39
39
|
*/
|
|
40
|
-
import { cmdContext, cmdGitGate, cmdRuleGate, cmdLearn } from '../js/hooks.mjs';
|
|
41
|
-
import { printHelp } from '../js/cli.mjs';
|
|
40
|
+
import { cmdContext, cmdGitGate, cmdRuleGate, cmdLearn } from '../js/hosts/hooks.mjs';
|
|
41
|
+
import { printHelp } from '../js/ui/cli.mjs';
|
|
42
42
|
|
|
43
43
|
const VERBS = {
|
|
44
44
|
context: { fn: cmdContext, flags: { '--root': 'root' } },
|
|
@@ -53,7 +53,7 @@ const VERBS = {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
function die(code, msg) {
|
|
56
|
-
// CRLF on Windows, for the same reason `js/hooks.mjs`'s funnels translate: argparse
|
|
56
|
+
// CRLF on Windows, for the same reason `js/hosts/hooks.mjs`'s funnels translate: argparse
|
|
57
57
|
// writes this line through `sys.stderr`, which does.
|
|
58
58
|
process.stderr.write(`geneseed-hook: error: ${msg}${process.platform === 'win32' ? '\r\n' : '\n'}`);
|
|
59
59
|
return code;
|
|
@@ -115,7 +115,7 @@ function main(argv) {
|
|
|
115
115
|
// `<verb> --help`, for the same reason and by the same owner as `bin/geneseed-cli.mjs`:
|
|
116
116
|
// argparse holds `-h` at the parser, so this entry's `parse` calls it an unrecognized
|
|
117
117
|
// argument. The four hook verbs are four of the reference's 26 and had the same gap.
|
|
118
|
-
// `js/cli.mjs` reads the CLI table lazily, inside its functions, so importing it costs this
|
|
118
|
+
// `js/ui/cli.mjs` reads the CLI table lazily, inside its functions, so importing it costs this
|
|
119
119
|
// entry a module parse and no file read on the hook path.
|
|
120
120
|
if (argv.slice(1).some((t) => t === '-h' || t === '--help')) {
|
|
121
121
|
const rc = printHelp('geneseed-hook', verb);
|