geneseed 3.14.0 → 3.15.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 +54 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +23 -9
- package/adapters/opencode/plugins/geneseed-guard.js +16 -4
- package/adapters/opencode/plugins/geneseed-learn.js +37 -11
- package/adapters/opencode/plugins/geneseed-notify.js +37 -19
- package/adapters/opencode/plugins/geneseed-workflow.js +3 -1
- package/adapters/opencode/workflows/_runtime.js +16 -3
- package/bin/geneseed-cli.mjs +22 -20
- package/bin/geneseed-hook.mjs +6 -3
- package/harness.config.json +1 -1
- package/js/README.md +9 -5
- package/js/build/args.mjs +4 -3
- package/js/build/bundle.mjs +10 -10
- package/js/build/driver.mjs +8 -7
- package/js/build/emit-claude.mjs +3 -2
- package/js/build/emit-common.mjs +2 -14
- package/js/build/emit-opencode.mjs +12 -8
- package/js/build/generate.mjs +15 -11
- package/js/build/render.mjs +1 -1
- package/js/build/source.mjs +6 -6
- package/js/build/stubs.mjs +6 -5
- package/js/build/version.mjs +21 -2
- package/js/hosts/hooks-context.mjs +2 -1
- package/js/hosts/hooks-prims.mjs +2 -2
- package/js/hosts/hosts.mjs +24 -26
- package/js/hosts/installs.mjs +33 -14
- package/js/hosts/link.mjs +75 -26
- package/js/hosts/memory-files.mjs +13 -4
- package/js/hosts/native.mjs +106 -48
- package/js/hosts/opencode.mjs +44 -36
- package/js/hosts/shim.mjs +4 -2
- package/js/inspect/diff.mjs +5 -6
- package/js/inspect/excludes.mjs +10 -5
- package/js/inspect/status.mjs +18 -21
- package/js/inspect/validate.mjs +2 -2
- package/js/lib/fs.mjs +12 -11
- package/js/lib/json.mjs +4 -4
- package/js/lib/paths.mjs +27 -33
- package/js/lib/proc.mjs +3 -3
- package/js/lib/prompt.mjs +132 -0
- package/js/lib/text.mjs +18 -4
- package/js/lib/udiff.mjs +3 -2
- package/js/maintain/memory.mjs +5 -6
- package/js/maintain/migrate.mjs +4 -3
- package/js/maintain/setup.mjs +9 -131
- package/js/maintain/uninstall.mjs +10 -9
- package/js/maintain/update.mjs +115 -34
- package/js/ui/anim.mjs +8 -12
- package/js/ui/cli.mjs +29 -35
- package/js/ui/menu.mjs +7 -13
- package/js/ui/tui.mjs +14 -123
- package/js/web/actions.mjs +82 -386
- package/js/web/activity.mjs +1 -1
- package/js/web/api.mjs +33 -717
- package/js/web/catalog.mjs +527 -0
- package/js/web/daemon.mjs +21 -3
- package/js/web/handler.mjs +51 -27
- package/js/web/jobs.mjs +24 -2
- package/js/web/routes.mjs +61 -6
- package/js/web/server.mjs +4 -4
- package/js/web/state.mjs +138 -0
- package/js/web/user-files.mjs +339 -0
- package/package.json +1 -1
- package/web/dist/assets/{Activity-CjRqZnr-.js → Activity-kpWxd70F.js} +1 -1
- package/web/dist/assets/{ActivityDetail-OQTxKBj2.js → ActivityDetail-Ccr942VL.js} +1 -1
- package/web/dist/assets/Dashboard-D1lE7jcg.js +2 -0
- package/web/dist/assets/{Docs-CvOZ8akL.js → Docs-BHyaggFy.js} +3 -3
- package/web/dist/assets/{GreenhouseView-6KDjX8da.js → GreenhouseView-Sc_UDMOE.js} +1 -1
- package/web/dist/assets/Installs-Gw-pJ3Zi.js +2 -0
- package/web/dist/assets/JournalView-_gxpsxXi.js +1 -0
- package/web/dist/assets/{Laws-Du0-Zyp2.js → Laws-S9fq1r9T.js} +1 -1
- package/web/dist/assets/{Library-DOkO0TtB.js → Library-MBxadRzn.js} +1 -1
- package/web/dist/assets/{Loops-Dv_aLByJ.js → Loops-Da_JhlzM.js} +2 -2
- package/web/dist/assets/{OperatorHudView-BPP1dtLz.js → OperatorHudView--TkvDybH.js} +1 -1
- package/web/dist/assets/{Personal-26VPYkK0.js → Personal-CTXKHVbV.js} +1 -1
- package/web/dist/assets/Tabs-B0cdf8k6.js +1 -0
- package/web/dist/assets/api-CxgfYu18.js +1 -0
- package/web/dist/assets/drawable-BbWQxe82.js +1 -0
- package/web/dist/assets/index-ujKG1ZNf.js +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +0 -2
- package/web/dist/assets/Installs-MWuhNjSm.js +0 -2
- package/web/dist/assets/JournalView-C8MrOz2c.js +0 -1
- package/web/dist/assets/Tabs-BzKuFskD.js +0 -1
- package/web/dist/assets/api-D_ixdyEY.js +0 -1
- package/web/dist/assets/drawable-zVVDbsyw.js +0 -1
- package/web/dist/assets/index-KHt3wE_K.js +0 -2
package/js/README.md
CHANGED
|
@@ -129,6 +129,7 @@ into files users have committed, so change the test that states it in the same c
|
|
|
129
129
|
| `text.mjs` | String primitives: `codePointLength`, `WHITESPACE`, `parseIntStrict`, `percentDecode` |
|
|
130
130
|
| `udiff.mjs` | `geneseed diff`'s own difflib clone — the alignment IS the printed hunks — plus `splitLines` |
|
|
131
131
|
| `proc.mjs` | One constant: the `windowsHide` flag every spawn in `js/` must reuse |
|
|
132
|
+
| `prompt.mjs` | The line-mode stdin readers — `ask`, `promptLine`, `confirm`, `askChoice` — read a byte at a time so they stop where Enter stops. Shared by `setup`, `uninstall` and `web` without dragging in the wizard |
|
|
132
133
|
| `node-floor.mjs` | The first import of all three binaries: below Node 22.3, one sentence on stderr and exit 1 (0 in the hook) instead of a stack trace |
|
|
133
134
|
|
|
134
135
|
**Before editing:** parse with `parseJson`, never `JSON.parse` — the latter collapses int `1` and
|
|
@@ -202,10 +203,13 @@ the deployed harness as JSON.
|
|
|
202
203
|
| module | owns |
|
|
203
204
|
|---|---|
|
|
204
205
|
| `server.mjs` | `serve()`/`cmdWeb`: UI-build precheck, socket bind, token mint, daemon record wiring |
|
|
205
|
-
| `routes.mjs` |
|
|
206
|
-
| `handler.mjs` | One function per request: dispatch, gzip, Host/CSRF guards, static SPA fallback |
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
206
|
+
| `routes.mjs` | The route tables — `STATE_ROUTES`, `PREFIX_ROUTES`, `POST_ROUTES` (+ its 409 column) — and the inline lists |
|
|
207
|
+
| `handler.mjs` | One function per request: dispatch, gzip, Host/CSRF guards and body cap (before the body is read), static SPA fallback |
|
|
208
|
+
| `state.mjs` | `webState` — the resolved install view every endpoint reads, with its cached inventory and doctor |
|
|
209
|
+
| `catalog.mjs` | The Library read model: inventory, constitution tiers, memory/notebook/wiki/config items, `apiCatalog`/`apiItem`, `NotFound` |
|
|
210
|
+
| `api.mjs` | The dashboard and settings reads: overview, recent, themes, setup, doctor, diff, installs, excludes, loops |
|
|
211
|
+
| `user-files.mjs` | The fingerprint-guarded editors for `user-rules.md` (incl. memory promotion) and `PROFILE.md`, read and write |
|
|
212
|
+
| `actions.mjs` | The other mutating endpoints — memory delete, MCP, excludes, installs, view, restore — plus install/deploy argv |
|
|
209
213
|
| `daemon.mjs` | Detached web start/stop/status/restart, the `.geneseed-web.json` record, and `openUrl` |
|
|
210
214
|
| `jobs.mjs` | `JobManager` and the action→argv table. The only module here that spawns job children |
|
|
211
215
|
| `docs.mjs` | The Docs tab: the `docs/<folder>/` page registry, `?harness=` filtering, link rewriting, the six page kinds |
|
|
@@ -232,4 +236,4 @@ and inline lists are held against a surface list written out in
|
|
|
232
236
|
| a hook verb | `js/hosts/hooks.mjs` (a gate) or its own `js/hosts/hooks-<verb>.mjs` | `claudeHookGroups` in `settings.mjs`, the `VERBS` row (with its `load`) in `bin/geneseed-hook.mjs`. Keep the import graph tiny |
|
|
233
237
|
| a console doc page | `docs/<understand|guides|concepts|reference>/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
|
|
234
238
|
| an OpenCode plugin | `adapters/opencode/plugins/geneseed-<name>.js` | `docs/extending.md` §4a — the `geneseed-` prefix is mechanically load-bearing |
|
|
235
|
-
| to change a `js/lib/` primitive | `tests/
|
|
239
|
+
| to change a `js/lib/` primitive | `tests/unit/lib_primitives.test.mjs` **first** | the docblock says which Node default it deliberately departs from — change that sentence too, or the next reader restores the default |
|
package/js/build/args.mjs
CHANGED
|
@@ -24,8 +24,8 @@ const EMITS = ['files', 'opencode', 'opencode-global', 'claude', 'claude-global'
|
|
|
24
24
|
*
|
|
25
25
|
* The corrupt-file branch is reproduced including its imprecision: the warning says only
|
|
26
26
|
* "using theme 'neutral'", but the branch also resets posture and mode. Matching the
|
|
27
|
-
* Python text
|
|
28
|
-
* byte-for-byte by
|
|
27
|
+
* Python text mattered more than fixing it here, because the two CLIs' stderr was compared
|
|
28
|
+
* byte-for-byte by the former Python golden harness; nothing pins the wording now.
|
|
29
29
|
*/
|
|
30
30
|
export function configDefaults() {
|
|
31
31
|
const d = {
|
|
@@ -325,7 +325,8 @@ export function parseArgs(argv, defaults) {
|
|
|
325
325
|
* `--validate-only` reads exactly the flags `build.py`'s parser already produced, so the port
|
|
326
326
|
* hands the CLI this parser rather than a second one beside it. That is not tidiness: a
|
|
327
327
|
* hand-rolled copy would have its own `--target` alias, its own `choices` lists and its own
|
|
328
|
-
* `-h`, and `
|
|
328
|
+
* `-h`, and `tests/unit/node_driver.test.mjs`'s '--help names every flag the parser takes' gates only
|
|
329
|
+
* this one.
|
|
329
330
|
*
|
|
330
331
|
* `withPlatformNewlines` because the refusal and `-h` paths WRITE: called from `bin/build-driver.mjs`
|
|
331
332
|
* they sit inside `main`'s funnel, and called from the CLI binary they would not.
|
package/js/build/bundle.mjs
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
15
|
-
import { copyFile, readText, writeText } from '../lib/fs.mjs';
|
|
15
|
+
import { copyFile, isDir, isFile, readText, writeText } from '../lib/fs.mjs';
|
|
16
16
|
import { jsonDumpsIndent, parseJson, formatReprAscii, isTruthy } from '../lib/json.mjs';
|
|
17
|
-
import { OWNED_SRC_DIRS, SRC_DIRS_MARKER, TMPL_SPEC_RE
|
|
17
|
+
import { OWNED_SRC_DIRS, SRC_DIRS_MARKER, TMPL_SPEC_RE } from './emit-common.mjs';
|
|
18
18
|
import { SRC_DIR_TOKENS, renderAll } from './render.mjs';
|
|
19
19
|
import {
|
|
20
20
|
ensureBundleGitignore, ensureContextStub, ensureMemoryIndex, ensureNotebookIndex,
|
|
@@ -114,16 +114,9 @@ function safePriorDirName(out, priorName) {
|
|
|
114
114
|
// build
|
|
115
115
|
// ---------------------------------------------------------------------------
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
* `_build_render.build` — render the bundle into `out`.
|
|
119
|
-
*
|
|
120
|
-
* `out` arrives as the STRING Python already resolved, and is echoed into the progress
|
|
121
|
-
* lines verbatim: Python interpolates `str(Path)`, which is backslash-separated on
|
|
122
|
-
* Windows, and re-deriving it here would differ by separator in every message.
|
|
123
|
-
*/
|
|
124
117
|
/**
|
|
125
118
|
* THE PHASE BOUNDARY MARKER — T8, and the whole of what this port owes
|
|
126
|
-
* `tests/
|
|
119
|
+
* `tests/unit/emit_phase_order.test.mjs`.
|
|
127
120
|
*
|
|
128
121
|
* Every emit runs its stages in one order: RENDER* → WIRE* → PRUNE → MANIFEST → VERIFY. RENDER
|
|
129
122
|
* writes files Geneseed owns wholesale; WIRE reconciles files the USER co-owns (the
|
|
@@ -156,6 +149,13 @@ export function phaseLog(phase) {
|
|
|
156
149
|
process.stderr.write(`[geneseed:phase] ${phase}\n`);
|
|
157
150
|
}
|
|
158
151
|
|
|
152
|
+
/**
|
|
153
|
+
* `_build_render.build` — render the bundle into `out`.
|
|
154
|
+
*
|
|
155
|
+
* `out` arrives as the STRING Python already resolved, and is echoed into the progress
|
|
156
|
+
* lines verbatim: Python interpolates `str(Path)`, which is backslash-separated on
|
|
157
|
+
* Windows, and re-deriving it here would differ by separator in every message.
|
|
158
|
+
*/
|
|
159
159
|
export function build(cfg, themeName, out, { footprint = 'full', nativeCatalog = false } = {}) {
|
|
160
160
|
phaseLog('RENDER');
|
|
161
161
|
const { theme, items } = renderAll(cfg, themeName, { footprint, nativeCatalog });
|
package/js/build/driver.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { settingsIntegrityCheck } from '../hosts/settings.mjs';
|
|
|
42
42
|
import { hookRunnerEntry } from '../hosts/shim.mjs';
|
|
43
43
|
import { writeText, withPlatformNewlines, isFile } from '../lib/fs.mjs';
|
|
44
44
|
import { parseJson, jsonDumpsIndent } from '../lib/json.mjs';
|
|
45
|
+
import { relPosix } from '../lib/text.mjs';
|
|
45
46
|
// P5c moved these out of this file: `bin/geneseed-cli.mjs` needs the same four resolvers to
|
|
46
47
|
// find a global install, and a resolver that decides WHERE a driver writes is the last thing
|
|
47
48
|
// that should exist twice. golden.py's 259 cells are what made the move safe to attempt.
|
|
@@ -90,7 +91,8 @@ const PRIMARY_AGENT_SRC = path.join(ROOT, 'adapters', 'opencode', 'agents', 'orc
|
|
|
90
91
|
* It is invisible on a machine whose paths are already canonical, which is every machine
|
|
91
92
|
* this ran on until GitHub's Windows runner handed it a `C:\\Users\\RUNNER~1\\...` and the
|
|
92
93
|
* two CLIs printed the same directory under two different names — a 70-byte difference in a
|
|
93
|
-
* line compared BYTE for byte
|
|
94
|
+
* line the former Python suite compared BYTE for byte; `tests/unit/resolve_out.test.mjs` pins
|
|
95
|
+
* it now. The
|
|
94
96
|
* argument to `resolvePath` is made absolute first so its `expanduser` cannot fire: the
|
|
95
97
|
* reference does not expand `~` here, and a bare `~` is a legal directory name. */
|
|
96
98
|
export function resolveOut(raw) {
|
|
@@ -149,10 +151,10 @@ function warnBobGlobalOverProject() {
|
|
|
149
151
|
* would be written into `opencode.json`'s instruction path.
|
|
150
152
|
*/
|
|
151
153
|
function relUnder(out, root) {
|
|
152
|
-
const rel =
|
|
154
|
+
const rel = relPosix(root, out);
|
|
153
155
|
if (rel === '' || rel === '.') return '';
|
|
154
156
|
if (rel.startsWith('..') || path.isAbsolute(rel)) return '';
|
|
155
|
-
return rel
|
|
157
|
+
return rel;
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
/**
|
|
@@ -255,7 +257,7 @@ function projectSurvivors(emitName) {
|
|
|
255
257
|
* RENDER and WIRE already ran in Node before P4; what lived only in Python was the stage
|
|
256
258
|
* either side of them: reading the previous manifest (PRE), pruning what this layer no
|
|
257
259
|
* longer owns, writing the manifest atomically, and the summary line. All five stages now
|
|
258
|
-
* run in one process, in the order `
|
|
260
|
+
* run in one process, in the order `tests/unit/emit_phase_order.test.mjs` pins:
|
|
259
261
|
* RENDER -> WIRE -> PRUNE -> MANIFEST -> VERIFY (opencode has no VERIFY; it writes no
|
|
260
262
|
* settings file of its own).
|
|
261
263
|
*/
|
|
@@ -511,9 +513,8 @@ const claudeShaped = (name) => (cfg, args, out) => {
|
|
|
511
513
|
|
|
512
514
|
/**
|
|
513
515
|
* build.py:437-466 — the POST stage, which writes markers and records the install and
|
|
514
|
-
* wires NOTHING. `
|
|
515
|
-
*
|
|
516
|
-
* the same reason.
|
|
516
|
+
* wires NOTHING. `tests/unit/emit_phase_order.test.mjs`'s 'the post-emit marker stage wires
|
|
517
|
+
* nothing' classifies it, and that is why it stays after the dispatch.
|
|
517
518
|
*
|
|
518
519
|
* `writeText`, never `writeFileSync`: Python's `Path.write_text` opens in text mode with
|
|
519
520
|
* `newline=None` and translates the trailing `\n` to CRLF on Windows. A marker written
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -14,10 +14,11 @@ import {
|
|
|
14
14
|
unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
|
|
15
15
|
} from '../hosts/settings.mjs';
|
|
16
16
|
import { writeText } from '../lib/fs.mjs';
|
|
17
|
-
import { isTruthy } from '../lib/json.mjs';
|
|
17
|
+
import { get, isDict, isTruthy } from '../lib/json.mjs';
|
|
18
|
+
import { relPosix } from '../lib/text.mjs';
|
|
18
19
|
import { assertSourceComplete, phaseLog } from './bundle.mjs';
|
|
19
20
|
import {
|
|
20
|
-
|
|
21
|
+
globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
|
|
21
22
|
} from './emit-common.mjs';
|
|
22
23
|
import { renderAll, renderFile } from './render.mjs';
|
|
23
24
|
import {
|
package/js/build/emit-common.mjs
CHANGED
|
@@ -10,18 +10,11 @@
|
|
|
10
10
|
* `emit-claude.mjs` do that; `bundle.mjs` renders the bundle they read from.
|
|
11
11
|
*/
|
|
12
12
|
import path from 'node:path';
|
|
13
|
-
import { copyFile, writeText,
|
|
14
|
-
import {
|
|
13
|
+
import { copyFile, writeText, isDir } from '../lib/fs.mjs';
|
|
14
|
+
import { relPosix } from '../lib/text.mjs';
|
|
15
15
|
import { SRC_DIR_TOKENS, destRel } from './render.mjs';
|
|
16
16
|
import { mkdirSync, readdirSync, cpSync } from 'node:fs';
|
|
17
17
|
|
|
18
|
-
// `isFile`/`isDir`/`get`/`isDict` are owned by `js/lib` now (single owner across `hosts/`,
|
|
19
|
-
// `build/`, `inspect/` and `web/` — layering forbids `build/` reaching into `inspect/scan.mjs`
|
|
20
|
-
// for them, so `js/lib` is the one home both sides can reach). Re-exported here so this
|
|
21
|
-
// module's own existing callers (`emit-claude.mjs`, `emit-opencode.mjs`, `version.mjs`,
|
|
22
|
-
// `bundle.mjs`) keep importing them from `./emit-common.mjs` unchanged.
|
|
23
|
-
export { isFile, isDir, get, isDict };
|
|
24
|
-
|
|
25
18
|
/** `_build_render.SRC_DIRS_MARKER`. */
|
|
26
19
|
export const SRC_DIRS_MARKER = '.geneseed-srcdirs.json';
|
|
27
20
|
/**
|
|
@@ -79,11 +72,6 @@ export function rglobFiles(root) {
|
|
|
79
72
|
.map((d) => path.join(d.parentPath, d.name));
|
|
80
73
|
}
|
|
81
74
|
|
|
82
|
-
/** `p.relative_to(base).as_posix()`. */
|
|
83
|
-
export function relPosix(base, p) {
|
|
84
|
-
return path.relative(base, p).split(path.sep).join('/');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
75
|
/**
|
|
88
76
|
* `_build_emit._strip_capability_links` — `re.sub(r"\1")`, so the pattern needs `g`.
|
|
89
77
|
*
|
|
@@ -8,16 +8,17 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
11
|
-
import { loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
11
|
+
import { claimer, loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
12
12
|
import {
|
|
13
13
|
copyPlugins, copyWorkflows, ensureAgentOverridesStub, writeColorThemes, writeCommandLayer,
|
|
14
14
|
writePonytailCommand, writePrimaryAgent, writeTheme,
|
|
15
15
|
} from '../hosts/opencode.mjs';
|
|
16
16
|
import { mergeOpencodeJson } from '../hosts/settings.mjs';
|
|
17
|
-
import { readText, writeText } from '../lib/fs.mjs';
|
|
17
|
+
import { isFile, readText, writeText } from '../lib/fs.mjs';
|
|
18
|
+
import { relPosix } from '../lib/text.mjs';
|
|
18
19
|
import { assertSourceComplete, build, phaseLog } from './bundle.mjs';
|
|
19
20
|
import {
|
|
20
|
-
globalMemory, globalNotebook,
|
|
21
|
+
globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
|
|
21
22
|
} from './emit-common.mjs';
|
|
22
23
|
import { renderAll } from './render.mjs';
|
|
23
24
|
import {
|
|
@@ -56,17 +57,20 @@ function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
|
56
57
|
} = opts;
|
|
57
58
|
ensureAgentOverridesStub(cfg, overridesDir);
|
|
58
59
|
const overrides = loadAgentOverrides(overridesDir);
|
|
60
|
+
// One claim for every writer below whose path lands in `owned` — see `claimer`.
|
|
61
|
+
const claim = claimer(oldOwned, dir, manifestExisted);
|
|
59
62
|
|
|
60
63
|
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
61
64
|
items, path.join(dir, 'agents'), path.join(dir, 'skills'), overrides,
|
|
62
|
-
{ host: 'opencode',
|
|
65
|
+
{ host: 'opencode', theme, src: cfg.src, claim });
|
|
63
66
|
for (const p of written) owned.push(relPosix(dir, p));
|
|
64
67
|
|
|
65
|
-
const primary = writePrimaryAgent(cfg, path.join(dir, 'agents'), overrides);
|
|
68
|
+
const primary = writePrimaryAgent(cfg, path.join(dir, 'agents'), overrides, claim);
|
|
66
69
|
if (primary) owned.push(relPosix(dir, primary));
|
|
67
70
|
|
|
68
|
-
const commands = writeCommandLayer(cfg, items, path.join(dir, 'command'));
|
|
69
|
-
|
|
71
|
+
const commands = writeCommandLayer(cfg, items, path.join(dir, 'command'), claim);
|
|
72
|
+
const ponytail = writePonytailCommand(path.join(dir, 'command'), claim); // always-on /ponytail
|
|
73
|
+
if (ponytail) commands.push(ponytail);
|
|
70
74
|
for (const p of commands) owned.push(relPosix(dir, p));
|
|
71
75
|
|
|
72
76
|
owned.push(relPosix(dir, writeTheme(path.join(dir, 'themes'), themeName, theme)));
|
|
@@ -97,7 +101,7 @@ function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
|
97
101
|
* manifest is the DRIVER's stage: it reads the file, hands the prior claim in, and prunes
|
|
98
102
|
* and rewrites it after this returns. One owner of the file, and this function is not it.
|
|
99
103
|
*
|
|
100
|
-
* `nativeCatalog` likewise arrives decided. It is `
|
|
104
|
+
* `nativeCatalog` likewise arrives decided. It is `hostCatalogsNatively('opencode')` from
|
|
101
105
|
* `js/hosts/hosts.mjs`, resolved by the driver before the call; passing the decision rather
|
|
102
106
|
* than re-reading the registry keeps one owner of it.
|
|
103
107
|
*/
|
package/js/build/generate.mjs
CHANGED
|
@@ -9,25 +9,27 @@
|
|
|
9
9
|
* WHY `build` IMPORTS THE DRIVER RATHER THAN SPAWNING IT. The Python is
|
|
10
10
|
* `run([sys.executable, BUILD, *extra]).returncode` — a passthrough to a second process.
|
|
11
11
|
* Reproducing that shape here would mean spawning `node bin/build-driver.mjs`, and
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* `tests/unit/hook_cli.test.mjs`'s 'the CLI entry reaches child_process only where it is
|
|
13
|
+
* declared' forbids it: the check is transitive over this entry's imports and allow-lists only
|
|
14
|
+
* the modules that genuinely start processes — `web`, `upgrade`, `setup` — and P5c's argument
|
|
15
|
+
* for it is that the ban should outlive them rather than be dismantled by a verb that does not.
|
|
16
|
+
* It is also the wrong shape on its own terms. Python needs a second
|
|
16
17
|
* process because `build.py` is a different PROGRAM; `bin/build-driver.mjs` is a module in this
|
|
17
18
|
* one, and a Node CLI spawning a Node driver to do work it could do in-process is exactly
|
|
18
19
|
* the passthrough the whole port has been removing. So the driver's `main` is exported and
|
|
19
20
|
* called directly, and the observable contract — the tree, the streams, the exit code — is
|
|
20
|
-
* what `build/*`
|
|
21
|
+
* what the `build/*` cells of `tests/golden.mjs --cli` assert.
|
|
21
22
|
*
|
|
22
23
|
* WHAT NO CELL CAN REACH HERE, stated rather than left implicit. `fenceFor`'s real arm is
|
|
23
24
|
* unreachable from the matrix: measured over the whole rendered tree, the longest backtick
|
|
24
25
|
* run in any source text is 3, so `max(4, longest + 1)` picks 4 for all 96 files, and a port
|
|
25
26
|
* that hardcoded four backticks is byte-identical in every prompt cell. Reaching the other
|
|
26
27
|
* arm needs a file in `src/`, and `src/` is the tree no fixture can redirect (P5d). It is
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* the former Python suite gated it as a pure function over a corpus, with the unreachability
|
|
29
|
+
* measured in BOTH directions rather than asserted: one test proved the corpus left the floor,
|
|
30
|
+
* and one re-rendered every theme × posture × mode of the live tree and proved `src/` still did
|
|
31
|
+
* not. The 96 above is one render — the sweep was 13 440 files. That suite is gone and no Node
|
|
32
|
+
* test calls `fenceFor`, so the real arm is pinned by nothing today.
|
|
31
33
|
*/
|
|
32
34
|
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
33
35
|
import path from 'node:path';
|
|
@@ -202,7 +204,7 @@ export function installProfile(host, scope, root) {
|
|
|
202
204
|
const posture = postureOfDir(root) || defaultPosture();
|
|
203
205
|
const mode = modeOfDir(root) || defaultMode();
|
|
204
206
|
// Read off the deployed loop skill; no config fallback — `DEFAULT_PRESET` is the engine's.
|
|
205
|
-
const trust = trustOfDir(root) || DEFAULT_PRESET;
|
|
207
|
+
const trust = trustOfDir(root, host) || DEFAULT_PRESET;
|
|
206
208
|
// A rebuild that defaulted the pack selection would re-emit an install into a constitution
|
|
207
209
|
// its owner did not choose — and in one direction that is not merely surprising: dropping
|
|
208
210
|
// the process pack takes the commit/push consent RULES out of AGENT.md while
|
|
@@ -360,7 +362,9 @@ export function buildPrompt(cfg, themeName) {
|
|
|
360
362
|
* Windows. At ~10,000 newlines in a 400 KB document that is the largest byte difference this
|
|
361
363
|
* port could ship, and `harness_golden` can only see half of it — the FILE half is compared
|
|
362
364
|
* directly, the stdout half is folded by `subprocess`'s universal-newline decode, which is
|
|
363
|
-
* why
|
|
365
|
+
* why the former Python suite's stdout-bytes comparison grew a `prompt` row — kept today as
|
|
366
|
+
* a row of `tests/unit/hook_cli.test.mjs`'s 'every newline the entry points write is the
|
|
367
|
+
* platform's'.
|
|
364
368
|
*/
|
|
365
369
|
export function cmdPrompt(args) {
|
|
366
370
|
// `args.theme or "neutral"` — a literal in the Python, NOT the config's theme, so this
|
package/js/build/render.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* WHY THIS HALF AND NOT MORE. The emits split into five stages —
|
|
10
10
|
* RENDER* -> WIRE* -> PRUNE -> MANIFEST -> VERIFY (see `_build_global._emit_claude_core`
|
|
11
|
-
* and tests/
|
|
11
|
+
* and tests/unit/emit_phase_order.test.mjs). RENDER produces files Geneseed owns wholesale;
|
|
12
12
|
* WIRE reconciles files the user co-owns (settings.json, opencode.json, the CLAUDE.md
|
|
13
13
|
* managed block) and lives in `js/hosts/settings.mjs`, driven from the two emit modules.
|
|
14
14
|
* THIS module is only RENDER
|
package/js/build/source.mjs
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* `capabilityLinkRe`'s equivalent override in `stripCapabilityLinks` had no producer either
|
|
21
21
|
* and was deleted outright in the over-engineering cleanup, `cfg` param and all.
|
|
22
22
|
*
|
|
23
|
-
* The move is safe for the reason P5c's was: `tests/golden.
|
|
24
|
-
* over
|
|
25
|
-
* from these paths. A depth error or a renamed key fails
|
|
23
|
+
* The move is safe for the reason P5c's was: `tests/golden.mjs` drives `bin/build-driver.mjs`
|
|
24
|
+
* over every emit configuration and requires each to render, and every one of them builds a
|
|
25
|
+
* cfg from these paths. A depth error or a renamed key fails every cell, not zero.
|
|
26
26
|
*/
|
|
27
27
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
28
28
|
import path from 'node:path';
|
|
@@ -38,7 +38,7 @@ import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
|
38
38
|
* on raw or folded text, so one behaviour serves both.
|
|
39
39
|
*/
|
|
40
40
|
const _packTexts = new Map();
|
|
41
|
-
|
|
41
|
+
function readRawText(file) {
|
|
42
42
|
const key = path.resolve(file);
|
|
43
43
|
const mtime = statSync(file).mtimeMs;
|
|
44
44
|
const hit = _packTexts.get(key);
|
|
@@ -160,8 +160,8 @@ export function resolveRuleIds(text, src = SRC) {
|
|
|
160
160
|
* `_OWNED` redirect the suite uses is an in-process write, and no env var moves it
|
|
161
161
|
* (`$GENESEED_ROOT` is `harness context`'s doc-discovery root — a different name for a
|
|
162
162
|
* different job). Both implementations therefore answer from their own file's location,
|
|
163
|
-
* which is what
|
|
164
|
-
* status
|
|
163
|
+
* which is what made them comparable and what makes them unfenceable. See
|
|
164
|
+
* `js/inspect/status.mjs`'s header for what that leaves unreachable from any cell.
|
|
165
165
|
*/
|
|
166
166
|
export const ROOT = path.resolve(import.meta.dirname, '../..');
|
|
167
167
|
export const SRC = path.join(ROOT, 'src');
|
package/js/build/stubs.mjs
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { writeText } from '../lib/fs.mjs';
|
|
12
|
+
import { isDir } from '../lib/fs.mjs';
|
|
12
13
|
import { jsonDumpsIndent } from '../lib/json.mjs';
|
|
13
|
-
import {
|
|
14
|
+
import { EXCLUDES_FILE } from '../hosts/hosts.mjs';
|
|
14
15
|
import { existsSync } from 'node:fs';
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -102,8 +103,8 @@ usually one promoted from a recurring memory. Review it by that date, then
|
|
|
102
103
|
graduate it (remove the marker) or demote it back to memory.
|
|
103
104
|
`;
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
export
|
|
106
|
+
// Owned by `js/hosts/hosts.mjs` (the hook reads it there); re-exported for `js/inspect/excludes.mjs`.
|
|
107
|
+
export { EXCLUDES_FILE };
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* `_build_render.EXCLUDES_STUB` — one long line, exactly as Python spells it.
|
|
@@ -232,8 +233,8 @@ export const ensureProfileStub = stubWriter(PROFILE_FILE, PROFILE_STUB);
|
|
|
232
233
|
|
|
233
234
|
/**
|
|
234
235
|
* `_build_render.ensure_excludes_stub` — the sovereign-repo list, seeded once and NEVER
|
|
235
|
-
* overwritten.
|
|
236
|
-
*
|
|
236
|
+
* overwritten. Called by every GLOBAL emit — the Claude-shaped ones and opencode-global —
|
|
237
|
+
* since that is where the hook looks for it.
|
|
237
238
|
*/
|
|
238
239
|
export const ensureExcludesStub = stubWriter(EXCLUDES_FILE, EXCLUDES_STUB);
|
|
239
240
|
|
package/js/build/version.mjs
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
11
|
-
import { sourceReleaseVersion } from '../hosts/opencode.mjs';
|
|
12
11
|
import { readText, writeText } from '../lib/fs.mjs';
|
|
13
|
-
import {
|
|
12
|
+
import { parseJson } from '../lib/json.mjs';
|
|
13
|
+
import { isDir } from '../lib/fs.mjs';
|
|
14
|
+
import { relPosix } from '../lib/text.mjs';
|
|
15
|
+
import { rglobFiles } from './emit-common.mjs';
|
|
14
16
|
import { createHash } from 'node:crypto';
|
|
15
17
|
import { readFileSync } from 'node:fs';
|
|
16
18
|
|
|
@@ -132,6 +134,23 @@ export function versionIsNewer(a, b) {
|
|
|
132
134
|
return false;
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
/**
|
|
138
|
+
* `_build_core.source_release_version` — the release this checkout would install. It reads
|
|
139
|
+
* `cfg.config`, so it takes `cfg`. It lived in `js/hosts/opencode.mjs` for no better reason
|
|
140
|
+
* than that the first caller was there; every reader since asks a version question, so it
|
|
141
|
+
* lives with them. `js/hosts/opencode.mjs` re-exports it for its older importers.
|
|
142
|
+
*/
|
|
143
|
+
export function sourceReleaseVersion(cfg) {
|
|
144
|
+
try {
|
|
145
|
+
const data = parseJson(readText(cfg.config));
|
|
146
|
+
const v = data && typeof data === 'object' && !Array.isArray(data) ? data.version : null;
|
|
147
|
+
if (typeof v === 'string' && v) return v;
|
|
148
|
+
} catch {
|
|
149
|
+
// OSError / JSONDecodeError — degrade to "not newer" rather than raising.
|
|
150
|
+
}
|
|
151
|
+
return '0.0.0';
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
/** `_build_render._warn_if_downgrade` — STDOUT, matching the Python. */
|
|
136
155
|
function warnIfDowngrade(cfg, out) {
|
|
137
156
|
const deployed = readReleaseVersion(out);
|
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
9
|
import { readText, printOut as out, printErr as err, withDiscardableStderr } from '../lib/fs.mjs';
|
|
10
10
|
import { normcase, toPlatformPath } from '../lib/paths.mjs';
|
|
11
|
+
import { relPosix } from '../lib/text.mjs';
|
|
11
12
|
import { resolvePath, sovereignBypass } from './hosts.mjs';
|
|
12
13
|
import { isFile, isDir, selfAndParents, sortPaths, listDir } from './hooks-prims.mjs';
|
|
13
14
|
|
|
@@ -177,7 +178,7 @@ function disp(pathStr, root) {
|
|
|
177
178
|
!== path.parse(path.resolve(root)).root.toLowerCase()) {
|
|
178
179
|
return pathStr;
|
|
179
180
|
}
|
|
180
|
-
return
|
|
181
|
+
return relPosix(root, pathStr);
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
|
package/js/hosts/hooks-prims.mjs
CHANGED
|
@@ -23,8 +23,8 @@ import { comparePaths } from '../lib/paths.mjs';
|
|
|
23
23
|
// none of that reached the hook, because a corpus finds what it is pointed at and nothing
|
|
24
24
|
// named `pyStrPath` was. The consequence was live: `sovereignBypass` compares an
|
|
25
25
|
// `excludes.json` entry against cwd, so a hand-edited `..` entry made the NODE hook stand
|
|
26
|
-
// down for a repo Python still gates.
|
|
27
|
-
// the cell.
|
|
26
|
+
// down for a repo Python still gates. (Since the Python is gone, exclude entries resolve `..`
|
|
27
|
+
// and symlinks on every host — `context/sovereign-bypass-matches-a-dotdot-entry` is the cell.)
|
|
28
28
|
//
|
|
29
29
|
// So the line is not "duplicate rather than couple" — it is "one owner for anything that
|
|
30
30
|
// reproduces a language primitive, wherever it is used". `fs.mjs` carries no
|
package/js/hosts/hosts.mjs
CHANGED
|
@@ -66,8 +66,12 @@ export function sovereignBypass(root) {
|
|
|
66
66
|
// `withDiscardableStderr` because `expanduser` writes its refusal at the RAISE SITE
|
|
67
67
|
// and this caller catches: the reference prints nothing here, so replaying the
|
|
68
68
|
// message would be a stderr divergence on every hook call for one bad entry.
|
|
69
|
+
//
|
|
70
|
+
// `resolvePath`, the same resolution as `cwd` above: symlinks and junctions are followed on
|
|
71
|
+
// BOTH sides, so an entry spelled through a link matches the real directory the session
|
|
72
|
+
// stands in (and the OpenCode plugins' `norm` applies the identical rule).
|
|
69
73
|
base = withDiscardableStderr(
|
|
70
|
-
() => normcase(
|
|
74
|
+
() => normcase(resolvePath(raw.trim())).replace(/[\\/]+$/, ''));
|
|
71
75
|
} catch {
|
|
72
76
|
// PER ENTRY, and the loop continues. One unusable line in a file the USER hand-edits
|
|
73
77
|
// must not decide the whole function — `expanduser` refuses a `~user` form, and the
|
|
@@ -208,14 +212,13 @@ export function openclaudeConfigDir() {
|
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
/**
|
|
211
|
-
* `_build_global.HOSTS
|
|
212
|
-
*
|
|
215
|
+
* `_build_global.HOSTS` — one row per host, IN ITS ORDER: opencode, claude, bob, openclaude.
|
|
216
|
+
* A new host is one row here; `EMIT_HOST_SCOPE` (`js/hosts/installs.mjs`) and `CLAUDE_STYLE`
|
|
217
|
+
* below are derived from it.
|
|
213
218
|
*
|
|
214
219
|
* An array rather than an object because the order is observable output, not an
|
|
215
220
|
* implementation detail: `harness exclude add` walks it and prints one message per host, so
|
|
216
|
-
* a reordering is a diff in stderr.
|
|
217
|
-
* `bin/build-driver.mjs` receives those decisions from its own dispatch rather than reading a
|
|
218
|
-
* table.)
|
|
221
|
+
* a reordering is a diff in stderr.
|
|
219
222
|
*
|
|
220
223
|
* `projectMarker` joined in P5f. `_install_targets` asks "does this cwd carry a project
|
|
221
224
|
* install of host H", and `_claude_cfg` asks "which subdir holds a project install's
|
|
@@ -232,13 +235,16 @@ export function openclaudeConfigDir() {
|
|
|
232
235
|
* `family` joined in 2026-10: `'claude'` for every host emitted through the Claude-shaped
|
|
233
236
|
* engine — one manifest shape, one `settings.json` hook wiring, one strict-JSON `mcpServers`
|
|
234
237
|
* config, one reversal. The list had been spelled out by hand in ten places, one of which
|
|
235
|
-
* ignored the named constant right above it
|
|
238
|
+
* ignored the named constant right above it.
|
|
239
|
+
*
|
|
240
|
+
* `catalog` — does the host list every skill and agent to the model by itself? See
|
|
241
|
+
* `hostCatalogsNatively` below for what it decides and why it is split per kind.
|
|
236
242
|
*/
|
|
237
243
|
export const HOSTS = [
|
|
238
|
-
{ host: 'opencode', family: 'opencode', configDir: opencodeConfigDir, projectMarker: '.opencode', agentFile: 'AGENT.md' },
|
|
239
|
-
{ host: 'claude', family: 'claude', configDir: claudeConfigDir, projectMarker: '.claude', agentFile: 'CLAUDE.md' },
|
|
240
|
-
{ host: 'bob', family: 'claude', configDir: bobConfigDir, projectMarker: '.bob', agentFile: 'AGENTS.md' },
|
|
241
|
-
{ host: 'openclaude', family: 'claude', configDir: openclaudeConfigDir, projectMarker: '.openclaude', agentFile: 'CLAUDE.md' },
|
|
244
|
+
{ host: 'opencode', family: 'opencode', configDir: opencodeConfigDir, projectMarker: '.opencode', agentFile: 'AGENT.md', catalog: { skills: true, agents: true } },
|
|
245
|
+
{ host: 'claude', family: 'claude', configDir: claudeConfigDir, projectMarker: '.claude', agentFile: 'CLAUDE.md', catalog: { skills: true, agents: true } },
|
|
246
|
+
{ host: 'bob', family: 'claude', configDir: bobConfigDir, projectMarker: '.bob', agentFile: 'AGENTS.md', catalog: { skills: true, agents: false } },
|
|
247
|
+
{ host: 'openclaude', family: 'claude', configDir: openclaudeConfigDir, projectMarker: '.openclaude', agentFile: 'CLAUDE.md', catalog: { skills: true, agents: true } },
|
|
242
248
|
];
|
|
243
249
|
|
|
244
250
|
/** The Claude-STYLE hosts (`family: 'claude'`), in `HOSTS` order. Test with `.includes(host)`. */
|
|
@@ -248,12 +254,11 @@ export const CLAUDE_STYLE = HOSTS.filter((h) => h.family === 'claude').map((h) =
|
|
|
248
254
|
* `_build_global.host_catalogs_natively` — does `host` list every skill and agent to the
|
|
249
255
|
* model by itself?
|
|
250
256
|
*
|
|
251
|
-
*
|
|
252
|
-
* `native_catalog` stays out: `doctor`'s `_rendered_problems` has to know, because a host
|
|
257
|
+
* `HOSTS`' `catalog` column. `doctor`'s `_rendered_problems` has to know, because a host
|
|
253
258
|
* that catalogues natively gets AGENT.md's tables collapsed to a pointer and comparing the
|
|
254
259
|
* portable shape against its bundle reports AGENT.md stale on every run forever. It is a
|
|
255
|
-
* column and not a set literal so that the capability is
|
|
256
|
-
* — the Python's own reason for the helper.
|
|
260
|
+
* column and not a set literal so that the capability is declared in exactly one place
|
|
261
|
+
* — the Python's own reason for the helper. The driver reads it through this function too.
|
|
257
262
|
*
|
|
258
263
|
* Unknown host -> false, which is the shape that KEEPS the tables. The value arrives from a
|
|
259
264
|
* user-editable `.geneseed-emit` marker, so an unrecognised one must degrade to the portable
|
|
@@ -264,19 +269,12 @@ export const CLAUDE_STYLE = HOSTS.filter((h) => h.family === 'claude').map((h) =
|
|
|
264
269
|
* single boolean was wrong both ways: `false` shipped the §4 Skills table on top of a
|
|
265
270
|
* catalogue Bob already had (~1.7k tokens twice), `true` would have stripped the §3 Agents
|
|
266
271
|
* table that is Bob's ONLY agent catalogue. Each CATALOG block in AGENT.md names its kind and
|
|
267
|
-
* resolves against its own flag.
|
|
268
|
-
*
|
|
272
|
+
* resolves against its own flag.
|
|
273
|
+
*
|
|
274
|
+
* Returns `{ skills, agents }` for a known host, `false` for an unknown one.
|
|
269
275
|
*/
|
|
270
|
-
const NATIVE_CATALOG = {
|
|
271
|
-
opencode: { skills: true, agents: true },
|
|
272
|
-
claude: { skills: true, agents: true },
|
|
273
|
-
bob: { skills: true, agents: false },
|
|
274
|
-
openclaude: { skills: true, agents: true },
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
/** `{ skills, agents }` for a known host, `false` for an unknown one. */
|
|
278
276
|
export function hostCatalogsNatively(host) {
|
|
279
|
-
return
|
|
277
|
+
return HOSTS.find((h) => h.host === host)?.catalog || false;
|
|
280
278
|
}
|
|
281
279
|
|
|
282
280
|
/** `_harness_learn.MEMORY_DIR_NAMES` — the neutral name and the imperial theme's. */
|