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/hosts/opencode.mjs
CHANGED
|
@@ -16,18 +16,21 @@
|
|
|
16
16
|
*
|
|
17
17
|
* TWO STREAMS, AND THE SPLIT IS NOT TIDY. `_warn_if_overrides_stale` prints to STDOUT
|
|
18
18
|
* while every warning in `js/hosts/native.mjs` goes to stderr. That is the Python's own
|
|
19
|
-
* inconsistency and it is reproduced, not corrected:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* a stray stdout byte silently disarms a blocking gate.
|
|
19
|
+
* inconsistency and it is reproduced, not corrected: `tests/unit/agent_overrides.test.mjs`
|
|
20
|
+
* pins the stale notice to stdout. It is safe because this is the generator's CLI, which
|
|
21
|
+
* prints progress to stdout by design — unlike a hook path, where a stray stdout byte
|
|
22
|
+
* silently disarms a blocking gate.
|
|
24
23
|
*/
|
|
25
24
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
26
25
|
import path from 'node:path';
|
|
27
26
|
import { writeText, readText, copyFile } from '../lib/fs.mjs';
|
|
28
27
|
import { jsonDumps, jsonDumpsIndent, parseJson } from '../lib/json.mjs';
|
|
29
28
|
import { comparePaths } from '../lib/paths.mjs';
|
|
30
|
-
import {
|
|
29
|
+
import { sourceReleaseVersion } from '../build/version.mjs';
|
|
30
|
+
import { firstBlockquote, stripSkillBodyLinks, pushOverrideLines } from './native.mjs';
|
|
31
|
+
|
|
32
|
+
// Re-exported for `js/web/api.mjs` and the tests, which import it from here.
|
|
33
|
+
export { sourceReleaseVersion };
|
|
31
34
|
|
|
32
35
|
/** ANSI colour-name -> integer (0-7). Mirrors `_build_emit._ANSI`. */
|
|
33
36
|
const ANSI = {
|
|
@@ -94,9 +97,17 @@ const AGENT_OVERRIDES_STUB = {
|
|
|
94
97
|
},
|
|
95
98
|
};
|
|
96
99
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
/**
|
|
101
|
+
* `_build_emit.COMMAND_SET` — the hot skills worth a one-keystroke trigger, as command name ->
|
|
102
|
+
* skill name. A map, not a list, because the two can differ: `code-review` became
|
|
103
|
+
* `geneseed-code-review` (so it no longer shadows a host built-in) while the command stays
|
|
104
|
+
* `/code-review`. As a list the renamed entry missed its skill and was dropped in silence, so a
|
|
105
|
+
* missing skill is now a thrown error in `writeCommandLayer`.
|
|
106
|
+
*/
|
|
107
|
+
const COMMAND_SET = {
|
|
108
|
+
commit: 'commit', plan: 'plan', 'code-review': 'geneseed-code-review',
|
|
109
|
+
'review-response': 'review-response', ship: 'ship', debug: 'debug', research: 'research',
|
|
110
|
+
};
|
|
100
111
|
|
|
101
112
|
/** Mirrors `_build_emit.PONYTAIL_COMMAND_BODY`. */
|
|
102
113
|
const PONYTAIL_COMMAND_BODY =
|
|
@@ -113,22 +124,6 @@ function truthyEnv(name) {
|
|
|
113
124
|
return ['1', 'on', 'true', 'yes'].includes((process.env[name] || '').toLowerCase());
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
/**
|
|
117
|
-
* `_build_core.source_release_version`. It reads `cfg.config`, so it takes `cfg` rather than
|
|
118
|
-
* importing core — and it stayed HERE when core landed as `js/build/source.mjs`, which is why
|
|
119
|
-
* `js/build/version.mjs` reaches back into the OpenCode module for it.
|
|
120
|
-
*/
|
|
121
|
-
export function sourceReleaseVersion(cfg) {
|
|
122
|
-
try {
|
|
123
|
-
const data = parseJson(readText(cfg.config));
|
|
124
|
-
const v = data && typeof data === 'object' && !Array.isArray(data) ? data.version : null;
|
|
125
|
-
if (typeof v === 'string' && v) return v;
|
|
126
|
-
} catch {
|
|
127
|
-
// OSError / JSONDecodeError — degrade to "not newer" rather than raising.
|
|
128
|
-
}
|
|
129
|
-
return '0.0.0';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
127
|
const GRAY = 8;
|
|
133
128
|
const GREEN = 2;
|
|
134
129
|
const RED = 1;
|
|
@@ -181,7 +176,6 @@ export function writeTheme(themesDir, themeName, theme) {
|
|
|
181
176
|
return dest;
|
|
182
177
|
}
|
|
183
178
|
|
|
184
|
-
/** `_build_emit._color_theme_json`. */
|
|
185
179
|
/**
|
|
186
180
|
* `_build_emit._PALETTE_ROLES` — every role some slot reads, as a set.
|
|
187
181
|
*
|
|
@@ -192,6 +186,7 @@ export function writeTheme(themesDir, themeName, theme) {
|
|
|
192
186
|
*/
|
|
193
187
|
export const PALETTE_ROLES = new Set(Object.values(SLOT_ROLE));
|
|
194
188
|
|
|
189
|
+
/** `_build_emit._color_theme_json`. */
|
|
195
190
|
export function colorThemeJson(palette, transparent) {
|
|
196
191
|
const t = {};
|
|
197
192
|
for (const [slot, role] of Object.entries(SLOT_ROLE)) {
|
|
@@ -272,8 +267,16 @@ export function ensureAgentOverridesStub(cfg, base) {
|
|
|
272
267
|
writeText(dest, `${jsonDumpsIndent(stub, { ensureAscii: false })}\n`);
|
|
273
268
|
}
|
|
274
269
|
|
|
275
|
-
/**
|
|
276
|
-
|
|
270
|
+
/**
|
|
271
|
+
* `_build_emit._write_primary_agent` — the opt-in `mode: primary` orchestrator.
|
|
272
|
+
*
|
|
273
|
+
* `claim` is `claimer`'s closure from `js/hosts/native.mjs`, and this and the two command
|
|
274
|
+
* writers below take it for the reason `writeNativeLayer` does: their paths are pushed into
|
|
275
|
+
* `owned`, and uninstall deletes what `owned` names — so writing over a user's own
|
|
276
|
+
* `command/commit.md` without the check made uninstall delete it. Default: claim everything,
|
|
277
|
+
* for a caller with no manifest to consult.
|
|
278
|
+
*/
|
|
279
|
+
export function writePrimaryAgent(cfg, agentsDir, overrides, claim = () => true) {
|
|
277
280
|
if (!truthyEnv('GENESEED_PRIMARY') || !existsSync(cfg.primaryAgentSrc)
|
|
278
281
|
|| !statSync(cfg.primaryAgentSrc).isFile()) {
|
|
279
282
|
return null;
|
|
@@ -286,13 +289,14 @@ export function writePrimaryAgent(cfg, agentsDir, overrides) {
|
|
|
286
289
|
&& overrides.orchestrator) || {};
|
|
287
290
|
pushOverrideLines(fm, ov);
|
|
288
291
|
const dest = path.join(agentsDir, 'orchestrator.md');
|
|
292
|
+
if (!claim(dest)) return null;
|
|
289
293
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
290
294
|
writeText(dest, `---\n${fm.join('\n')}\n---\n\n${body}`);
|
|
291
295
|
return dest;
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
/** `_build_emit._write_command_layer` — the opt-in /slash commands. */
|
|
295
|
-
export function writeCommandLayer(cfg, items, commandDir) {
|
|
299
|
+
export function writeCommandLayer(cfg, items, commandDir, claim = () => true) {
|
|
296
300
|
if (!truthyEnv('GENESEED_COMMANDS')) return [];
|
|
297
301
|
const byName = new Map();
|
|
298
302
|
for (const { text, src } of items) {
|
|
@@ -304,12 +308,15 @@ export function writeCommandLayer(cfg, items, commandDir) {
|
|
|
304
308
|
}
|
|
305
309
|
}
|
|
306
310
|
const written = [];
|
|
307
|
-
for (const name of COMMAND_SET) {
|
|
308
|
-
const text = byName.get(
|
|
309
|
-
if (text === undefined)
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
for (const [name, skill] of Object.entries(COMMAND_SET)) {
|
|
312
|
+
const text = byName.get(skill);
|
|
313
|
+
if (text === undefined) {
|
|
314
|
+
throw new Error(`COMMAND_SET: /${name} runs skill '${skill}', which src/skills/ does not have`);
|
|
315
|
+
}
|
|
312
316
|
const dest = path.join(commandDir, `${name}.md`);
|
|
317
|
+
if (!claim(dest)) continue;
|
|
318
|
+
const desc = firstBlockquote(text);
|
|
319
|
+
const body = stripSkillBodyLinks(text.replace(/^\n+/, ''));
|
|
313
320
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
314
321
|
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${body}`);
|
|
315
322
|
written.push(dest);
|
|
@@ -317,9 +324,10 @@ export function writeCommandLayer(cfg, items, commandDir) {
|
|
|
317
324
|
return written;
|
|
318
325
|
}
|
|
319
326
|
|
|
320
|
-
/** `_build_emit._write_ponytail_command` — registered UNCONDITIONALLY. */
|
|
321
|
-
export function writePonytailCommand(commandDir) {
|
|
327
|
+
/** `_build_emit._write_ponytail_command` — registered UNCONDITIONALLY; null when the user's. */
|
|
328
|
+
export function writePonytailCommand(commandDir, claim = () => true) {
|
|
322
329
|
const dest = path.join(commandDir, 'ponytail.md');
|
|
330
|
+
if (!claim(dest)) return null;
|
|
323
331
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
324
332
|
const desc = 'Set the ponytail minimal-code level for the session: lite | full | ultra | off';
|
|
325
333
|
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${PONYTAIL_COMMAND_BODY}`);
|
package/js/hosts/shim.mjs
CHANGED
|
@@ -273,7 +273,8 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
273
273
|
* interpreter silently disables every hook in the install. Both halves are gone. `runner` is
|
|
274
274
|
* `process.execPath` — the node already running this file, which by construction exists —
|
|
275
275
|
* so there is nothing left to discover and no way for discovery to fail. P4e kept the
|
|
276
|
-
* unreachable exit-3 branch because
|
|
276
|
+
* unreachable exit-3 branch because 'the driver classifies every emit'
|
|
277
|
+
* (`tests/unit/node_driver.test.mjs`) asserts a
|
|
277
278
|
* partition it belongs to; nothing asserts a partition over this one, so keeping it would
|
|
278
279
|
* be keeping code no test can reach for no stated reason.
|
|
279
280
|
*
|
|
@@ -287,7 +288,8 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
287
288
|
*
|
|
288
289
|
* That is correct exactly while the two entry points answer the same verbs the same way, so
|
|
289
290
|
* the gate that used to be a formality is now load-bearing:
|
|
290
|
-
*
|
|
291
|
+
* 'the hook entry carries exactly the verbs the emitter wires' (`tests/unit/hook_cli.test.mjs`)
|
|
292
|
+
* reads the emitter's wiring
|
|
291
293
|
* and `bin/geneseed-hook.mjs`'s VERBS table and requires them EQUAL — a wired verb the entry
|
|
292
294
|
* lacks is a dead hook on every install on the machine, not just this one. The alternative
|
|
293
295
|
* considered and rejected was a per-driver shim path: the path is baked into every already
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
* * **The diff algorithm.** A cell can produce ONE edited file and see one hunk. It cannot
|
|
23
23
|
* produce the shapes where `difflib`'s choices differ from any other diff's — ties, the
|
|
24
24
|
* recursion's alignment, and above all `autojunk`, which only engages at 200 lines and
|
|
25
|
-
* changes the hunks on every real harness file.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* switched off, so the corpus is not describing a constant).
|
|
25
|
+
* changes the hunks on every real harness file. The former Python suite gated it as a
|
|
26
|
+
* corpus, measured in both directions (28 of its 176 cases disagreed when `autojunk` was
|
|
27
|
+
* switched off). That corpus is gone and no Node test drives `unifiedDiff` directly, so
|
|
28
|
+
* beyond the one hunk the `diff/--full-…` cell sees, nothing pins these shapes today.
|
|
30
29
|
* * **The timestamp.** `_write_improvements` names the file `improvements-%Y%m%d-%H%M%S.md`
|
|
31
30
|
* and stamps the report with `datetime.now()`, so the two implementations cannot write the
|
|
32
31
|
* same bytes across a second boundary — and a cell runs them in sequence, so the boundary
|
|
@@ -177,7 +176,7 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
177
176
|
// makes explicit rather than leaving to `makeCfg`'s parameter default two layers down.
|
|
178
177
|
posture: postureOfDir(dir), mode: modeOfDir(dir), doctrines: doctrinesForBuild(dir),
|
|
179
178
|
// The loop skill's preset, or its `Default trust preset:` line reads as edited.
|
|
180
|
-
trust: trustOfDir(dir),
|
|
179
|
+
trust: trustOfDir(dir, host),
|
|
181
180
|
// The excluded rules, or an install built `--exclude-rules "process 5"` reads its
|
|
182
181
|
// carrier as drift: `expected` would re-state the rule the deployment took away.
|
|
183
182
|
excludeRules: excludedRulesOfDir(dir),
|
package/js/inspect/excludes.mjs
CHANGED
|
@@ -78,14 +78,19 @@ const canon = (p) => resolvePath(p);
|
|
|
78
78
|
/**
|
|
79
79
|
* `_same` — is this stored entry the same folder as `repo`?
|
|
80
80
|
*
|
|
81
|
-
* `
|
|
82
|
-
* `
|
|
83
|
-
*
|
|
84
|
-
*
|
|
81
|
+
* Resolved like `repo` itself (`canon`: `..` collapsed, symlinks followed), the one rule the
|
|
82
|
+
* hook's `sovereignBypass` and the OpenCode plugins apply too — so an entry that EXCLUDES a
|
|
83
|
+
* folder is exactly the entry `exclude remove` can take back. It used to compare lexically,
|
|
84
|
+
* mirroring the former Python `Path()`, which made a hand-edited `<repo>/../repo` an entry the
|
|
85
|
+
* plugins honoured, the hook ignored, and `remove` could not find.
|
|
85
86
|
*/
|
|
86
87
|
function same(stored, repo) {
|
|
87
88
|
if (typeof stored !== 'string' || !stored) return false;
|
|
88
|
-
|
|
89
|
+
try {
|
|
90
|
+
return normcase(canon(stored)) === normcase(repo);
|
|
91
|
+
} catch {
|
|
92
|
+
return false; // a `~user` form `expanduser` refuses names no folder
|
|
93
|
+
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
package/js/inspect/status.mjs
CHANGED
|
@@ -34,25 +34,26 @@
|
|
|
34
34
|
* became the second and third. Nothing about them changed in the move — the 166-cell
|
|
35
35
|
* acceptance matrix is what licensed attempting it, `status`'s own eleven cells among them.
|
|
36
36
|
*
|
|
37
|
-
* WHAT NO CELL CAN REACH, and where it is gated instead.
|
|
38
|
-
* status/version section
|
|
37
|
+
* WHAT NO CELL CAN REACH, and where it is gated instead. The former Python matrix's
|
|
38
|
+
* status/version section carried the long form; the short form is that ROOT is not
|
|
39
39
|
* redirectable across a process boundary, so three things here are unreachable from any
|
|
40
40
|
* cell: `versionVerdict`'s "up to date" (needs a marker holding a fingerprint no cell can
|
|
41
41
|
* know), `accentFor`'s cyan fallback (an unknown theme is refused upstream by
|
|
42
42
|
* `effectiveTheme`), and the whole ANSI half of `statusLines` (`_color_enabled` is
|
|
43
43
|
* `sys.stdout.isatty()`, and every harness captures stdout through a pipe). All three are
|
|
44
|
-
* PURE FUNCTIONS of their arguments, so
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* PURE FUNCTIONS of their arguments, so the former Python suite gated all three as a corpus
|
|
45
|
+
* instead — a third answer to the colour question, which had been posed as a choice between
|
|
46
|
+
* shipping it ungated and not shipping it. That corpus is gone: `tests/unit/lifecycle.test.mjs`
|
|
47
|
+
* now calls `versionVerdict` and both halves of `statusLines` directly, and nothing pins
|
|
48
|
+
* `accentFor`'s fallback.
|
|
49
49
|
*
|
|
50
50
|
* `manifestIsClaude` was a FOURTH until wave 2 of the P0/P1 review: it is only reached for
|
|
51
51
|
* a candidate with no known host, and `ROOT/"Harness"` was ordered ahead of the sandbox's
|
|
52
52
|
* own. Both ROOT-relative bundle candidates are gone from this walk and from
|
|
53
53
|
* `installedDefaults`, so the only no-known-host candidate left is `cwd/"Harness"`, which
|
|
54
54
|
* is inside the sandbox. It is reachable now; no cell seeds a manifest without an emit
|
|
55
|
-
* marker, so it is
|
|
55
|
+
* marker, so it is gated only by the direct calls in `tests/unit/claude.test.mjs`, and that is
|
|
56
|
+
* a gap rather than a wall.
|
|
56
57
|
*/
|
|
57
58
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
58
59
|
import path from 'node:path';
|
|
@@ -66,19 +67,15 @@ import {
|
|
|
66
67
|
} from '../hosts/hosts.mjs';
|
|
67
68
|
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
68
69
|
// deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
|
|
69
|
-
// three verbs now read them.
|
|
70
|
-
// re-exported below because `tests/fixtures/pure_probe.mjs` names this module for two of them
|
|
71
|
-
// and a corpus that followed the code to its new file would stop testing the caller's view.
|
|
70
|
+
// three verbs now read them.
|
|
72
71
|
import {
|
|
73
|
-
defaultTheme, installedDefaults, installTargets,
|
|
72
|
+
defaultTheme, installedDefaults, installTargets, readJsonMaybe, readMaybe,
|
|
74
73
|
} from '../hosts/installs.mjs';
|
|
75
74
|
import { installProfile, rebuildCommand } from '../build/generate.mjs';
|
|
76
75
|
import { shimDead } from '../hosts/shim.mjs';
|
|
77
76
|
import { printOut } from '../lib/fs.mjs';
|
|
78
77
|
import { codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
79
78
|
|
|
80
|
-
export { defaultTheme, manifestIsClaude };
|
|
81
|
-
|
|
82
79
|
// ---- version ---------------------------------------------------------------------------
|
|
83
80
|
|
|
84
81
|
/** `_harness_status._version_verdict`. */
|
|
@@ -331,13 +328,13 @@ export function statusLines(d, color = false) {
|
|
|
331
328
|
+ `(${d.facts} fact${d.facts === 1 ? '' : 's'})`],
|
|
332
329
|
['version', `${d.installed_fp || '(none)'} ${DOT} source ${d.source_fp}`],
|
|
333
330
|
];
|
|
334
|
-
// ⚠ A ROW, NOT A WIDER `components` LINE
|
|
335
|
-
//
|
|
336
|
-
// `
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
331
|
+
// ⚠ A ROW, NOT A WIDER `components` LINE. When this landed, `statusLines` had 30 recorded
|
|
332
|
+
// cases per platform in the former Python suite's corpus, every one holding the box's exact
|
|
333
|
+
// bytes — including `17 agents · 47 skills · 37 laws` — and no recorder left to re-bless them.
|
|
334
|
+
// Touching `components` would have moved all 30; adding a row would too, since the box width
|
|
335
|
+
// is measured over `rows`. So the row is CONDITIONAL on a key those arguments did not carry,
|
|
336
|
+
// exactly as `agent_md` below already is. The corpus is gone; a caller without the key still
|
|
337
|
+
// gets the box it got before.
|
|
341
338
|
//
|
|
342
339
|
// It carries the two tiers `components` cannot: `components` counts the invariants as
|
|
343
340
|
// `laws`, which is still true, and says nothing about the ontology or the packs.
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -149,8 +149,8 @@ export function cmdValidate(args) {
|
|
|
149
149
|
// which ends `raise SystemExit(res.get("exit", 1))` — an INTEGER — so every refusal
|
|
150
150
|
// that crosses the seam arrives here as `SystemExit(1)` and `str(e)` is `'1'`, not the
|
|
151
151
|
// sentence `load_theme` wrote on stderr on the way out. A port that printed `e.message`
|
|
152
|
-
// says `unknown theme 'x'` where the reference says `1`. The
|
|
153
|
-
//
|
|
152
|
+
// says `unknown theme 'x'` where the reference says `1`. The former Python
|
|
153
|
+
// cross-implementation corpus is what found it; nothing else could.
|
|
154
154
|
if (e && e.exitCode !== undefined) {
|
|
155
155
|
printOut(`[validate-only] render/emit FAILED for theme '${args.theme}' `
|
|
156
156
|
+ `emit '${emit}': ${e.exitCode}\n`);
|
package/js/lib/fs.mjs
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* These are definitions, not an adaptation of somebody else's. Every rule below was
|
|
5
5
|
* originally MEASURED against CPython, because the generator these functions serve had to
|
|
6
6
|
* emit bytes indistinguishable from a Python one; that second party is gone and the rules
|
|
7
|
-
* are not.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* are not. The recorded corpora that compared this module's output byte for byte went with
|
|
8
|
+
* the former Python suite; what holds the rules now is `tests/unit/lib_primitives.test.mjs`
|
|
9
|
+
* (`isFile`/`isDir`), `tests/unit/hook_cli.test.mjs`'s platform-newline count (the stream
|
|
10
|
+
* funnels), and for the rest only the per-function provenance below. A rule here is still a
|
|
11
|
+
* frozen fact about what this tool emits, and changing one is a product change.
|
|
10
12
|
*
|
|
11
13
|
* ⚠ NONE OF THESE IS THE NODE DEFAULT, AND THAT IS THE ONE THING TO KNOW BEFORE EDITING ONE.
|
|
12
14
|
* Each is a deliberate divergence from the obvious one-liner, and the obvious one-liner is
|
|
@@ -107,10 +109,10 @@ export function copyFile(src, dest) {
|
|
|
107
109
|
* its caller different BYTES than the Python one: measured at 176 vs 171 for one `context`
|
|
108
110
|
* run before this existed.
|
|
109
111
|
*
|
|
110
|
-
*
|
|
111
|
-
* universal newlines
|
|
112
|
-
*
|
|
113
|
-
*
|
|
112
|
+
* The CLI matrix structurally cannot see the difference — every cell decodes stdout with
|
|
113
|
+
* universal newlines, folding both shapes to `\n` before anything compares them. So the gate
|
|
114
|
+
* for it is `tests/unit/hook_cli.test.mjs`'s `every newline the entry points write is the
|
|
115
|
+
* platform's`, which counts raw bytes. Moved here
|
|
114
116
|
* from `js/hosts/hooks.mjs` in P5c, when `js/inspect/excludes.mjs` became the second caller: a translation
|
|
115
117
|
* that exists twice is a translation that can be fixed once.
|
|
116
118
|
*/
|
|
@@ -167,10 +169,9 @@ export function withDiscardableStderr(fn) {
|
|
|
167
169
|
* `emitGlobalInto`.
|
|
168
170
|
*
|
|
169
171
|
* The item it closes: a plain `--emit files` handed its caller 104 bytes where `python
|
|
170
|
-
* build.py` handed over 105, and
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* what can see it.
|
|
172
|
+
* build.py` handed over 105, and a `text=True` capture folds both to the same string.
|
|
173
|
+
* `tests/unit/hook_cli.test.mjs`'s newline test carries a `build` row (through `geneseed
|
|
174
|
+
* build`, which calls this `main` in-process), and that is what can see it.
|
|
174
175
|
*
|
|
175
176
|
* A Buffer chunk passes through untouched: `writeText` owns file bytes and nothing on this
|
|
176
177
|
* path writes binary to a stream, so translating one would be corrupting data rather than
|
package/js/lib/json.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* These are definitions, not an adaptation of somebody else's; see `fs.mjs` for why the
|
|
6
6
|
* measurements against CPython are recorded per function, and why NONE of these is the Node
|
|
7
7
|
* default — the warning the retired `py` prefix used to carry now lives in each docblock.
|
|
8
|
-
* A rule here is a frozen fact about what this tool emits — the
|
|
9
|
-
* `tests/
|
|
8
|
+
* A rule here is a frozen fact about what this tool emits — the written-out tables in
|
|
9
|
+
* `tests/unit/lib_primitives.test.mjs` and `tests/unit/settings_jsonc.test.mjs` pin it.
|
|
10
10
|
*
|
|
11
11
|
* `JsonNumber` is why this module holds both the parser and the renderers: the wrapper is
|
|
12
12
|
* created by `parseJson` and read by `formatValue`, `formatRepr`, `isTruthy` and `deepEquals`, so the
|
|
@@ -305,8 +305,8 @@ function escapeNonAscii(text) {
|
|
|
305
305
|
* Indentation is what makes this tractable: passing `indent` switches Python's
|
|
306
306
|
* separators from `(', ', ': ')` to `(',', ': ')`, which is exactly what
|
|
307
307
|
* `JSON.stringify(v, null, 2)` emits. Verified rather than argued \u2014
|
|
308
|
-
*
|
|
309
|
-
*
|
|
308
|
+
* the former Python suite compared both variants against Python over a corpus of container
|
|
309
|
+
* shapes; `tests/unit/lib_primitives.test.mjs` now pins only this writer's number spellings.
|
|
310
310
|
*/
|
|
311
311
|
export function jsonDumpsIndent(value, { ensureAscii = true } = {}) {
|
|
312
312
|
const text = JSON.stringify(value, null, 2);
|
package/js/lib/paths.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const { X_OK } = constants;
|
|
|
22
22
|
* BMP), and `str.lower()` is not `toLowerCase()` for a handful of characters.
|
|
23
23
|
*
|
|
24
24
|
* THE SEPARATOR CONVERSION IS NOT OPTIONAL, and it was missing until the byte-bearing
|
|
25
|
-
* corpus in
|
|
25
|
+
* corpus in the former Python parity suite asked. `ntpath.normcase` is
|
|
26
26
|
* `s.replace('/', '\\').lower()`, and `PurePath._str_normcase` gets the same conversion for
|
|
27
27
|
* free because `Path` has already done it — so BOTH readings of the reference fold slashes
|
|
28
28
|
* and this did not. Every sort call site feeds it `path.join` output or a bare `readdir`
|
|
@@ -50,7 +50,7 @@ export const normcase = process.platform === 'win32'
|
|
|
50
50
|
* a directory depth, and `golden.py` compared CONTENT rather than order. It became visible
|
|
51
51
|
* the first time a full path list was PRINTED \u2014 `geneseed validate -v` \u2014 against a real
|
|
52
52
|
* skills tree that happens to contain both `skills/geneseed/` and `skills/geneseed-code-review/`.
|
|
53
|
-
*
|
|
53
|
+
* The former Python maintainer-tools test found it and died with the reference it compared
|
|
54
54
|
* against. What HOLDS it now is `tests/unit/maintainer_tools.test.mjs`, and specifically its
|
|
55
55
|
* `theme files are visited in THIS platform's Path collation order` test: it re-derives Python's
|
|
56
56
|
* `sorted(Path)` order for the running platform instead of replaying a recording, so it cannot
|
|
@@ -73,10 +73,14 @@ export function comparePaths(a, b) {
|
|
|
73
73
|
* `_install_move_list` in `js/maintain/uninstall.mjs` \u2014 a CLI-level module that must not import the
|
|
74
74
|
* web tree to reach it. Beside `normcase`, which is the thing it is actually about.
|
|
75
75
|
* `js/web/api.mjs` re-exports it so its existing importers are unchanged.
|
|
76
|
+
*
|
|
77
|
+
* TRAILING SEPARATORS ARE STRIPPED FROM `parent` FIRST. A root is the one path whose canonical
|
|
78
|
+
* spelling ends in one — `D:\` and `/` — and splitting it leaves an empty last segment that no
|
|
79
|
+
* child's segment equals, so nothing was ever `within` a drive root or `/`.
|
|
76
80
|
*/
|
|
77
81
|
export function within(child, parent) {
|
|
78
82
|
const c = normcase(child).split(/[\\/]/);
|
|
79
|
-
const p = normcase(parent).split(/[\\/]/);
|
|
83
|
+
const p = normcase(parent).replace(/[\\/]+$/, '').split(/[\\/]/);
|
|
80
84
|
return p.length <= c.length && p.every((seg, i) => c[i] === seg);
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -86,7 +90,7 @@ export function within(child, parent) {
|
|
|
86
90
|
* `Path` is `WindowsPath` on Windows and `PosixPath` everywhere else, and the two parsers
|
|
87
91
|
* disagree about the two things below. A body that is right on one host is therefore wrong
|
|
88
92
|
* on the other, which is exactly how this one passed a whole Windows suite and failed two
|
|
89
|
-
* cases on the first Linux run of
|
|
93
|
+
* cases on the first Linux run of the former Python pure-function parity suite:
|
|
90
94
|
*
|
|
91
95
|
* * SEPARATORS. `ntpath` treats `\` and `/` alike; `posixpath` treats `\` as an ordinary
|
|
92
96
|
* filename character. `str(PurePosixPath('C:\\x\\bin\\'))` is that string VERBATIM —
|
|
@@ -100,8 +104,8 @@ export function within(child, parent) {
|
|
|
100
104
|
* NOT reproduced, and named here rather than left as a gap nobody wrote down: `//` and
|
|
101
105
|
* `///x` on WINDOWS, where `PureWindowsPath` answers `\\` and `\\\x` and this answers `\`
|
|
102
106
|
* and `\x`. Those are incomplete UNC prefixes and matching them needs ntpath's share-name
|
|
103
|
-
* parsing; both inputs
|
|
104
|
-
*
|
|
107
|
+
* parsing; both inputs were kept OUT of the retired parity corpus instead of sitting in it
|
|
108
|
+
* wrong.
|
|
105
109
|
*
|
|
106
110
|
* The root's separators are still rewritten ONE AT A TIME, never `replace(/[\\/]+/g, sep)`:
|
|
107
111
|
* a UNC root's leading pair is part of it, so collapsing runs turns `//server/share/x` into
|
|
@@ -143,7 +147,8 @@ export function toPlatformPath(s) {
|
|
|
143
147
|
* `context` cell varies it, and changing it in this phase would be an ungated edit to a verb
|
|
144
148
|
* that crossed three phases ago. Recorded in the spec's "Still owed" with this reproduction.
|
|
145
149
|
*
|
|
146
|
-
*
|
|
150
|
+
* It was gated by a corpus in the retired Python pure-function parity suite; nothing has
|
|
151
|
+
* replaced it, and no CLI test can vary the shape.
|
|
147
152
|
*/
|
|
148
153
|
export function isAbsolutePath(s) {
|
|
149
154
|
if (process.platform !== 'win32') return s.startsWith('/');
|
|
@@ -166,27 +171,21 @@ export function isAbsolutePath(s) {
|
|
|
166
171
|
* PATH-removal refutation is aimed at, and the reason this is a real lookup rather than one
|
|
167
172
|
* line of "we are node, we know where node is".
|
|
168
173
|
*
|
|
169
|
-
* The Windows half is the part with behaviour in it, and
|
|
170
|
-
*
|
|
174
|
+
* The Windows half is the part with behaviour in it, and its suffix rules were MEASURED
|
|
175
|
+
* against CPython 3.13 rather than assumed. `PATHEXT` supplies the suffixes, and the
|
|
171
176
|
* returned path carries PATHEXT's OWN SPELLING of the extension rather than the file's
|
|
172
177
|
* (`zzcmd.CMD` for a `zzcmd.cmd` on disk). An extension already in that set means the command
|
|
173
|
-
* is spelled in full.
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* up beside the repo root, this returned null, and the parity corpus said so on the first CI
|
|
185
|
-
* run. The variable is an INPUT, so it is read here rather than baked, and
|
|
186
|
-
* `test_the_curdir_rule_agrees_in_both_environment_states` runs the corpus with it both ways.
|
|
187
|
-
*
|
|
188
|
-
* Gated as a pure function over a seeded directory in `tests/test_pure_function_parity.py`:
|
|
189
|
-
* a cell can only ever observe the one answer this machine's PATH gives.
|
|
178
|
+
* is spelled in full.
|
|
179
|
+
*
|
|
180
|
+
* THE CURRENT DIRECTORY IS NEVER SEARCHED IMPLICITLY, and here this departs from
|
|
181
|
+
* `shutil.which` on purpose. The reference prepends `os.curdir` on Windows unless
|
|
182
|
+
* `NoDefaultCurrentDirectoryInExePath` is defined, and answers an EMPTY PATH entry (a
|
|
183
|
+
* trailing `;` is routine) with a cwd-relative path. Every caller SPAWNS what this returns —
|
|
184
|
+
* `git` from `upgrade`, `java` from `setup`, `node` from the doctor, `npm` from the web server
|
|
185
|
+
* — from whatever directory the user is in, so a `git.exe` committed to an untrusted clone ran
|
|
186
|
+
* the moment `geneseed upgrade` was typed inside it. Both routes are closed; a `.` the user
|
|
187
|
+
* wrote into PATH themselves is still honoured, because that is their choice and not ours.
|
|
188
|
+
* Gated in `tests/unit/lib_primitives.test.mjs`.
|
|
190
189
|
*/
|
|
191
190
|
export function which(cmd, searchPath = null) {
|
|
192
191
|
const win = process.platform === 'win32';
|
|
@@ -209,14 +208,9 @@ export function which(cmd, searchPath = null) {
|
|
|
209
208
|
// then spawns. Measured: the corpus case with a posix-spelled directory was the one that
|
|
210
209
|
// differed, and `path.join` had rewritten the whole path rather than appending to it.
|
|
211
210
|
const joinRaw = (dir, name) => (
|
|
212
|
-
|
|
211
|
+
/[\\/]$/.test(dir) || (win && dir.endsWith(':'))
|
|
213
212
|
? dir + name : dir + path.sep + name);
|
|
214
|
-
const dirs = (searchPath ?? process.env.PATH ?? '').split(path.delimiter);
|
|
215
|
-
// `NeedCurrentDirectoryForExePath(cmd)`: TRUE — so `.` goes in FRONT — unless the variable
|
|
216
|
-
// is defined. Only reachable with a separator-free `cmd`, which is the only way execution
|
|
217
|
-
// gets this far. Inserted before the dedupe, exactly where `shutil.which` inserts it, so a
|
|
218
|
-
// PATH that already names `.` is searched once.
|
|
219
|
-
if (win && process.env.NoDefaultCurrentDirectoryInExePath === undefined) dirs.unshift('.');
|
|
213
|
+
const dirs = (searchPath ?? process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
|
|
220
214
|
const seen = new Set();
|
|
221
215
|
for (const dir of dirs) {
|
|
222
216
|
const key = win ? dir.toLowerCase() : dir;
|
package/js/lib/proc.mjs
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* that module and six of its spawns used it, so the four spawn sites in `js/inspect/checks-authoring.mjs`,
|
|
12
12
|
* `js/hosts/hooks.mjs`, `js/maintain/setup.mjs` and `js/hosts/link.mjs` each had to remember the flag on their
|
|
13
13
|
* own. Two of them did not — and the miss is BYTE-INVISIBLE: a console window is not
|
|
14
|
-
* something the CLI prints, so every
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* something the CLI prints, so every cell of `tests/golden.mjs --cli` stays green while the
|
|
15
|
+
* windows pop. The property under test never reaches the comparison, which is why a byte
|
|
16
|
+
* comparison cannot be the gate here. A shared constant plus
|
|
17
17
|
* `tests/unit/spawn_hygiene.test.mjs` is what makes the next spawn site inherit the answer
|
|
18
18
|
* instead of re-deciding it.
|
|
19
19
|
*
|