geneseed 4.5.0 → 4.7.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 +30 -0
- package/README.md +6 -6
- package/SHIPPED.md +1 -1
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +3 -2
- package/adapters/opencode/plugins/geneseed-context.js +108 -61
- package/adapters/opencode/plugins/geneseed-guard.js +33 -28
- package/adapters/opencode/plugins/geneseed-learn.js +1 -1
- package/docs/guides/install-clone.md +1 -1
- package/docs/guides/install.md +2 -2
- package/docs/reference/troubleshoot-install.md +5 -5
- package/harness.config.json +1 -1
- package/js/README.md +5 -4
- package/js/build/bundle.mjs +11 -2
- package/js/build/driver.mjs +1 -10
- package/js/build/emit-claude.mjs +7 -22
- package/js/build/emit-common.mjs +16 -9
- package/js/build/emit-opencode.mjs +5 -11
- package/js/build/generate.mjs +1 -1
- package/js/hosts/hooks-context.mjs +1 -4
- package/js/hosts/hooks-done.mjs +23 -6
- package/js/hosts/hooks-learn.mjs +5 -5
- package/js/hosts/hooks.mjs +28 -15
- package/js/hosts/hosts.mjs +2 -0
- package/js/hosts/installs.mjs +45 -10
- package/js/hosts/native.mjs +5 -13
- package/js/hosts/opencode.mjs +37 -22
- package/js/hosts/owned.mjs +19 -5
- package/js/hosts/settings.mjs +27 -18
- package/js/hosts/shim.mjs +4 -3
- package/js/hosts/usage.mjs +4 -3
- package/js/inspect/checks-authoring.mjs +9 -7
- package/js/inspect/checks-probe.mjs +14 -17
- package/js/inspect/checks-repo.mjs +13 -54
- package/js/inspect/cost.mjs +5 -6
- package/js/inspect/doctor.mjs +2 -2
- package/js/inspect/scan.mjs +9 -5
- package/js/inspect/scorecard-svg.mjs +3 -3
- package/js/inspect/scorecard-yaml.mjs +407 -0
- package/js/inspect/scorecard.mjs +7 -405
- package/js/inspect/status.mjs +14 -13
- package/js/lib/node-floor.mjs +4 -3
- package/js/maintain/migrate.mjs +2 -3
- package/js/maintain/uninstall.mjs +20 -26
- package/js/web/actions.mjs +2 -2
- package/js/web/api.mjs +36 -24
- package/js/web/gates-api.mjs +5 -8
- package/js/web/usage-api.mjs +14 -17
- package/package.json +2 -2
- package/web/dist/assets/{Activity-BZE6REef.js → Activity-Dj0izJwj.js} +1 -1
- package/web/dist/assets/{ActivityDetail-DCB8Cr2W.js → ActivityDetail-wjq5W02K.js} +1 -1
- package/web/dist/assets/Dashboard-UMbgHbzF.js +2 -0
- package/web/dist/assets/{Docs-D30CiqYy.js → Docs-D3ABE_ds.js} +3 -3
- package/web/dist/assets/{GreenhouseView-Djt-4oO5.js → GreenhouseView-C238LWBD.js} +1 -1
- package/web/dist/assets/{Installs-B3bw5ckh.js → Installs-XSm0a_zQ.js} +2 -2
- package/web/dist/assets/JournalView-BUj4Ff-6.js +1 -0
- package/web/dist/assets/{Laws-B9day2FV.js → Laws-yGBJFKgY.js} +1 -1
- package/web/dist/assets/{Library-BpIrLH-O.js → Library-CkaPVUYU.js} +1 -1
- package/web/dist/assets/{Loops-CxH14kyE.js → Loops-CexwOARr.js} +1 -1
- package/web/dist/assets/{OperatorHudView-otOz6Gha.js → OperatorHudView-D5qTQat0.js} +1 -1
- package/web/dist/assets/{Personal-abL6089y.js → Personal-B_Vx9a7a.js} +1 -1
- package/web/dist/assets/Tabs-YZbGqWPW.js +1 -0
- package/web/dist/assets/drawable-BoQnwVpr.js +1 -0
- package/web/dist/assets/index-BmbWeoTM.js +2 -0
- package/web/dist/assets/{index-CO-9wMJY.css → index-BoG1M8Xy.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/Dashboard-B4Drq5wp.js +0 -2
- package/web/dist/assets/JournalView-OWhx9YKg.js +0 -1
- package/web/dist/assets/Tabs-DjWN0i7A.js +0 -1
- package/web/dist/assets/drawable-C2ZsBZCv.js +0 -1
- package/web/dist/assets/index-CdpLpPQN.js +0 -2
package/js/hosts/opencode.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { jsonDumps, jsonDumpsIndent, parseJson } from '../lib/json.mjs';
|
|
|
28
28
|
import { comparePaths } from '../lib/paths.mjs';
|
|
29
29
|
import { sourceReleaseVersion } from '../build/version.mjs';
|
|
30
30
|
import { aliasesOf } from '../build/source.mjs';
|
|
31
|
+
import { lstripNewlines } from './settings.mjs';
|
|
31
32
|
import {
|
|
32
33
|
firstBlockquote, isUserInvokedOnly, skillDescription, stripSkillBodyLinks, pointSideFiles,
|
|
33
34
|
pushOverrideLines,
|
|
@@ -295,7 +296,7 @@ export function writePrimaryAgent(cfg, agentsDir, overrides, claim = (_dest) =>
|
|
|
295
296
|
|| !statSync(cfg.primaryAgentSrc).isFile()) {
|
|
296
297
|
return null;
|
|
297
298
|
}
|
|
298
|
-
const body = readText(cfg.primaryAgentSrc)
|
|
299
|
+
const body = lstripNewlines(readText(cfg.primaryAgentSrc));
|
|
299
300
|
const desc = 'Primary orchestrator — works by the harness Rules and delegates to the '
|
|
300
301
|
+ 'capability subagents.';
|
|
301
302
|
const fm = [`description: ${jsonDumps(desc)}`, 'mode: primary', 'color: primary'];
|
|
@@ -309,19 +310,43 @@ export function writePrimaryAgent(cfg, agentsDir, overrides, claim = (_dest) =>
|
|
|
309
310
|
return dest;
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
/**
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
/**
|
|
314
|
+
* The top-level skills of `items` (`src/skills/<stem>.md`, no `_` partials) as ordered
|
|
315
|
+
* `[stem, text]` pairs — the one parse both command writers below share.
|
|
316
|
+
*/
|
|
317
|
+
function topLevelSkills(cfg, items) {
|
|
318
|
+
/** @type {[string, string][]} */
|
|
319
|
+
const found = [];
|
|
317
320
|
for (const { text, src } of items) {
|
|
318
321
|
if (text === null) continue;
|
|
319
322
|
const sp = path.relative(cfg.src, src).split(path.sep);
|
|
320
323
|
if (sp.length === 2 && sp[0] === 'skills' && sp[1].endsWith('.md')
|
|
321
324
|
&& !sp[1].startsWith('_')) {
|
|
322
|
-
|
|
325
|
+
found.push([sp[1].slice(0, -3), text]);
|
|
323
326
|
}
|
|
324
327
|
}
|
|
328
|
+
return found;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Both command writers' default `skillDirOf`: the skill's own directory beside `commandDir`. */
|
|
332
|
+
const siblingSkillDir = (commandDir) => (n) =>
|
|
333
|
+
path.join(path.dirname(commandDir), 'skills', n).split(path.sep).join('/');
|
|
334
|
+
|
|
335
|
+
/** A command has no base directory, so a side-file pointer names the skill's own. */
|
|
336
|
+
function commandBody(text, stem, skillDir) {
|
|
337
|
+
return stripSkillBodyLinks(pointSideFiles(lstripNewlines(text), stem, skillDir));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function writeCommandFile(dest, desc, body) {
|
|
341
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
342
|
+
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${body}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** `_build_emit._write_command_layer` — the opt-in /slash commands. */
|
|
346
|
+
export function writeCommandLayer(cfg, items, commandDir, claim = (_dest) => true,
|
|
347
|
+
skillDirOf = siblingSkillDir(commandDir)) {
|
|
348
|
+
if (!truthyEnv('GENESEED_COMMANDS')) return [];
|
|
349
|
+
const byName = new Map(topLevelSkills(cfg, items));
|
|
325
350
|
const written = [];
|
|
326
351
|
for (const [name, skill] of Object.entries(COMMAND_SET)) {
|
|
327
352
|
// A command runs a skill; `--exclude-skills` took that skill out, so the command goes too.
|
|
@@ -333,10 +358,7 @@ export function writeCommandLayer(cfg, items, commandDir, claim = (_dest) => tru
|
|
|
333
358
|
const dest = path.join(commandDir, `${name}.md`);
|
|
334
359
|
if (!claim(dest)) continue;
|
|
335
360
|
const desc = firstBlockquote(text);
|
|
336
|
-
|
|
337
|
-
const body = stripSkillBodyLinks(pointSideFiles(text.replace(/^\n+/, ''), skill, skillDirOf(skill)));
|
|
338
|
-
mkdirSync(path.dirname(dest), { recursive: true });
|
|
339
|
-
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${body}`);
|
|
361
|
+
writeCommandFile(dest, desc, commandBody(text, skill, skillDirOf(skill)));
|
|
340
362
|
written.push(dest);
|
|
341
363
|
}
|
|
342
364
|
return written;
|
|
@@ -358,22 +380,15 @@ export function writeCommandLayer(cfg, items, commandDir, claim = (_dest) => tru
|
|
|
358
380
|
* (Task 4) — it keeps its SKILL.md and does not need a second, command-shaped copy.
|
|
359
381
|
*/
|
|
360
382
|
export function writeAliasCommands(cfg, items, commandDir, claim = (_dest) => true,
|
|
361
|
-
skillDirOf = (
|
|
362
|
-
alsoUserOnly = false) {
|
|
383
|
+
skillDirOf = siblingSkillDir(commandDir), alsoUserOnly = false) {
|
|
363
384
|
const written = [];
|
|
364
|
-
for (const
|
|
365
|
-
|
|
366
|
-
const sp = path.relative(cfg.src, src).split(path.sep);
|
|
367
|
-
if (sp.length !== 2 || sp[0] !== 'skills' || !sp[1].endsWith('.md') || sp[1].startsWith('_')) continue;
|
|
368
|
-
const stem = sp[1].slice(0, -3);
|
|
369
|
-
const body = stripSkillBodyLinks(pointSideFiles(text.replace(/^\n+/, ''), stem, skillDirOf(stem)));
|
|
385
|
+
for (const [stem, text] of topLevelSkills(cfg, items)) {
|
|
386
|
+
const body = commandBody(text, stem, skillDirOf(stem));
|
|
370
387
|
const names = alsoUserOnly && isUserInvokedOnly(text) ? [...aliasesOf(text), stem] : aliasesOf(text);
|
|
371
388
|
for (const name of names) {
|
|
372
389
|
const dest = path.join(commandDir, `${name}.md`);
|
|
373
390
|
if (!claim(dest)) continue;
|
|
374
|
-
|
|
375
|
-
const desc = name === stem ? skillDescription(text) : `Alias of ${stem}.`;
|
|
376
|
-
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${body}`);
|
|
391
|
+
writeCommandFile(dest, name === stem ? skillDescription(text) : `Alias of ${stem}.`, body);
|
|
377
392
|
written.push(dest);
|
|
378
393
|
}
|
|
379
394
|
}
|
package/js/hosts/owned.mjs
CHANGED
|
@@ -144,11 +144,25 @@ export function hashesOf(doc) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* `
|
|
150
|
-
*
|
|
151
|
-
*
|
|
147
|
+
* An OpenCode manifest's `skill_denies` — the `permission.skill` denies Geneseed itself added
|
|
148
|
+
* (`opencodeLayer` records them; a deny the user wrote, even on a Geneseed skill, is never in
|
|
149
|
+
* it) — or `[]` when the manifest, or the key, is absent or malformed, which sweeps nothing.
|
|
150
|
+
* Unlike the Law IV `permission.bash` gates (permanent, never taken back — see
|
|
151
|
+
* `reconcileOpencodePermission`), a skill-visibility deny is reversible: once uninstalled there
|
|
152
|
+
* is no skill left to hide, and the name is the user's to reuse. One home because rebuild's
|
|
153
|
+
* deny sweep and uninstall must agree on what Geneseed owns.
|
|
154
|
+
*/
|
|
155
|
+
export function recordedDenies(doc) {
|
|
156
|
+
const d = doc && doc.skill_denies;
|
|
157
|
+
return Array.isArray(d) ? d.filter((n) => typeof n === 'string') : [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Where one owned file stands against its recorded hash — the table rebuild, prune and
|
|
162
|
+
* uninstall branch on so none of them clobbers a user's edit to a Geneseed-owned file.
|
|
163
|
+
* `'unknown'` is deliberately distinct from `'unmodified'`: a manifest written before hashes
|
|
164
|
+
* were recorded has no entry for this rel, and must not be read as "proven untouched" — a
|
|
165
|
+
* manifest without `hashes` keeps the pre-hash behaviour exactly.
|
|
152
166
|
*/
|
|
153
167
|
export function ownedState(root, rel, hashes) {
|
|
154
168
|
if (!Object.hasOwn(hashes, rel)) return 'unknown';
|
package/js/hosts/settings.mjs
CHANGED
|
@@ -103,11 +103,16 @@ const consentRuleOn = (d, excluded = []) =>
|
|
|
103
103
|
* like `-d`, so it is deliberately NOT a key here; `-D --force`/`--delete -f`/`-d --force`/
|
|
104
104
|
* `-df` are the same act again and are a known ceiling this does not chase); `checkout
|
|
105
105
|
* -f`/`--force` and `switch -f`/`--force`/`--discard-changes` (git's own `switch -h` lists
|
|
106
|
-
* `-f, --force` and `--discard-changes` as two separate options); `
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
106
|
+
* `-f, --force` and `--discard-changes` as two separate options); `reflog expire`; `gc
|
|
107
|
+
* --prune` (matched bare, since a `--prune` with no `=now` still eventually prunes); and `push
|
|
108
|
+
* *--delete*` / `push * :*` (a literal space before the colon, so `push origin HEAD:main`, an
|
|
109
|
+
* ordinary refspec, does not match).
|
|
110
|
+
*
|
|
111
|
+
* `git worktree remove -f`/`--force` was a key here and was REMOVED (user decision, 2026-10-17:
|
|
112
|
+
* worktree removal is routine housekeeping, not a Law IV deletion). Removal from this list also
|
|
113
|
+
* removes the four globs from `OWNED_BASH`, so an `opencode.json` written before keeps them —
|
|
114
|
+
* neither taken back nor reported as stranded, since Geneseed no longer claims them. Its user
|
|
115
|
+
* deletes them by hand (the CHANGELOG upgrade note says so).
|
|
111
116
|
*
|
|
112
117
|
* `git restore` is DELIBERATELY NOT HERE. Its rule is "ask unless `--staged` appears ALONE" —
|
|
113
118
|
* `--staged --worktree` together still discards the working tree — and a glob has no negation,
|
|
@@ -128,8 +133,6 @@ const LAW_IV_BASH = ['git push --force*', 'git push -f*', 'git push *--force*',
|
|
|
128
133
|
'git checkout -f*', 'git checkout * -f*', 'git checkout --force*', 'git checkout * --force*',
|
|
129
134
|
'git switch -f*', 'git switch * -f*', 'git switch --force*', 'git switch * --force*',
|
|
130
135
|
'git switch --discard-changes*', 'git switch * --discard-changes*',
|
|
131
|
-
'git worktree remove -f*', 'git worktree remove --force*',
|
|
132
|
-
'git worktree remove * -f*', 'git worktree remove * --force*',
|
|
133
136
|
'git reflog expire*', 'git gc --prune*', 'git gc * --prune*',
|
|
134
137
|
'git stash drop*', 'git stash clear*',
|
|
135
138
|
'git push *--delete*', 'git push * :*'];
|
|
@@ -145,7 +148,8 @@ const LAW_IV_BASH = ['git push --force*', 'git push -f*', 'git push *--force*',
|
|
|
145
148
|
* `-u origin`/`--set-upstream origin`/any remote through its leading `*`; the second key covers
|
|
146
149
|
* the `refs/heads/` spelling the same way.
|
|
147
150
|
*
|
|
148
|
-
* ORDER IS LOAD-BEARING. `
|
|
151
|
+
* ORDER IS LOAD-BEARING. `PUSH_CHAIN` (which `defaultPermission` and `OWNED_BASH` both read)
|
|
152
|
+
* puts these AFTER `git push*`'s ask (so a loop
|
|
149
153
|
* push is not swallowed by the blanket consent ask) and BEFORE `LAW_IV_BASH` (so `findLast`
|
|
150
154
|
* — OpenCode's own `permission.bash` evaluator — lets a force push, a `+refspec`, or a
|
|
151
155
|
* `--delete`/bare-`:` push still ask even when the target is `loop/*`; those LAW_IV_BASH keys
|
|
@@ -222,18 +226,22 @@ const LOOP_PUSH_ASK_FLAG_EXTRA = [
|
|
|
222
226
|
];
|
|
223
227
|
const LOOP_PUSH_ASK_TRAILING = ['git push * HEAD:loop/* ?*', 'git push * HEAD:refs/heads/loop/* ?*'];
|
|
224
228
|
|
|
229
|
+
/**
|
|
230
|
+
* The tiers after `git push*`, in the `findLast` order the docblocks above depend on — ONE
|
|
231
|
+
* ordered table, so `defaultPermission` and `OWNED_BASH` cannot disagree about it.
|
|
232
|
+
*/
|
|
233
|
+
const PUSH_CHAIN = [
|
|
234
|
+
[LOOP_PUSH_ALLOW, 'allow'], [LOOP_PUSH_ASK_EXTRA, 'ask'], [LOOP_PUSH_ALLOW_FLAG, 'allow'],
|
|
235
|
+
[LOOP_PUSH_ASK_FLAG_EXTRA, 'ask'], [LOOP_PUSH_ASK_TRAILING, 'ask'], [LAW_IV_BASH, 'ask'],
|
|
236
|
+
];
|
|
237
|
+
|
|
225
238
|
function defaultPermission(doctrines = null, excluded = []) {
|
|
226
239
|
const bash = { 'rm -rf *': 'ask' };
|
|
227
240
|
if (consentRuleOn(doctrines, excluded)) {
|
|
228
241
|
bash['git commit*'] = 'ask';
|
|
229
242
|
bash['git push*'] = 'ask';
|
|
230
243
|
}
|
|
231
|
-
for (const k of
|
|
232
|
-
for (const k of LOOP_PUSH_ASK_EXTRA) bash[k] = 'ask';
|
|
233
|
-
for (const k of LOOP_PUSH_ALLOW_FLAG) bash[k] = 'allow';
|
|
234
|
-
for (const k of LOOP_PUSH_ASK_FLAG_EXTRA) bash[k] = 'ask';
|
|
235
|
-
for (const k of LOOP_PUSH_ASK_TRAILING) bash[k] = 'ask';
|
|
236
|
-
for (const k of LAW_IV_BASH) bash[k] = 'ask';
|
|
244
|
+
for (const [keys, verdict] of PUSH_CHAIN) for (const k of keys) bash[k] = verdict;
|
|
237
245
|
return { bash };
|
|
238
246
|
}
|
|
239
247
|
|
|
@@ -245,8 +253,7 @@ function defaultPermission(doctrines = null, excluded = []) {
|
|
|
245
253
|
* the pack returns, and to name it when this build no longer wants it but will not remove it.
|
|
246
254
|
*/
|
|
247
255
|
const OWNED_BASH = ['rm -rf *', 'git commit*', 'git push*',
|
|
248
|
-
...
|
|
249
|
-
...LOOP_PUSH_ASK_TRAILING, ...LAW_IV_BASH];
|
|
256
|
+
...PUSH_CHAIN.flatMap(([keys]) => keys)];
|
|
250
257
|
|
|
251
258
|
/**
|
|
252
259
|
* Bring an ALREADY-WRITTEN `permission` block back into line with the pack selection, and
|
|
@@ -626,7 +633,8 @@ export function mergeOpencodeJson(p, agentPath, doctrines = null, excluded = [],
|
|
|
626
633
|
// skill reconcile above never ran either — the unwired skill denies are folded into THIS
|
|
627
634
|
// one WARN rather than going unmentioned (a dedicated `skillOpaque` needs `perm` to be a
|
|
628
635
|
// dict to even look at `perm.skill`, which this case by definition is not).
|
|
629
|
-
const
|
|
636
|
+
const wanted = defaultPermission(doctrines, excluded).bash;
|
|
637
|
+
const names = [...OWNED_BASH.filter((k) => has(wanted, k)),
|
|
630
638
|
...(!permIsObject ? skillNames : [])];
|
|
631
639
|
process.stderr.write(`[geneseed] WARN: "permission" in ${path.basename(target)} `
|
|
632
640
|
+ `${opaque}, so Geneseed cannot wire its own gates into it — including `
|
|
@@ -1236,7 +1244,8 @@ export function managedBlockRead(p, blockId = 'GENESEED') {
|
|
|
1236
1244
|
return stripNewlines(inner.slice(0, inner.indexOf(end)));
|
|
1237
1245
|
}
|
|
1238
1246
|
|
|
1239
|
-
|
|
1247
|
+
/** `str.lstrip("\n")`; the one copy — `native.mjs` and `opencode.mjs` import it from here. */
|
|
1248
|
+
export function lstripNewlines(s) {
|
|
1240
1249
|
return s.replace(/^\n+/, '');
|
|
1241
1250
|
}
|
|
1242
1251
|
|
package/js/hosts/shim.mjs
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* `js/build/driver.mjs`, and through it the whole generator, for the two-line
|
|
10
10
|
* `hookRunnerEntry`, which now lives here beside the `hookPrefix` that consumes its answer.
|
|
11
11
|
*
|
|
12
|
-
* Keep this module's imports to `node:` builtins, `js/lib/fs.mjs
|
|
13
|
-
*
|
|
12
|
+
* Keep this module's imports to `node:` builtins, `js/lib/fs.mjs`, `./hosts.mjs` and
|
|
13
|
+
* `./hooks-prims.mjs` (whose only import, `js/lib/paths.mjs`, `./hosts.mjs` already loads):
|
|
14
|
+
* being cheap to load is its whole reason to exist.
|
|
14
15
|
*/
|
|
15
16
|
import {
|
|
16
17
|
chmodSync, existsSync, mkdirSync, realpathSync, renameSync, statSync, writeFileSync,
|
|
@@ -19,6 +20,7 @@ import path from 'node:path';
|
|
|
19
20
|
import os from 'node:os';
|
|
20
21
|
import { readText, isOsError } from '../lib/fs.mjs';
|
|
21
22
|
import { expanduser } from './hosts.mjs';
|
|
23
|
+
import { isFile } from './hooks-prims.mjs';
|
|
22
24
|
|
|
23
25
|
/** The checkout this module ships in — `js/build/source.mjs`'s `ROOT`, computed locally so the
|
|
24
26
|
* shim does not import the source loader. */
|
|
@@ -295,7 +297,6 @@ export function claudeHookShell(env = process.env, platform = process.platform)
|
|
|
295
297
|
*/
|
|
296
298
|
export function claudeGitBashPath(env = process.env) {
|
|
297
299
|
const read = (name) => Object.entries(env).find(([k]) => k.toUpperCase() === name)?.[1];
|
|
298
|
-
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
299
300
|
const forced = read('CLAUDE_CODE_GIT_BASH_PATH');
|
|
300
301
|
if (forced && GIT_BASH_NAMES.has(path.basename(forced).toLowerCase()) && isFile(forced)) {
|
|
301
302
|
return forced;
|
package/js/hosts/usage.mjs
CHANGED
|
@@ -26,8 +26,9 @@ const NAME_RE = /^[A-Za-z0-9][\w.:@/-]{0,99}$/;
|
|
|
26
26
|
/**
|
|
27
27
|
* `{kind, name}` for a skill or subagent call, else null. Reads the Claude shape (`tool_name` /
|
|
28
28
|
* `tool_input`: `Skill` → `skill`, `Agent` or the older `Task` → `subagent_type`), OpenCode's
|
|
29
|
-
* (`skill` → `name`, `task` → `subagent_type
|
|
30
|
-
* tool whose name contains "skill"): Bob's skill
|
|
29
|
+
* (`skill` → `name`, `task` → `subagent_type`; V2 hooks see the pre-alias `subagent` → `agent`)
|
|
30
|
+
* and, best-effort, Bob's (`tool` / `input`, any tool whose name contains "skill"): Bob's skill
|
|
31
|
+
* tool name is unverified, no Bob runs here.
|
|
31
32
|
*/
|
|
32
33
|
export function usageOf(payload) {
|
|
33
34
|
if (!payload || typeof payload !== 'object') return null;
|
|
@@ -38,7 +39,7 @@ export function usageOf(payload) {
|
|
|
38
39
|
if (!tool.startsWith('mcp') && tool.includes('skill')) {
|
|
39
40
|
kind = 'skill';
|
|
40
41
|
name = ti.skill ?? ti.name ?? ti.skill_name;
|
|
41
|
-
} else if (tool === 'agent' || tool === 'task') {
|
|
42
|
+
} else if (tool === 'agent' || tool === 'task' || tool === 'subagent') {
|
|
42
43
|
kind = 'agent';
|
|
43
44
|
name = ti.subagent_type ?? ti.agent;
|
|
44
45
|
}
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { PLUGIN_SRC, SRC } from '../build/source.mjs';
|
|
17
|
+
import { HOSTS } from '../hosts/hosts.mjs';
|
|
17
18
|
import { descBlockProblem, firstBlockquote } from '../hosts/native.mjs';
|
|
18
19
|
import { readText } from '../lib/fs.mjs';
|
|
20
|
+
import { blankCodeSpans } from '../lib/text.mjs';
|
|
19
21
|
import { which } from '../lib/paths.mjs';
|
|
20
22
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
21
23
|
import {
|
|
@@ -25,15 +27,15 @@ import {
|
|
|
25
27
|
import { constitutionProblems, leanBlockProblems } from './checks-constitution.mjs';
|
|
26
28
|
import { countTableProblems } from './checks-counts.mjs';
|
|
27
29
|
import { catalogProblems } from '../loop/catalog.mjs';
|
|
28
|
-
import {
|
|
30
|
+
import { globSorted, isDir, isFile, rglob } from './scan.mjs';
|
|
29
31
|
import { spawnSync } from 'node:child_process';
|
|
30
32
|
|
|
31
|
-
// The
|
|
32
|
-
//
|
|
33
|
-
// reference, status and update modules,
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
const HARNESS_IDS = '
|
|
33
|
+
// The harness ids `js/web/docs.mjs`'s `HARNESS_OPEN_RE` accepts — its `HARNESSES`, keyed by the
|
|
34
|
+
// same host ids as `HOSTS`. Not imported from there: `js/web/docs.mjs` pulls in the web daemon's
|
|
35
|
+
// CLI reference, status and update modules, a weight every `doctor --all` would pay for one id
|
|
36
|
+
// list. `HOSTS` is the ids' own home and already in doctor's import closure. Alternation order
|
|
37
|
+
// does not matter: the pattern is anchored at both ends.
|
|
38
|
+
const HARNESS_IDS = HOSTS.map((h) => h.host).join('|');
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* The comment bodies (trimmed) that are known to be FUNCTIONAL — consumed by the render, a
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
* `GENESEED_GATE_PROBE=1` keeps the probe's asks out of the gate ledger (`ledger()` in
|
|
26
26
|
* `js/hosts/hooks.mjs`), so `status` never counts a doctor run as a refusal.
|
|
27
27
|
*/
|
|
28
|
-
import { existsSync
|
|
29
|
-
import os from 'node:os';
|
|
28
|
+
import { existsSync } from 'node:fs';
|
|
30
29
|
import path from 'node:path';
|
|
31
30
|
import { pathToFileURL } from 'node:url';
|
|
32
31
|
import { liveRecordedHooks } from '../hosts/hosts.mjs';
|
|
@@ -34,7 +33,7 @@ import { claudeCfg, installState, installTargets } from '../hosts/installs.mjs';
|
|
|
34
33
|
import { SHIM_MARK, claudeGitBashPath } from '../hosts/shim.mjs';
|
|
35
34
|
import { which } from '../lib/paths.mjs';
|
|
36
35
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
37
|
-
import { NOTE, withTempDir } from './scan.mjs';
|
|
36
|
+
import { NOTE, withTempDir, withTempDirAsync } from './scan.mjs';
|
|
38
37
|
import { spawnSync } from 'node:child_process';
|
|
39
38
|
|
|
40
39
|
/**
|
|
@@ -43,7 +42,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
43
42
|
* runs everything through `tool-gate --host bob`. OpenCode's guard is an in-process plugin with
|
|
44
43
|
* no command: `guardProbe` below imports it instead.
|
|
45
44
|
*/
|
|
46
|
-
|
|
45
|
+
const PROBED_VERB = { claude: 'git-gate', openclaude: 'git-gate', bob: 'tool-gate' };
|
|
47
46
|
|
|
48
47
|
export const BAD_COMMAND = 'git reset --hard HEAD';
|
|
49
48
|
export const GOOD_COMMAND = 'git status';
|
|
@@ -51,7 +50,7 @@ export const GOOD_COMMAND = 'git status';
|
|
|
51
50
|
const PROBE_TIMEOUT_MS = 20_000;
|
|
52
51
|
|
|
53
52
|
/** A PreToolUse payload in Claude's shape — the shape every probed host's gate reads. */
|
|
54
|
-
|
|
53
|
+
function probePayload(command, cwd) {
|
|
55
54
|
return JSON.stringify({
|
|
56
55
|
session_id: 'geneseed-probe', hook_event_name: 'PreToolUse', tool_name: 'Bash',
|
|
57
56
|
tool_input: { command }, cwd,
|
|
@@ -64,7 +63,7 @@ export function probePayload(command, cwd) {
|
|
|
64
63
|
* `shell: "powershell"` wins; Bob runs `cmd /c` on Windows; Claude/OpenClaude run Git Bash
|
|
65
64
|
* there (`claudeGitBashPath`, the rule `claudeHookShell` uses); `sh -c` everywhere else.
|
|
66
65
|
*/
|
|
67
|
-
|
|
66
|
+
function probeShell(host, handler, platform = process.platform, env = process.env) {
|
|
68
67
|
const cmd = handler.command;
|
|
69
68
|
if (handler.shell === 'powershell') {
|
|
70
69
|
const ps = which('powershell') || which('pwsh');
|
|
@@ -173,10 +172,10 @@ export function gateProbe(targets = installTargets(), { platform = process.platf
|
|
|
173
172
|
|
|
174
173
|
// ---- OpenCode: the guard plugin, probed in-process ------------------------------------------
|
|
175
174
|
|
|
176
|
-
|
|
175
|
+
const GUARD_PLUGIN = 'geneseed-guard.js';
|
|
177
176
|
/** The guard blocks catastrophic shell (`SHELL_BLOCK_RE`); destructive git is the permission
|
|
178
177
|
* prompt's job there, and only WARNS in the guard — so its must-refuse payload is this one. */
|
|
179
|
-
|
|
178
|
+
const GUARD_BAD_COMMAND = 'rm -rf /';
|
|
180
179
|
|
|
181
180
|
/** The guard plugin of an OpenCode install: `<global>/plugins/` or `<repo>/.opencode/plugins/`
|
|
182
181
|
* (`emitOpencodeInto` copies the plugins to `<dir>/plugins`). */
|
|
@@ -203,7 +202,7 @@ async function quietProbe(fn) {
|
|
|
203
202
|
* (`setupV2`'s docblock). An unexpected throw is not a refusal — it is rethrown, and the caller
|
|
204
203
|
* reports the guard as broken.
|
|
205
204
|
*/
|
|
206
|
-
|
|
205
|
+
async function runGuard(mod, cwd) {
|
|
207
206
|
const v1 = async (command) => {
|
|
208
207
|
const hooks = await mod.server({ worktree: cwd, directory: cwd });
|
|
209
208
|
try {
|
|
@@ -233,7 +232,7 @@ export async function runGuard(mod, cwd) {
|
|
|
233
232
|
}
|
|
234
233
|
|
|
235
234
|
/** The verdict for one API's pair — null when sound. */
|
|
236
|
-
|
|
235
|
+
function guardVerdict([badRefused, goodRefused]) {
|
|
237
236
|
if (!badRefused) return `registered but not refusing — \`${GUARD_BAD_COMMAND}\` went through`;
|
|
238
237
|
if (goodRefused) return `refuses everything — \`${GOOD_COMMAND}\` was refused too`;
|
|
239
238
|
return null;
|
|
@@ -269,13 +268,17 @@ export async function guardProbe(targets = installTargets(), env = process.env)
|
|
|
269
268
|
}
|
|
270
269
|
let verdicts;
|
|
271
270
|
try {
|
|
271
|
+
// ESM under OpenCode's typeless package.json: parses only with syntax detection (Node >=
|
|
272
|
+
// 22.7, the floor). Its MODULE_TYPELESS_PACKAGE_JSON warning fires a tick later, inside
|
|
273
|
+
// quietProbe's stderr swap, so the doctor never prints it.
|
|
272
274
|
const mod = (await import(pathToFileURL(file).href)).default;
|
|
273
275
|
if (mod?.id !== 'geneseed-guard' || typeof mod.server !== 'function' || typeof mod.setup !== 'function') {
|
|
274
276
|
problems.push(`${label}: not probed — it does not export the Geneseed guard `
|
|
275
277
|
+ '({ id, server, setup }) — run: geneseed rebuild-all');
|
|
276
278
|
continue;
|
|
277
279
|
}
|
|
278
|
-
verdicts = await withTempDirAsync(
|
|
280
|
+
verdicts = await withTempDirAsync(
|
|
281
|
+
(tmp) => quietProbe(() => runGuard(mod, tmp)), 'geneseed-probe-');
|
|
279
282
|
} catch (e) {
|
|
280
283
|
problems.push(`${label}: does not run (${e?.message || e})`);
|
|
281
284
|
continue;
|
|
@@ -287,9 +290,3 @@ export async function guardProbe(targets = installTargets(), env = process.env)
|
|
|
287
290
|
}
|
|
288
291
|
return { problems, probed };
|
|
289
292
|
}
|
|
290
|
-
|
|
291
|
-
/** `withTempDir` for an async body: the dir outlives the promise, then goes. */
|
|
292
|
-
async function withTempDirAsync(fn) {
|
|
293
|
-
const tmp = mkdtempSync(path.join(os.tmpdir(), 'geneseed-probe-'));
|
|
294
|
-
try { return await fn(tmp); } finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
295
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The checks over the REPO's own records — the registry, committed secrets, the hook shim
|
|
3
|
-
*
|
|
2
|
+
* The checks over the REPO's own records — the registry, committed secrets, the hook shim,
|
|
3
|
+
* the vendored skill folders (their pins and their SKILL.md frontmatter), skill aliases and
|
|
4
|
+
* side files, the `js/README.md` module map, the host tables, and the scorecard ratchet.
|
|
4
5
|
*
|
|
5
6
|
* Same contract as `checks-build.mjs`: an array of problem strings, empty when clean, one
|
|
6
7
|
* planted fault per check in the gate. This is the group that reads what the repo SAYS about
|
|
@@ -10,8 +11,8 @@ import path from 'node:path';
|
|
|
10
11
|
import {
|
|
11
12
|
ALIASES_RE, PLUGIN_SRC, RETIRED_SKILL_IDS, ROOT, SRC, THEMES, WORKFLOW_SRC, aliasesOf, knownSkillIds,
|
|
12
13
|
} from '../build/source.mjs';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { HOSTS } from '../hosts/hosts.mjs';
|
|
15
|
+
import { installTargets, installsReferencingShim } from '../hosts/installs.mjs';
|
|
15
16
|
import { VENDORED_SKILL_DIRS } from '../hosts/native.mjs';
|
|
16
17
|
import { COMMAND_SET } from '../hosts/opencode.mjs';
|
|
17
18
|
import { hookShimPath, shimDeadPaths } from '../hosts/shim.mjs';
|
|
@@ -41,12 +42,8 @@ function registryKeys() {
|
|
|
41
42
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
42
43
|
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
43
44
|
|
|
44
|
-
// `ENTITY_STATUSES
|
|
45
|
-
//
|
|
46
|
-
// doctor's authoring gates are what validate them, and they belong where Python keeps
|
|
47
|
-
// them (`_harness_tui`) now that the catalog reads them too. The gates below are
|
|
48
|
-
// unchanged, which is what licensed the move: a copy would have stopped being the
|
|
49
|
-
// value under test the first time one of them was edited.
|
|
45
|
+
// `ENTITY_STATUSES` is imported from `js/inspect/inventory.mjs`, not copied: the catalog reads
|
|
46
|
+
// it too, and a copy would stop being the value under test the first time one side changed.
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
49
|
* `_harness_build._registry_problems` — `registry.json` describes exactly what `src/`
|
|
@@ -167,44 +164,6 @@ export function secretProblems() {
|
|
|
167
164
|
return problems;
|
|
168
165
|
}
|
|
169
166
|
|
|
170
|
-
/**
|
|
171
|
-
* Every active Claude-STYLE install (`CLAUDE_STYLE`: claude, bob, openclaude — the hosts that
|
|
172
|
-
* share this one machine-wide shim) whose LIVE, recorded hooks still name `shimPath` by exact
|
|
173
|
-
* string, keyed by its own config dir.
|
|
174
|
-
*
|
|
175
|
-
* Split out of `shimProblems` for the case `shimDeadPaths` cannot see: that function reads
|
|
176
|
-
* paths back OUT of the shim body, so it has nothing to read once the shim file is gone
|
|
177
|
-
* outright (deleted, not moved-and-regenerated) — a plain `isFile` check upstream then calls
|
|
178
|
-
* that clean, the same as a source checkout that never emitted. `liveRecordedHooks` is already
|
|
179
|
-
* "the manifest claims it AND the settings file still runs it" (`js/hosts/hosts.mjs`), so a
|
|
180
|
-
* disabled or never-wired install cannot appear here — only installs for which the missing
|
|
181
|
-
* shim is a live, standing failure.
|
|
182
|
-
*
|
|
183
|
-
* MATCHES THE EXACT PATH, not merely a command that mentions the shim's filename
|
|
184
|
-
* (`geneseed-hook`): the shim path is baked per `GENESEED_HOME`, so a machine with more than
|
|
185
|
-
* one Geneseed home (a relocated one, or a sandboxed test run beside a real install) must not
|
|
186
|
-
* let an install naming a DIFFERENT, perfectly healthy shim answer for this one.
|
|
187
|
-
*
|
|
188
|
-
* EXPORTED: `status`'s `gateSummary` (`js/inspect/status.mjs`) needs the same answer to fold
|
|
189
|
-
* into its `dead` field, the one `statusLines` already renders as a `DEAD` gates row — two
|
|
190
|
-
* readers of one fact, not two copies of the install walk.
|
|
191
|
-
*
|
|
192
|
-
* `targets` is a parameter, the same convention `hookShellProblems` uses, so a test can hand
|
|
193
|
-
* it a sandboxed `[host, scope, root]` triple instead of walking this machine's real config
|
|
194
|
-
* dirs and registry — the live default every production caller takes.
|
|
195
|
-
*/
|
|
196
|
-
export function installsReferencingShim(shimPath = hookShimPath(), targets = installTargets()) {
|
|
197
|
-
const out = [];
|
|
198
|
-
for (const [host, scope, root] of targets) {
|
|
199
|
-
if (!CLAUDE_STYLE.includes(host)) continue;
|
|
200
|
-
const cfg = claudeCfg(root, scope, host);
|
|
201
|
-
const live = liveRecordedHooks(cfg)
|
|
202
|
-
.some(([, h]) => typeof h.command === 'string' && h.command.includes(shimPath));
|
|
203
|
-
if (live) out.push(cfg);
|
|
204
|
-
}
|
|
205
|
-
return out;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
167
|
/**
|
|
209
168
|
* `_harness_build._shim_problems` — the hook shim exists and points at something real.
|
|
210
169
|
*
|
|
@@ -220,7 +179,7 @@ export function installsReferencingShim(shimPath = hookShimPath(), targets = ins
|
|
|
220
179
|
* reason to own one. But one deleted by hand (or by a cleanup script) out from under an
|
|
221
180
|
* install that still names it is the same dead-hooks failure as a moved checkout, newly worth
|
|
222
181
|
* catching now that the two Claude gates fail CLOSED on a dead hook instead of open
|
|
223
|
-
* (`onFailure: "block"
|
|
182
|
+
* (`onFailure: "block"`) — a Bash/Write/Edit call blocked with no shim to even try
|
|
224
183
|
* running is otherwise invisible until the user notices nothing works.
|
|
225
184
|
* `installsReferencingShim` answers that half; this still returns `[]` when nothing live
|
|
226
185
|
* names the missing shim.
|
|
@@ -243,7 +202,7 @@ export function shimProblems(targets = installTargets()) {
|
|
|
243
202
|
if (!isFile(p)) {
|
|
244
203
|
return installsReferencingShim(p, targets).map((cfg) => `[shim] ${p} does not exist, but ${cfg} `
|
|
245
204
|
+ 'still runs its hooks through it — every hook in that install is dead, and (since '
|
|
246
|
-
+ "
|
|
205
|
+
+ "Geneseed 4.0.0) Claude's git-gate/rule-gate now BLOCK instead of silently passing. Run: "
|
|
247
206
|
+ 'geneseed rebuild-all');
|
|
248
207
|
}
|
|
249
208
|
let body;
|
|
@@ -597,7 +556,7 @@ export function hostMatrixProblems(root = ROOT) {
|
|
|
597
556
|
* invisible here. A check this repo starts to pass is added by hand; one it stops passing is a
|
|
598
557
|
* doctor problem until fixed or deliberately removed from this list, in the same commit.
|
|
599
558
|
*/
|
|
600
|
-
|
|
559
|
+
const SCORECARD_FLOOR = [
|
|
601
560
|
'architecture_doc',
|
|
602
561
|
'agent_instructions',
|
|
603
562
|
'adr_presence',
|
|
@@ -630,6 +589,9 @@ export const SCORECARD_FLOOR = [
|
|
|
630
589
|
'security_critical_marking',
|
|
631
590
|
];
|
|
632
591
|
|
|
592
|
+
/** Where the README's scorecard card lives, and what doctor compares against a fresh render. */
|
|
593
|
+
const SCORECARD_CARD = 'docs/assets/scorecard.svg';
|
|
594
|
+
|
|
633
595
|
/**
|
|
634
596
|
* The scorecard ratchet — one problem per `SCORECARD_FLOOR` check that no longer passes.
|
|
635
597
|
*
|
|
@@ -659,6 +621,3 @@ export function scorecardProblems(root = ROOT) {
|
|
|
659
621
|
+ (k ? `: ${k.evidence}. Fix: ${k.remediation}` : ', and the scorecard has no such check');
|
|
660
622
|
}));
|
|
661
623
|
}
|
|
662
|
-
|
|
663
|
-
/** Where the README's scorecard card lives, and what doctor compares against a fresh render. */
|
|
664
|
-
export const SCORECARD_CARD = 'docs/assets/scorecard.svg';
|
package/js/inspect/cost.mjs
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Four parts, each the thing the host loads before the user types:
|
|
8
8
|
* - `carrier` the root instruction file (`AGENT.md`, `CLAUDE.md`, Bob's `rules/geneseed.md` or
|
|
9
|
-
* `AGENTS.md`), CRLF
|
|
10
|
-
*
|
|
9
|
+
* `AGENTS.md`), CRLF folded to LF as the smoke test folds it, so the figure and
|
|
10
|
+
* `CARRIER_CEILING` are the same measurement. `readText` also folds a lone CR,
|
|
11
|
+
* which the smoke test does not, so the two agree on any carrier without one.
|
|
11
12
|
* - `skills` the name + description every native skill lists (aliases included: they are
|
|
12
13
|
* SKILL.md files the host lists like any other).
|
|
13
14
|
* - `agents` the same for native agents; Bob has none, its agents table is in the carrier.
|
|
@@ -30,8 +31,6 @@ import { isFile, readText } from '../lib/fs.mjs';
|
|
|
30
31
|
*/
|
|
31
32
|
export const CARRIER_CEILING = { full: 62_219, lean: 39_088 };
|
|
32
33
|
|
|
33
|
-
const decode = (p) => readText(p).split('\r\n').join('\n');
|
|
34
|
-
|
|
35
34
|
/**
|
|
36
35
|
* A frontmatter value as the host shows it. The emit writes descriptions JSON-escaped
|
|
37
36
|
* (`—` for one em dash), and `frontmatter()` has already stripped the quotes, so the
|
|
@@ -48,7 +47,7 @@ function fmValue(raw) {
|
|
|
48
47
|
/** name + description of one spec file; `fallbackName` for agents, whose name is the file stem. */
|
|
49
48
|
function listedChars(file, fallbackName) {
|
|
50
49
|
let fm;
|
|
51
|
-
try { [fm] = frontmatter(
|
|
50
|
+
try { [fm] = frontmatter(readText(file)); } catch { return 0; }
|
|
52
51
|
const name = fmValue(fm.get('name')) || fallbackName;
|
|
53
52
|
return name.length + fmValue(fm.get('description')).length;
|
|
54
53
|
}
|
|
@@ -61,7 +60,7 @@ function listedChars(file, fallbackName) {
|
|
|
61
60
|
export function alwaysOnCost({ host, root, dir, footprint }) {
|
|
62
61
|
const carrierPath = carriersFor(host).map((rel) => path.join(root, rel)).find(isFile);
|
|
63
62
|
if (!carrierPath) return null;
|
|
64
|
-
const carrier =
|
|
63
|
+
const carrier = readText(carrierPath).length;
|
|
65
64
|
|
|
66
65
|
let skills = 0;
|
|
67
66
|
const skillsDir = path.join(dir, 'skills');
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -241,8 +241,8 @@ export function doctorCollect({
|
|
|
241
241
|
problems = problems.concat(ran('loops', 'Loop catalogue', loopProblems()));
|
|
242
242
|
problems = problems.concat(ran('shim', 'Hook shim', shimProbs));
|
|
243
243
|
// Windows: a Claude install's hooks emitted for Git Bash on a machine that no longer has it
|
|
244
|
-
// misbehave under PowerShell
|
|
245
|
-
//
|
|
244
|
+
// misbehave under PowerShell — context/learn fail open, and the two gates fail CLOSED
|
|
245
|
+
// instead (`onFailure: "block"`). The reverse direction is a note.
|
|
246
246
|
problems = problems.concat(ran('hook_shell', 'Hook shell', hookShellProblems()));
|
|
247
247
|
if (probeRun) {
|
|
248
248
|
problems = problems.concat(ran('probe', 'Gate probe', [...probeRun.problems, ...extraProbeProblems]));
|
package/js/inspect/scan.mjs
CHANGED
|
@@ -14,15 +14,13 @@ import { SRC } from '../build/source.mjs';
|
|
|
14
14
|
import { comparePaths, within } from '../lib/paths.mjs';
|
|
15
15
|
import { isDir, isFile } from '../lib/fs.mjs';
|
|
16
16
|
import { has } from '../lib/json.mjs';
|
|
17
|
-
import { FENCE_RE, INLINE_CODE_RE
|
|
17
|
+
import { FENCE_RE, INLINE_CODE_RE } from '../lib/text.mjs';
|
|
18
18
|
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
19
19
|
|
|
20
20
|
// `isDir`/`isFile`/`has` are owned by `js/lib` now (single owner across `hosts/`, `build/`,
|
|
21
21
|
// `inspect/` and `web/`) — re-exported here so this module's own existing callers keep
|
|
22
|
-
// importing them from `./scan.mjs` unchanged.
|
|
23
|
-
|
|
24
|
-
// answer and cannot import this module (it would cycle back through `../build/source.mjs` above).
|
|
25
|
-
export { isDir, isFile, has, blankCodeSpans };
|
|
22
|
+
// importing them from `./scan.mjs` unchanged.
|
|
23
|
+
export { isDir, isFile, has };
|
|
26
24
|
|
|
27
25
|
// --------------------------------------------------------------------------------------
|
|
28
26
|
// _harness_core's scanning primitives
|
|
@@ -106,3 +104,9 @@ export function withTempDir(fn, prefix = 'geneseed-doctor-') {
|
|
|
106
104
|
try { return fn(tmp); } finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
107
105
|
}
|
|
108
106
|
|
|
107
|
+
/** `withTempDir` for an async body: the dir outlives the promise, then goes. */
|
|
108
|
+
export async function withTempDirAsync(fn, prefix = 'geneseed-doctor-') {
|
|
109
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
110
|
+
try { return await fn(tmp); } finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
111
|
+
}
|
|
112
|
+
|