geneseed 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/README.md +22 -12
- package/SETUP.md +38 -7
- package/SHIPPED.md +6 -5
- package/adapters/bob/README.md +40 -10
- package/adapters/claude-code/README.md +5 -0
- package/adapters/claude-code/settings.json +8 -1
- package/adapters/copilot/README.md +50 -24
- package/adapters/opencode/README.md +5 -1
- package/adapters/opencode/plugins/geneseed-guard.js +40 -6
- package/bin/build-driver.mjs +16 -10
- package/bin/geneseed-cli.mjs +55 -56
- package/bin/geneseed-hook.mjs +21 -14
- package/docs/candidate-hosts.md +74 -0
- package/docs/extending.md +6 -5
- package/docs/token-footprint.md +32 -21
- package/docs/web-ui.md +5 -13
- package/harness.config.json +1 -1
- package/js/README.md +5 -6
- package/js/build/emit-claude.mjs +22 -7
- package/js/build/render.mjs +37 -19
- package/js/build/source.mjs +22 -2
- package/js/cli-table.json +84 -0
- package/js/hosts/hooks.mjs +240 -78
- package/js/hosts/hosts.mjs +56 -2
- package/js/hosts/link.mjs +1 -1
- package/js/hosts/mcp.mjs +3 -1
- package/js/hosts/native.mjs +17 -0
- package/js/hosts/settings.mjs +173 -5
- package/js/inspect/excludes.mjs +1 -1
- package/js/inspect/inventory.mjs +1 -1
- package/js/inspect/status.mjs +50 -2
- package/js/maintain/uninstall.mjs +38 -11
- package/js/maintain/update.mjs +2 -2
- package/js/web/actions.mjs +8 -8
- package/js/web/api.mjs +5 -10
- package/js/web/docs.mjs +9 -31
- package/js/web/handler.mjs +20 -30
- package/js/web/jobs.mjs +5 -26
- package/js/web/routes.mjs +18 -84
- package/js/web/server.mjs +3 -3
- package/package.json +1 -1
- package/registry.json +14 -0
- package/src/AGENT.md.tmpl +55 -16
- package/src/agents/explorer.md +1 -1
- package/src/doctrines/README.md +3 -3
- package/src/doctrines/rigor.md +13 -0
- package/src/laws/universal.md +98 -20
- package/src/ontology/universal.md +38 -13
- package/src/skills/_template.md +5 -1
- package/src/skills/bruno-collection-generator.md +63 -0
- package/src/skills/bruno-test-writer.md +62 -0
- package/src/skills/crash-course.md +1 -0
- package/src/skills/decode.md +1 -0
- package/src/skills/drill.md +1 -0
- package/src/skills/feynman.md +1 -0
- package/src/skills/forge-mcp.md +1 -1
- package/src/skills/herdr.md +1 -0
- package/src/skills/learning-path.md +1 -0
- package/src/skills/opencode-theme.md +1 -0
- package/src/skills/research.md +1 -1
- package/themes/_TEMPLATE.json +3 -0
- package/themes/biker.json +3 -0
- package/themes/commentator.json +3 -0
- package/themes/cyberpunk.json +3 -0
- package/themes/gamer.json +3 -0
- package/themes/imperial.json +3 -0
- package/themes/joker.json +3 -0
- package/themes/marvin.json +3 -0
- package/themes/mean.json +3 -0
- package/themes/military.json +3 -0
- package/themes/neutral.json +3 -0
- package/themes/pirate.json +3 -0
- package/themes/sports.json +3 -0
- package/themes/verstappen.json +3 -0
- package/themes/wizard.json +3 -0
- package/web/dist/assets/Activity-BfVcLrda.js +1 -0
- package/web/dist/assets/ActivityDetail-CQXccGPl.js +1 -0
- package/web/dist/assets/{CatalogRow-DqS4e5XM.js → CatalogRow-DALEQImZ.js} +1 -1
- package/web/dist/assets/{Diff-CIrGbiqm.js → Diff-BIjxXx46.js} +1 -1
- package/web/dist/assets/{Doctor-CpI53oHY.js → Doctor-CJLbgsrw.js} +1 -1
- package/web/dist/assets/FilterInput-CkUCk_lY.js +1 -0
- package/web/dist/assets/GreenhouseView-DfC5IILJ.js +2 -0
- package/web/dist/assets/{Harness-vRYs11kX.js → Harness-Dm5CCU-v.js} +1 -1
- package/web/dist/assets/JournalView-Cpu2eI4s.js +1 -0
- package/web/dist/assets/Laws-DDDVFuQz.js +3 -0
- package/web/dist/assets/Library-Dao3T9na.js +1 -0
- package/web/dist/assets/{Markdown-kgpg9EQM.js → Markdown-C__nuWX5.js} +1 -1
- package/web/dist/assets/OperatorHudView-DEpCtAjP.js +2 -0
- package/web/dist/assets/{Profile-CHr-PPf6.js → Profile-BlXtUlh2.js} +1 -1
- package/web/dist/assets/{Rules-CsC6hfqk.js → Rules-DIre6gxR.js} +1 -1
- package/web/dist/assets/Skills-rY13ZPhM.js +1 -0
- package/web/dist/assets/{StatusBadge-gRrak0nO.js → StatusBadge-wW_RS_my.js} +1 -1
- package/web/dist/assets/{activity-B0-FXZCj.js → activity-DpYPKNrA.js} +1 -1
- package/web/dist/assets/{index-DJOhOwwR.js → index-BEZvPvpF.js} +1 -1
- package/web/dist/assets/index-DNQE5ydy.js +10 -0
- package/web/dist/assets/index-DzdQjvM1.css +1 -0
- package/web/dist/assets/{index-Bx3I7cRW.js → index-PmQGRS1X.js} +1 -1
- package/web/dist/assets/jobBuckets-DfTUJUxR.js +1 -0
- package/web/dist/assets/lawCats-DUS9UTH4.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +82 -64
- package/js/web/graph.mjs +0 -0
- package/web/dist/assets/Activity-BTCG2gMl.js +0 -1
- package/web/dist/assets/ActivityDetail-julsmPDB.js +0 -1
- package/web/dist/assets/Graph-CS4tA7YS.js +0 -1
- package/web/dist/assets/Laws-BjbHJrJt.js +0 -3
- package/web/dist/assets/Library-CjC7BAWN.js +0 -1
- package/web/dist/assets/Skills-BvatJGfe.js +0 -1
- package/web/dist/assets/index-BnRWQbZb.js +0 -12
- package/web/dist/assets/index-D0f9tJlA.css +0 -1
- package/web/dist/assets/roman-DkYN0LX9.js +0 -1
package/js/web/handler.mjs
CHANGED
|
@@ -19,10 +19,10 @@ import { apiDeployCmd, apiInstallCmd, apiPickFolder, apiRestore, buildOverride,
|
|
|
19
19
|
import { NotFound, PREFIX_ROUTES, STATE_ROUTES } from './api.mjs';
|
|
20
20
|
import { openUrl, requestRestart } from './daemon.mjs';
|
|
21
21
|
import { apiDocs, apiDocsPage } from './docs.mjs';
|
|
22
|
-
import {
|
|
23
|
-
import { POST_ROUTES,
|
|
22
|
+
import { actionCommands } from './jobs.mjs';
|
|
23
|
+
import { POST_ROUTES, readJsonBody } from './routes.mjs';
|
|
24
24
|
import { isFile } from '../lib/fs.mjs';
|
|
25
|
-
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
26
26
|
import { basename, dirname, extname, join, sep } from 'node:path';
|
|
27
27
|
import { gzipSync } from 'node:zlib';
|
|
28
28
|
|
|
@@ -57,13 +57,17 @@ const CTYPES = {
|
|
|
57
57
|
// ---- the request handler ---------------------------------------------------
|
|
58
58
|
|
|
59
59
|
export function makeHandler(state, jm, token, dist, holder = null) {
|
|
60
|
+
// fp -> { mtime, data, gz }. Keyed by path but VALIDATED by mtime on every hit, so a
|
|
61
|
+
// rebuilt web/dist serves fresh bytes without `web restart` — the re-stat costs one
|
|
62
|
+
// syscall against the readFileSync + gzipSync it saves. `gz` is the gzipped body,
|
|
63
|
+
// computed once on the first gzip-accepting request instead of per request.
|
|
60
64
|
const staticCache = new Map();
|
|
61
65
|
|
|
62
|
-
function sendBytes(res, body, ctype, code = 200, extra = null, acceptEncoding = '') {
|
|
66
|
+
function sendBytes(res, body, ctype, code = 200, extra = null, acceptEncoding = '', gzStore = null) {
|
|
63
67
|
const headers = { ...(extra || {}) };
|
|
64
68
|
if (body.length >= GZIP_MIN && GZIP_TYPES.some((t) => ctype.startsWith(t))
|
|
65
69
|
&& (acceptEncoding || '').includes('gzip')) {
|
|
66
|
-
body = gzipSync(body, { level: 6 });
|
|
70
|
+
body = gzStore ? (gzStore.gz ??= gzipSync(body, { level: 6 })) : gzipSync(body, { level: 6 });
|
|
67
71
|
headers['Content-Encoding'] = 'gzip';
|
|
68
72
|
headers.Vary = 'Accept-Encoding';
|
|
69
73
|
}
|
|
@@ -91,21 +95,25 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
91
95
|
if (!isFile(fp)) {
|
|
92
96
|
return sendJson(res, { error: 'web/dist missing — run the UI build' }, 500, acceptEncoding);
|
|
93
97
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
const mtime = statSync(fp).mtimeMs;
|
|
99
|
+
let entry = staticCache.get(fp);
|
|
100
|
+
if (entry === undefined || entry.mtime !== mtime) {
|
|
101
|
+
entry = { mtime, data: readFileSync(fp), gz: null };
|
|
102
|
+
staticCache.set(fp, entry);
|
|
98
103
|
}
|
|
104
|
+
let data = entry.data;
|
|
99
105
|
let extra = null;
|
|
100
106
|
if (basename(fp) === 'index.html') {
|
|
101
107
|
const inject = Buffer.from(`<script>window.__GENESEED_TOKEN__="${token}";</script>`, 'utf-8');
|
|
102
108
|
data = replaceOnce(data, Buffer.from('</head>'), Buffer.concat([inject, Buffer.from('</head>')]));
|
|
103
109
|
extra = { 'Cache-Control': 'no-store' };
|
|
110
|
+
// The injected bytes differ from `entry.data`, so the gz cache must not serve them.
|
|
111
|
+
entry = null;
|
|
104
112
|
} else if (path.includes('/assets/')) {
|
|
105
113
|
extra = { 'Cache-Control': 'public, max-age=31536000, immutable' };
|
|
106
114
|
}
|
|
107
115
|
return sendBytes(res, data, CTYPES[extname(fp)] || 'application/octet-stream',
|
|
108
|
-
200, extra, acceptEncoding);
|
|
116
|
+
200, extra, acceptEncoding, entry);
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
function doGet(req, res, path, ae) {
|
|
@@ -120,14 +128,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
120
128
|
}
|
|
121
129
|
if (path.startsWith('/api/docs/page/')) {
|
|
122
130
|
const pid = percentDecode(path.slice('/api/docs/page/'.length));
|
|
123
|
-
|
|
124
|
-
return sendJson(res, apiDocsPage(state, pid, harnessParam(req.url)), 200, ae);
|
|
125
|
-
} catch (e) {
|
|
126
|
-
// An unimplemented doc kind says so explicitly (501), rather than falling through
|
|
127
|
-
// to the `NotFound` at the bottom of `apiDocsPage` and claiming the page is missing.
|
|
128
|
-
if (e && e.notPorted) return sendJson(res, { error: e.message }, 501, ae);
|
|
129
|
-
throw e;
|
|
130
|
-
}
|
|
131
|
+
return sendJson(res, apiDocsPage(state, pid, harnessParam(req.url)), 200, ae);
|
|
131
132
|
}
|
|
132
133
|
if (path === '/api/jobs') return sendJson(res, { jobs: jm.recent() }, 200, ae);
|
|
133
134
|
if (path.startsWith('/api/jobs/')) {
|
|
@@ -137,9 +138,6 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
137
138
|
return j ? sendJson(res, j, 200, ae)
|
|
138
139
|
: sendJson(res, { error: 'no such job' }, 404, ae);
|
|
139
140
|
}
|
|
140
|
-
if (notPorted(path)) {
|
|
141
|
-
return sendJson(res, { error: `not ported yet: ${path}` }, 501, ae);
|
|
142
|
-
}
|
|
143
141
|
return serveStatic(res, path, ae);
|
|
144
142
|
}
|
|
145
143
|
|
|
@@ -230,9 +228,6 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
230
228
|
if (path.startsWith('/api/actions/')) {
|
|
231
229
|
return doAction(res, path.slice(path.lastIndexOf('/') + 1), readJsonBody(body), ae);
|
|
232
230
|
}
|
|
233
|
-
if (notPortedPost(path)) {
|
|
234
|
-
return sendJson(res, { error: `not ported yet: ${path}` }, 501, ae);
|
|
235
|
-
}
|
|
236
231
|
return sendJson(res, { error: 'not found' }, 404, ae);
|
|
237
232
|
}
|
|
238
233
|
|
|
@@ -253,9 +248,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
253
248
|
* it succeeded: a failed update changed nothing worth reloading and bouncing would disconnect
|
|
254
249
|
* the PWA for no reason.
|
|
255
250
|
*
|
|
256
|
-
* THEN the
|
|
257
|
-
* actions whose verb has not crossed, and letting them fall through to `actionCommands` would
|
|
258
|
-
* answer `{"error": "unknown action link"}` — the same thing a typo gets.
|
|
251
|
+
* THEN the table, which answers a 404 for any action it does not name.
|
|
259
252
|
*/
|
|
260
253
|
function doAction(res, action, body, ae) {
|
|
261
254
|
if (action === 'restore') {
|
|
@@ -284,9 +277,6 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
284
277
|
if (jid === null) return sendJson(res, { error: 'busy' }, 409, ae);
|
|
285
278
|
return sendJson(res, { job_id: jid }, 202, ae);
|
|
286
279
|
}
|
|
287
|
-
if (NOT_PORTED_ACTIONS.has(action)) {
|
|
288
|
-
return sendJson(res, { error: `not ported yet: /api/actions/${action}` }, 501, ae);
|
|
289
|
-
}
|
|
290
280
|
// Build can be re-themed/re-targeted from the UI picker; the other actions self-resolve
|
|
291
281
|
// the deployed theme downstream. Footprint, posture and mode always follow the current
|
|
292
282
|
// install, so a re-theme preserves lean/full, the register and the operating mode.
|
package/js/web/jobs.mjs
CHANGED
|
@@ -44,14 +44,10 @@
|
|
|
44
44
|
* `taskkill /T` is a machine primitive here, not this program.
|
|
45
45
|
*
|
|
46
46
|
* ---------------------------------------------------------------------------------------
|
|
47
|
-
* THE
|
|
48
|
-
*
|
|
49
|
-
* `
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `NOT_PORTED_ACTIONS` IS EMPTY AND STILL DECLARED, which is the point of it: a ninth action
|
|
53
|
-
* added later cannot quietly answer "unknown action", because `tests/unit/web_jobs.test.mjs`
|
|
54
|
-
* holds the two sets against `actionCommands`' own keys.
|
|
47
|
+
* THE ACTION SURFACE — `doctor`, `build`, `build-all`, `export`, `uninstall`, `update`,
|
|
48
|
+
* `link` and `unlink` each name a verb `bin/geneseed-cli.mjs` runs, plus the three
|
|
49
|
+
* `INLINE_ACTIONS` the dispatcher answers without the table. `tests/unit/web_jobs.test.mjs`
|
|
50
|
+
* holds the union against a written-out list, so a ninth action cannot appear unchecked.
|
|
55
51
|
*
|
|
56
52
|
* ---------------------------------------------------------------------------------------
|
|
57
53
|
* THREE DETAILS OF `_run`, EACH A COMMENT EXPLAINING A BUG IT ALREADY FIXED.
|
|
@@ -381,27 +377,10 @@ const NODE = () => process.execPath;
|
|
|
381
377
|
const CLI = () => path.join(ROOT, 'bin', 'geneseed-cli.mjs');
|
|
382
378
|
const GEN = () => path.join(ROOT, 'bin', 'build-driver.mjs');
|
|
383
379
|
|
|
384
|
-
/**
|
|
385
|
-
* The rows whose VERB has not crossed. EMPTY, AND STILL DECLARED.
|
|
386
|
-
*
|
|
387
|
-
* WHY AN EMPTY SET RATHER THAN A DELETED ONE. This set is cross-checked against
|
|
388
|
-
* `actionCommands`' own keys, and an empty set is the strongest form that check ever takes:
|
|
389
|
-
* it asserts that EVERY action this daemon knows how to dispatch on is answered by the
|
|
390
|
-
* runner. Deleting the declaration would delete that assertion, and the next unported
|
|
391
|
-
* action would come back as a silent 404 — the thing the 501 exists to prevent.
|
|
392
|
-
*
|
|
393
|
-
* A 501 AND NOT THE 404 THE TABLE WOULD PRODUCE. `actionCommands` returns `null` for an
|
|
394
|
-
* unknown action, mapped to `{"error": "unknown action <x>"}` at 404 — so a
|
|
395
|
-
* real-but-unported action falling through would answer the same thing a typo gets. With
|
|
396
|
-
* this set empty the 501 branch is UNREACHABLE by construction: every real action answers
|
|
397
|
-
* 202 and only an invented one answers 404.
|
|
398
|
-
*/
|
|
399
|
-
export const NOT_PORTED_ACTIONS = new Set();
|
|
400
|
-
|
|
401
380
|
/**
|
|
402
381
|
* The three actions the dispatcher answers INLINE, without consulting the table above:
|
|
403
382
|
* `restore` is synchronous and returns a result rather than a job id, and `install`/`deploy`
|
|
404
|
-
* resolve their argv from the request body first. Declared so the
|
|
383
|
+
* resolve their argv from the request body first. Declared so the surface check covers every
|
|
405
384
|
* action the dispatcher answers, not only the table's keys.
|
|
406
385
|
*/
|
|
407
386
|
export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
|
package/js/web/routes.mjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WHAT THE WEB API ANSWERS — as a declaration, separate from the dispatcher that honours it.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* not a dispatcher — a route can be declared
|
|
7
|
-
* a probe notices.
|
|
4
|
+
* Two tables and two inline lists. They are not documentation: `tests/unit/web_server.test.mjs`
|
|
5
|
+
* holds their union against a surface list written out in the test, and probes the real handler,
|
|
6
|
+
* because a declaration is not a dispatcher — a route can be declared here and missing from the
|
|
7
|
+
* dispatch, and nothing but a probe notices.
|
|
8
|
+
*
|
|
9
|
+
* This file used to also carry the Python→Node port's NOT_PORTED / DECLINED partition — five
|
|
10
|
+
* sets, all empty once the port finished. They were deleted when the port bookkeeping retired
|
|
11
|
+
* (see git history); an unmatched GET falls through to `serveStatic`'s SPA fallback and an
|
|
12
|
+
* unmatched POST answers 404, which is what the empty sets already produced.
|
|
8
13
|
*
|
|
9
14
|
* Split out of `server.mjs` because it is the half a reader consults to answer "does the console
|
|
10
15
|
* expose X?", which has nothing to do with sockets, daemons or gzip.
|
|
@@ -13,43 +18,6 @@ import { formatValue, isTruthy, parseJson } from '../lib/json.mjs';
|
|
|
13
18
|
import { apiExcludesMutate, apiInstallToggle, apiMcpToggle, apiMemoryDelete, apiProfileSave, apiRulesMutate, apiRulesPromote, apiSelectView } from './actions.mjs';
|
|
14
19
|
import { apiActivityToggle } from './activity.mjs';
|
|
15
20
|
|
|
16
|
-
/**
|
|
17
|
-
* The API paths this daemon does not answer — currently empty, but the DECLARATION STAYS:
|
|
18
|
-
* without it, an unmatched GET falls through to `serveStatic`'s SPA fallback (200 +
|
|
19
|
-
* index.html) instead of a clear failure. Half of a partition test, so an empty set still
|
|
20
|
-
* asserts there is nothing left to declare.
|
|
21
|
-
*
|
|
22
|
-
* Declared separately from the POST sets below because five paths — `/api/mcp`,
|
|
23
|
-
* `/api/excludes`, `/api/rules`, `/api/profile`, `/api/activity` — answer both verbs with
|
|
24
|
-
* different bodies, so one set keyed on path alone could not track GET and POST status
|
|
25
|
-
* independently.
|
|
26
|
-
*/
|
|
27
|
-
export const NOT_PORTED = new Set([]);
|
|
28
|
-
export const NOT_PORTED_PREFIXES = [];
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The POSTs that have not crossed yet — currently empty, for the same reason the GET pair
|
|
32
|
-
* above stays declared: deleting it would let an unrouted POST fall through to a
|
|
33
|
-
* plausible-looking 404 instead of the 501 this set exists to produce.
|
|
34
|
-
*/
|
|
35
|
-
export const NOT_PORTED_POST = new Set([]);
|
|
36
|
-
export const NOT_PORTED_POST_PREFIXES = [];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The POSTs that will NEVER cross, which is a different claim from the set above — currently
|
|
40
|
-
* EMPTY. `/api/pick-folder` was its one member (an OS-native folder chooser, "declined rather
|
|
41
|
-
* than deferred" because there was no Node twin that was not a new GUI dependency) until the
|
|
42
|
-
* user overrode that decision 2026-08-27; it now dispatches inline from `js/web/handler.mjs`'s
|
|
43
|
-
* `doPost` — see `apiPickFolder` in `js/web/actions.mjs`.
|
|
44
|
-
*
|
|
45
|
-
* KEPT DECLARED AND EMPTY FOR THE SAME REASON `NOT_PORTED_POST` STAYS DECLARED WHEN IT EMPTIES
|
|
46
|
-
* (see that set, just above): an empty set here is the partition asserting there is nothing
|
|
47
|
-
* PERMANENTLY refused, not an omission a later reader has to double-check. `notPortedPost`
|
|
48
|
-
* below still consults it, so a genuinely permanent decline — if one is ever added again —
|
|
49
|
-
* needs no dispatcher change, only a member here.
|
|
50
|
-
*/
|
|
51
|
-
export const DECLINED_POST = new Set([]);
|
|
52
|
-
|
|
53
21
|
/**
|
|
54
22
|
* The POSTs this daemon answers — and this table IS the dispatch, not a declaration beside
|
|
55
23
|
* it: `doPost` looks routes up HERE, so a hand-kept declaration and the running dispatcher
|
|
@@ -79,8 +47,6 @@ export const POST_ROUTES = new Map([
|
|
|
79
47
|
['/api/profile', [apiProfileSave, true]],
|
|
80
48
|
]);
|
|
81
49
|
|
|
82
|
-
export const PORTED_POST = [...POST_ROUTES.keys()];
|
|
83
|
-
|
|
84
50
|
/**
|
|
85
51
|
* The 409 column, exported so a test can assert it directly. Giving `/api/activity` the 409
|
|
86
52
|
* treatment is the invisible mutation: nothing in normal use can force the `ok: false` arm
|
|
@@ -92,59 +58,27 @@ export const POST_ROUTES_CONVENTION = Object.fromEntries(
|
|
|
92
58
|
);
|
|
93
59
|
|
|
94
60
|
/**
|
|
95
|
-
* The GET paths answered OUTSIDE the two tables, declared so the
|
|
61
|
+
* The GET paths answered OUTSIDE the two tables, declared so the surface cross-check can
|
|
96
62
|
* see them: `/api/ping` is the shell's own, and the two Docs routes take the `?harness=`
|
|
97
63
|
* query param, which a `(state)` table entry has no way to receive. A probe checks all
|
|
98
64
|
* three against the running handler too, not merely against this list.
|
|
99
65
|
*/
|
|
100
|
-
export const
|
|
66
|
+
export const GET_INLINE = ['/api/ping', '/api/docs', '/api/docs/page/',
|
|
101
67
|
'/api/jobs', '/api/jobs/'];
|
|
102
68
|
|
|
103
69
|
/**
|
|
104
70
|
* The POSTs that answer outside `POST_ROUTES`, because they don't fit its shape:
|
|
105
71
|
* `/api/jobs/<id>/cancel` answers 200 or 404 on a lookup, not an `ok` field, and
|
|
106
|
-
* `/api/actions/<x>` spans
|
|
72
|
+
* `/api/actions/<x>` spans five statuses (202, 409, 404, 400, 200) over one prefix, none
|
|
107
73
|
* read off `ok`. Bending the 409 column to fit either would make it lie about the routes it
|
|
108
|
-
* already describes correctly.
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* -> object` shape answers the instant `fn` returns, and this endpoint cannot — it spawns a
|
|
112
|
-
* native folder dialog and answers only once a human (or a timeout) closes it. See
|
|
74
|
+
* already describes correctly. `/api/reveal` answers 200/404 on an allowlist lookup, and
|
|
75
|
+
* `/api/pick-folder` cannot fit the table's `fn(state, body) -> object` shape at all: it
|
|
76
|
+
* spawns a native folder dialog and answers only once a human (or a timeout) closes it. See
|
|
113
77
|
* `apiPickFolder` in `js/web/actions.mjs`, dispatched inline from `js/web/handler.mjs`'s
|
|
114
|
-
* `doPost` the same way the other
|
|
115
|
-
*/
|
|
116
|
-
export const PORTED_POST_INLINE = ['/api/shutdown', '/api/restart', '/api/jobs/',
|
|
117
|
-
'/api/actions/', '/api/pick-folder'];
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* POST routes added after the ported reference surface was frozen. Kept separate from
|
|
121
|
-
* `NOT_PORTED_POST` so that frozen record stays honest, while a new route is still
|
|
122
|
-
* enumerated rather than appearing unchecked. Belongs here only once dispatched below — a
|
|
123
|
-
* probe, not just this declaration, is what keeps that true.
|
|
78
|
+
* `doPost` the same way the other rows here are.
|
|
124
79
|
*/
|
|
125
|
-
export const
|
|
126
|
-
'/api/reveal'
|
|
127
|
-
]);
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* GET routes added after the ported reference surface was frozen — the same reason
|
|
131
|
-
* `POST_BEYOND_REF` above exists, one verb over. `/api/recent` is a genuine gap: no catalog
|
|
132
|
-
* row carries a date and a browser cannot stat the `source` path it is handed, so "what
|
|
133
|
-
* changed lately" needed a server that looks. See `apiRecent` in `api.mjs` for what it will
|
|
134
|
-
* and will not date.
|
|
135
|
-
*/
|
|
136
|
-
export const GET_BEYOND_REF = new Set([
|
|
137
|
-
'/api/recent',
|
|
138
|
-
]);
|
|
139
|
-
|
|
140
|
-
export function notPorted(path) {
|
|
141
|
-
return NOT_PORTED.has(path) || NOT_PORTED_PREFIXES.some((p) => path.startsWith(p));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function notPortedPost(path) {
|
|
145
|
-
return NOT_PORTED_POST.has(path) || DECLINED_POST.has(path)
|
|
146
|
-
|| NOT_PORTED_POST_PREFIXES.some((p) => path.startsWith(p));
|
|
147
|
-
}
|
|
80
|
+
export const POST_INLINE = ['/api/shutdown', '/api/restart', '/api/jobs/',
|
|
81
|
+
'/api/actions/', '/api/pick-folder', '/api/reveal'];
|
|
148
82
|
|
|
149
83
|
/**
|
|
150
84
|
* The drained request body as a plain object, `{}` for ANYTHING else — never a raise, so a
|
package/js/web/server.mjs
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* WHAT WAS STILL NOT HERE, SINCE P6i UNTIL 2026-08-27: `/api/pick-folder`, DECLINED rather
|
|
19
19
|
* than deferred — an OS-native folder chooser had no Node twin that was not a new GUI
|
|
20
20
|
* dependency. The user overrode that decline; the endpoint now dispatches inline from
|
|
21
|
-
* `js/web/handler.mjs`'s `doPost` (see `apiPickFolder` in `js/web/actions.mjs`).
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* `js/web/handler.mjs`'s `doPost` (see `apiPickFolder` in `js/web/actions.mjs`). With it, the
|
|
22
|
+
* port's NOT_PORTED / DECLINED partition emptied for good and was retired — the declared
|
|
23
|
+
* surface now lives in `js/web/routes.mjs` and `tests/unit/web_server.test.mjs`.
|
|
24
24
|
*/
|
|
25
25
|
import { GLOBAL_MANIFEST, resolvePath } from '../hosts/hosts.mjs';
|
|
26
26
|
import { printOut } from '../lib/fs.mjs';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
|
|
3
3
|
"name": "geneseed",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.3.0",
|
|
5
5
|
"description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai",
|
package/registry.json
CHANGED
|
@@ -127,6 +127,20 @@
|
|
|
127
127
|
"added": "2026-06-03",
|
|
128
128
|
"last_verified": ""
|
|
129
129
|
},
|
|
130
|
+
"skills/bruno-collection-generator": {
|
|
131
|
+
"status": "experimental",
|
|
132
|
+
"version": "1.0.0",
|
|
133
|
+
"owner": "Guillaume Lemer",
|
|
134
|
+
"added": "2026-09-03",
|
|
135
|
+
"last_verified": ""
|
|
136
|
+
},
|
|
137
|
+
"skills/bruno-test-writer": {
|
|
138
|
+
"status": "experimental",
|
|
139
|
+
"version": "1.0.0",
|
|
140
|
+
"owner": "Guillaume Lemer",
|
|
141
|
+
"added": "2026-09-03",
|
|
142
|
+
"last_verified": ""
|
|
143
|
+
},
|
|
130
144
|
"skills/clarify": {
|
|
131
145
|
"status": "approved",
|
|
132
146
|
"version": "1.0.0",
|
package/src/AGENT.md.tmpl
CHANGED
|
@@ -11,12 +11,19 @@ delegate to, the **{{SKILLS}}** it can run, how it keeps **{{MEMORY}}**, the
|
|
|
11
11
|
**{{NOTEBOOK}}** it keeps as a space of its own, and how it draws on the surrounding
|
|
12
12
|
**{{VAULT}}**.
|
|
13
13
|
|
|
14
|
+
<!-- LEAN:begin -->
|
|
14
15
|
It is tool-agnostic: it works with any assistant that reads a root instructions
|
|
15
16
|
file. This entrypoint is named `AGENT.md`; a tool that only auto-loads `AGENTS.md`
|
|
16
17
|
or `CLAUDE.md` just needs to be pointed here — rename or symlink this file to that
|
|
17
18
|
name, or reference it from the tool's config. Where a tool supports sub-agents,
|
|
18
19
|
the {{AGENTS}} below are dispatched as real sub-agents; where it does not, treat
|
|
19
20
|
each as a *persona* the single agent adopts for that slice of work.
|
|
21
|
+
<!-- LEAN:else -->
|
|
22
|
+
It is tool-agnostic: any assistant that reads a root instructions file can be
|
|
23
|
+
pointed here. Where the tool supports sub-agents, the {{AGENTS}} below are
|
|
24
|
+
dispatched as real sub-agents; where it does not, treat each as a *persona* the
|
|
25
|
+
single agent adopts for that slice of work.
|
|
26
|
+
<!-- LEAN:end -->
|
|
20
27
|
|
|
21
28
|
**Readiness sigil — required.** Before your first reply, complete the startup
|
|
22
29
|
ritual: read this file and its {{LAWS}}, then **load the project context** — if a
|
|
@@ -32,6 +39,16 @@ it certifies you have loaded the {{LAWS}} *and* the project context.
|
|
|
32
39
|
the law *numbers*, file paths, and folder names stay plain English so tooling and links
|
|
33
40
|
never move.
|
|
34
41
|
|
|
42
|
+
**Precedence.** Stated once, here; nothing below restates it.
|
|
43
|
+
|
|
44
|
+
| Rank | Source | Against the rank above it |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| 1 | The {{LAWS}} (§1) | — |
|
|
47
|
+
| 2 | `user-rules.md` (§1) | may tighten a {{LAW}}, never repeal one |
|
|
48
|
+
| 3 | The {{ONTOLOGY}} | never against a user rule |
|
|
49
|
+
| 4 | The active {{DOCTRINES}} (§2) | may tighten, never repeal |
|
|
50
|
+
| 5 | `PROFILE.md`, posture, mode, theme | inform only; never bind |
|
|
51
|
+
|
|
35
52
|
---
|
|
36
53
|
|
|
37
54
|
## {{ONTOLOGY}}
|
|
@@ -69,10 +86,7 @@ posture: posture is the relationship register, mode is the operating register.
|
|
|
69
86
|
|
|
70
87
|
**The user's own rules.** If a `user-rules.md` sits beside this file, read it at
|
|
71
88
|
session start and obey every rule in it with the same force as the {{LAWS}} above
|
|
72
|
-
— always, in every task
|
|
73
|
-
where the two conflict, the {{LAW}} wins. Against a {{DOCTRINE}} rule the user's
|
|
74
|
-
file wins outright — a practice pack chosen at build time never overrides an
|
|
75
|
-
instruction the user wrote for their own repo. That file belongs
|
|
89
|
+
— always, in every task (its rank is in the precedence table above). That file belongs
|
|
76
90
|
to the user — the build seeds it once, never overwrites it, and no update touches
|
|
77
91
|
it — and it is the **only** place standing rules are ever written: when the user
|
|
78
92
|
asks for a standing rule, or a recurring {{MEMORY}} lesson has earned promotion,
|
|
@@ -84,10 +98,7 @@ update, and an edit to either is silently lost.
|
|
|
84
98
|
**The user's profile.** If a `PROFILE.md` sits beside this file, read it at session
|
|
85
99
|
start and let it colour how you work — who the user is, how they work, the register
|
|
86
100
|
they prefer. Unlike `user-rules.md`, it is *identity, not rules*: it never binds,
|
|
87
|
-
it only informs.
|
|
88
|
-
first, then `user-rules.md`, then the active {{DOCTRINES}} (§2), then this
|
|
89
|
-
profile; where a preference here meets a rule, a {{DOCTRINE}} rule or a {{LAW}},
|
|
90
|
-
the rule wins. It too belongs to the user: seeded once, never overwritten. Never
|
|
101
|
+
it only informs. It too belongs to the user: seeded once, never overwritten. Never
|
|
91
102
|
write standing rules into it — those go to `user-rules.md`. When the file is still
|
|
92
103
|
the untouched stub — or the user asks — the [profile]({{DIR_SKILLS}}/profile.md)
|
|
93
104
|
{{SKILL}} builds or refreshes it through a short interview, with their consent.
|
|
@@ -99,9 +110,7 @@ the untouched stub — or the user asks — the [profile]({{DIR_SKILLS}}/profile
|
|
|
99
110
|
*{{EPI_DOCTRINES}}*
|
|
100
111
|
|
|
101
112
|
Practices, not principles: how work is done here, chosen by this repository at
|
|
102
|
-
build time.
|
|
103
|
-
— a {{DOCTRINE}} rule may *tighten* a {{LAW}}, never repeal one, and a user rule
|
|
104
|
-
outranks it — and only the packs enabled for this install appear
|
|
113
|
+
build time. Only the packs enabled for this install appear
|
|
105
114
|
below. The packs that are **not** active still ship as files under
|
|
106
115
|
`{{DIR_DOCTRINES}}/`, so you can read the alternatives and so a rule here that
|
|
107
116
|
cites a rule in an inactive pack is still resolvable. Change the set by
|
|
@@ -214,6 +223,8 @@ subset that productively clashes on a given motion.
|
|
|
214
223
|
| [refactor]({{DIR_SKILLS}}/refactor.md) | improving code structure without changing behaviour |
|
|
215
224
|
| [ponytail]({{DIR_SKILLS}}/ponytail.md) | the user wants the minimal, laziest-that-works solution — "ponytail", "be lazy", "simplest path", or a complaint about over-engineering, bloat, or needless dependencies |
|
|
216
225
|
| [forge-mcp]({{DIR_SKILLS}}/forge-mcp.md) | building, extending, or hardening a Model Context Protocol (MCP) server |
|
|
226
|
+
| [bruno-collection-generator]({{DIR_SKILLS}}/bruno-collection-generator.md) | creating or converting a Bruno API collection — requests, environments, docs — from routes, an OpenAPI snippet, curl commands, or an endpoint list |
|
|
227
|
+
| [bruno-test-writer]({{DIR_SKILLS}}/bruno-test-writer.md) | writing or strengthening Bruno tests, assertions, scripts, and request chaining for a request or collection |
|
|
217
228
|
| [prose]({{DIR_SKILLS}}/prose.md) | writing prose for a human — a commit body, PR, doc, comment, memory note, or reply — that should not read like a model wrote it |
|
|
218
229
|
| [frontend-design]({{DIR_SKILLS}}/frontend-design.md) | building or reshaping a UI that should carry a deliberate point of view, not a templated default |
|
|
219
230
|
| [handoff]({{DIR_SKILLS}}/handoff.md) | context filling, or passing work to another agent or session |
|
|
@@ -229,20 +240,20 @@ subset that productively clashes on a given motion.
|
|
|
229
240
|
| [opencode-theme]({{DIR_SKILLS}}/opencode-theme.md) | the user wants their own OpenCode colour theme — pick or tweak a palette, get solid + transparent variants that survive rebuilds |
|
|
230
241
|
<!-- CATALOG:else -->
|
|
231
242
|
Your host lists every {{SKILL}} it can invoke, with its trigger, in your own
|
|
232
|
-
inventory — that list is the catalogue and it is authoritative.
|
|
233
|
-
{{
|
|
234
|
-
|
|
243
|
+
inventory — that list is the catalogue and it is authoritative. Each written
|
|
244
|
+
procedure is in [`{{DIR_SKILLS}}/`]({{DIR_SKILLS}}/) and the host loads it on
|
|
245
|
+
demand.
|
|
235
246
|
<!-- CATALOG:end -->
|
|
236
247
|
|
|
237
248
|
When a task repeats and no {{SKILL}} covers it, forge one: copy
|
|
238
249
|
[`{{DIR_SKILLS}}/_template.md`]({{DIR_SKILLS}}/_template.md), keep it to one coherent
|
|
239
|
-
domain ({{DOCTRINE}} craft 1), and register it in
|
|
250
|
+
domain ({{DOCTRINE}} craft 1), and register it in the catalogue above. Every {{SKILL}} ends with a
|
|
240
251
|
**self-improvement loop** — honour it: fold what a run teaches back into the
|
|
241
252
|
file, or into {{MEMORY}} ({{DOCTRINE}} process 1).
|
|
242
253
|
|
|
243
254
|
**Folder {{SKILLS}}.** Some {{SKILLS}} ship as self-contained folders under
|
|
244
255
|
`{{DIR_SKILLS}}/`, each with its own format and provenance (`VENDOR.md`) —
|
|
245
|
-
third-party vendors plus first-party ones bundling scripts. Not in the
|
|
256
|
+
third-party vendors plus first-party ones bundling scripts. Not in the catalogue
|
|
246
257
|
above — reach for one only when its domain comes up:
|
|
247
258
|
- [react-view-transitions]({{DIR_SKILLS}}/react-view-transitions/SKILL.md) — animating
|
|
248
259
|
React UIs with the View Transition API (`<ViewTransition>`, shared elements, Next.js).
|
|
@@ -278,6 +289,7 @@ convention: [`{{DIR_MEMORY}}/README.md`]({{DIR_MEMORY}}/README.md).
|
|
|
278
289
|
|
|
279
290
|
*{{EPI_NOTEBOOK}}*
|
|
280
291
|
|
|
292
|
+
<!-- LEAN:begin -->
|
|
281
293
|
{{INTRO_NOTEBOOK}} It lives in [`{{DIR_NOTEBOOK}}/`]({{DIR_NOTEBOOK}}/). This is
|
|
282
294
|
**not** {{MEMORY}}: {{MEMORY}} (§5) is a curated store of durable *facts*, one per
|
|
283
295
|
file in a fixed format, written for recall. The {{NOTEBOOK}} has **no imposed
|
|
@@ -299,6 +311,19 @@ format: {{MEMORY}} holds durable insight for future sessions, the {{NOTEBOOK}}
|
|
|
299
311
|
holds your working artifacts. The one fixed law: `{{DIR_NOTEBOOK}}/` is **personal and
|
|
300
312
|
git-ignored** — never committed or shared, and the `.gitignore` that guarantees
|
|
301
313
|
it is not yours to lift.
|
|
314
|
+
<!-- LEAN:else -->
|
|
315
|
+
{{INTRO_NOTEBOOK}} It lives in [`{{DIR_NOTEBOOK}}/`]({{DIR_NOTEBOOK}}/), with
|
|
316
|
+
**no imposed format and no imposed medium** — notes, plans, scratch code,
|
|
317
|
+
datasets, experiments. It is not {{MEMORY}} (§5): {{MEMORY}} holds curated
|
|
318
|
+
durable facts for recall, the {{NOTEBOOK}} holds your working artifacts, and a
|
|
319
|
+
note that hardens into a durable fact is promoted there ({{DOCTRINE}} process 1).
|
|
320
|
+
Any file you create for your own benefit belongs here, never in the host tree.
|
|
321
|
+
Read `NOTEBOOK.md` at the start of a session to recall what you keep; the
|
|
322
|
+
charter at [`{{DIR_NOTEBOOK}}/README.md`]({{DIR_NOTEBOOK}}/README.md) is yours
|
|
323
|
+
to rewrite. The one fixed law: `{{DIR_NOTEBOOK}}/` is **personal and
|
|
324
|
+
git-ignored** — never committed or shared, and the `.gitignore` that guarantees
|
|
325
|
+
it is not yours to lift.
|
|
326
|
+
<!-- LEAN:end -->
|
|
302
327
|
|
|
303
328
|
---
|
|
304
329
|
|
|
@@ -331,6 +356,7 @@ scratch scripts, analysis dumps, drafts, experiments — are made in the
|
|
|
331
356
|
|
|
332
357
|
*{{EPI_WIKI}}*
|
|
333
358
|
|
|
359
|
+
<!-- LEAN:begin -->
|
|
334
360
|
{{INTRO_WIKI}} It is declared in `wiki.jsonc`, **beside this AGENT.md** like
|
|
335
361
|
`context.json` (§9) — or wherever `$GENESEED_WIKI` points. An empty `wikis` list
|
|
336
362
|
means none is declared and this section stays dormant. (On OpenCode its `eager`
|
|
@@ -358,6 +384,19 @@ outside `protected`, but its conventions outrank your defaults: wikilinks
|
|
|
358
384
|
durable fact worth sharing across projects goes here via the
|
|
359
385
|
[wiki]({{DIR_SKILLS}}/wiki.md) {{SKILL}}; one that matters only to you stays in
|
|
360
386
|
{{MEMORY}} (§5).
|
|
387
|
+
<!-- LEAN:else -->
|
|
388
|
+
{{INTRO_WIKI}} It is declared in `wiki.jsonc`, **beside this AGENT.md** — or
|
|
389
|
+
wherever `$GENESEED_WIKI` points; an empty `wikis` list means this section
|
|
390
|
+
stays dormant. Read `wiki.jsonc` at the start of every session and honour each
|
|
391
|
+
declared wiki: load its `entries` by their `load` mode exactly as in
|
|
392
|
+
`context.json` (§9); read its `conventions` before your first write there and
|
|
393
|
+
follow them over your own habits; drop a note you cannot place into `inbox`
|
|
394
|
+
rather than guess a home; never write inside `protected`. The wiki is **the
|
|
395
|
+
user's**, not yours — wikilinks (`[[Note Name]]`) over bare paths, every new
|
|
396
|
+
note linked into the graph. A durable fact worth sharing across projects goes
|
|
397
|
+
here via the [wiki]({{DIR_SKILLS}}/wiki.md) {{SKILL}}; one that matters only to
|
|
398
|
+
you stays in {{MEMORY}} (§5).
|
|
399
|
+
<!-- LEAN:end -->
|
|
361
400
|
|
|
362
401
|
---
|
|
363
402
|
|
package/src/agents/explorer.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
it, and any open questions. Never the full contents of what was read.
|
|
33
33
|
- If the search comes up empty, report what was searched and where, and answer
|
|
34
34
|
not-found — a confident wrong location costs more than an honest blank
|
|
35
|
-
({{LAW}}
|
|
35
|
+
({{LAW}} III).
|
|
36
36
|
|
|
37
37
|
## Pipeline role
|
|
38
38
|
|
package/src/doctrines/README.md
CHANGED
|
@@ -27,9 +27,9 @@ catalogue ships in every bundle** — all four pack files are installed whether
|
|
|
27
27
|
their pack is built into `AGENT.md` — so a cross-pack reference is always readable
|
|
28
28
|
even when the pack it names is not active.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
A pack changes which *practices* are in force, never the principles: a rule may
|
|
31
|
+
tighten an invariant, never repeal one. The full precedence order is stated once, in
|
|
32
|
+
the table at the top of `AGENT.md.tmpl`.
|
|
33
33
|
|
|
34
34
|
To add a pack, drop a `<name>.md` here (body only, no top-level heading — it is
|
|
35
35
|
inlined under `AGENT.md`'s own Doctrines heading), open it with a bold pack name and
|
package/src/doctrines/rigor.md
CHANGED
|
@@ -50,3 +50,16 @@ hour it is needed, so read the reference at the moment of checking. Where a rule
|
|
|
50
50
|
in two directions, the gate must be able to fail in both, or it is satisfied by excess.
|
|
51
51
|
Where {{DOCTRINE}} rigor 3 has you write the test, this governs whether the test is
|
|
52
52
|
worth trusting ({{DOCTRINE}} rigor 2).
|
|
53
|
+
|
|
54
|
+
### {{DOCTRINE}} rigor 5 — {{DOC_RIGOR_5}}
|
|
55
|
+
A gate you build stands outside the mind it governs. Wherever a system lets an
|
|
56
|
+
agent — this one or another — act on the world, approval is asked of a party the
|
|
57
|
+
agent cannot satisfy alone, the allowlist is enforced at the boundary the call must
|
|
58
|
+
cross, and validation runs against real state, not the model's account of it.
|
|
59
|
+
Permission must never live in the governed agent's own prompt or judgement. Anything
|
|
60
|
+
gaining autonomous control over physical state, money, or production ships disarmed,
|
|
61
|
+
armed only by explicit human act. A rule in a prompt is a request; a rule at a
|
|
62
|
+
boundary is a constraint — and only the second holds when the model is wrong,
|
|
63
|
+
confused, or steered by untrusted input ({{LAW}} VI). Where {{LAW}} VII governs the
|
|
64
|
+
power you take, this governs the power you hand out; {{DOCTRINE}} rigor 4 proves the
|
|
65
|
+
gate once it stands.
|