geneseed 3.17.0 → 3.18.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 +22 -0
- package/README.md +17 -3
- package/SHIPPED.md +3 -3
- package/adapters/claude-code/skills/geneseed/SKILL.md +1 -1
- package/adapters/opencode/README.md +2 -2
- package/adapters/opencode/plugins/geneseed-context.js +19 -17
- package/adapters/opencode/plugins/geneseed-guard.js +7 -7
- package/bin/geneseed-cli.mjs +5 -0
- package/docs/concepts/exclusions.md +1 -1
- package/docs/concepts/skills.md +4 -0
- package/docs/extending.md +33 -0
- package/docs/guides/read-non-markdown.md +1 -1
- package/docs/guides/uninstall.md +1 -1
- package/docs/guides/upgrade.md +1 -1
- package/docs/guides/wiki.md +4 -4
- package/docs/opencode-plugin-setup.md +1 -1
- package/docs/reference/env-context.md +2 -2
- package/docs/reference/plugin-context.md +2 -2
- package/docs/reference/plugin-guard.md +1 -1
- package/docs/token-footprint.md +18 -15
- package/docs/understand/take-it-out.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +4 -1
- package/js/build/args.mjs +22 -20
- package/js/build/bundle.mjs +1 -3
- package/js/build/emit-claude.mjs +5 -4
- package/js/build/emit-opencode.mjs +9 -2
- package/js/build/generate.mjs +6 -6
- package/js/build/render.mjs +19 -12
- package/js/build/source.mjs +82 -7
- package/js/build/stubs.mjs +35 -6
- package/js/build/themes.mjs +1 -3
- package/js/cli-table.json +53 -0
- package/js/hosts/hooks-context.mjs +13 -6
- package/js/hosts/hosts.mjs +1 -3
- package/js/hosts/installs.mjs +13 -7
- package/js/hosts/mcp.mjs +1 -1
- package/js/hosts/native.mjs +60 -5
- package/js/hosts/opencode.mjs +37 -6
- package/js/hosts/settings.mjs +1 -0
- package/js/hosts/shim.mjs +1 -0
- package/js/inspect/checks-authoring.mjs +4 -1
- package/js/inspect/checks-repo.mjs +164 -2
- package/js/inspect/diff.mjs +5 -1
- package/js/inspect/doctor.mjs +2 -1
- package/js/inspect/inventory.mjs +7 -9
- package/js/inspect/scorecard-checks.mjs +571 -0
- package/js/inspect/scorecard-svg.mjs +85 -0
- package/js/inspect/scorecard.mjs +839 -0
- package/js/lib/json.mjs +1 -1
- package/js/maintain/migrate.mjs +2 -1
- package/js/maintain/uninstall.mjs +2 -3
- package/js/maintain/update.mjs +10 -3
- package/js/ui/menu.mjs +2 -2
- package/js/ui/tui.mjs +1 -0
- package/js/web/api.mjs +2 -0
- package/js/web/catalog.mjs +24 -7
- package/js/web/daemon.mjs +2 -1
- package/js/web/routes.mjs +2 -0
- package/js/web/server.mjs +1 -1
- package/js/web/user-files.mjs +1 -0
- package/package.json +11 -3
- package/registry.json +12 -75
- package/src/AGENT.md.tmpl +13 -22
- package/src/agents/docs.md +1 -1
- package/src/agents/researcher.md +1 -1
- package/src/bricks/bruno-test.md +2 -2
- package/src/bricks/ci-fix.md +3 -3
- package/src/bricks/deps-audit.md +3 -3
- package/src/bricks/migration-plan.md +2 -2
- package/src/doctrines/process.md +2 -2
- package/src/laws/universal.md +2 -2
- package/src/skills/brainstorm.md +14 -10
- package/src/skills/bruno.md +84 -0
- package/src/skills/codebase-design.md +41 -23
- package/src/skills/daydream/SKILL.md +3 -3
- package/src/skills/daydream/VENDOR.md +2 -2
- package/src/skills/daydream/instructions.md +1 -1
- package/src/skills/debug/ci.md +11 -0
- package/src/skills/debug.md +11 -8
- package/src/skills/dependencies.md +65 -0
- package/src/skills/develop.md +9 -4
- package/src/skills/document-project.md +27 -22
- package/src/skills/geneseed-code-review.md +13 -8
- package/src/skills/geneseed.md +4 -1
- package/src/skills/git-history.md +78 -0
- package/src/skills/handoff.md +4 -3
- package/src/skills/parallel-agents.md +1 -0
- package/src/skills/plan.md +5 -1
- package/src/skills/prototype.md +10 -7
- package/src/skills/research.md +6 -1
- package/src/skills/rule/consolidate.md +32 -0
- package/src/skills/rule.md +14 -4
- package/src/skills/security-audit.md +1 -1
- package/src/skills/ship.md +8 -5
- package/src/skills/teach.md +5 -3
- package/src/skills/wayfinder.md +19 -16
- package/src/skills/wiki.md +2 -2
- package/themes/_TEMPLATE.json +7 -16
- package/themes/biker.json +7 -16
- package/themes/commentator.json +7 -16
- package/themes/cyberpunk.json +7 -16
- package/themes/gamer.json +7 -16
- package/themes/imperial.json +7 -16
- package/themes/joker.json +7 -16
- package/themes/marvin.json +7 -16
- package/themes/mean.json +7 -16
- package/themes/military.json +7 -16
- package/themes/neutral.json +7 -16
- package/themes/pirate.json +7 -16
- package/themes/sports.json +7 -16
- package/themes/verstappen.json +7 -16
- package/themes/wizard.json +7 -16
- package/web/dist/assets/{Activity-DonKN1pi.js → Activity-4M4VQJOh.js} +1 -1
- package/web/dist/assets/{ActivityDetail-e47a3cdE.js → ActivityDetail-CnmDs43F.js} +1 -1
- package/web/dist/assets/{Dashboard-Cr7i0PsO.js → Dashboard-lTaH58R9.js} +2 -2
- package/web/dist/assets/{Docs-Lne3nuzD.js → Docs-DU5znu5f.js} +1 -1
- package/web/dist/assets/FilterInput-BSfpStOZ.js +1 -0
- package/web/dist/assets/{GreenhouseView-4CbIuTpl.js → GreenhouseView-BxO84bKX.js} +1 -1
- package/web/dist/assets/{Installs-CIzbV1Ks.js → Installs-B1WcObxK.js} +1 -1
- package/web/dist/assets/{JournalView-BlBfuvzj.js → JournalView-0YNKcaaD.js} +1 -1
- package/web/dist/assets/{Laws-B-pjcjHr.js → Laws-DnDpRwpE.js} +1 -1
- package/web/dist/assets/Library-DBkKsBMe.js +1 -0
- package/web/dist/assets/{Loops-DIOxW1xH.js → Loops-P-6RCdzS.js} +1 -1
- package/web/dist/assets/Markdown-C6fRd4A4.js +67 -0
- package/web/dist/assets/{OperatorHudView-D7S7z7CJ.js → OperatorHudView-BZZ3uJPH.js} +1 -1
- package/web/dist/assets/{Personal-Ws3v35AB.js → Personal-BoVw6OCW.js} +1 -1
- package/web/dist/assets/{Tabs-6Z1eziAq.js → Tabs-VYLhUTiX.js} +1 -1
- package/web/dist/assets/{activity-Dr7ZMBI3.js → activity-dcg5vF9j.js} +1 -1
- package/web/dist/assets/{drawable-ATNeYKbT.js → drawable-CdNARIKC.js} +1 -1
- package/web/dist/assets/{index-CDg5XUhG.js → index-hsabpYzv.js} +2 -2
- package/web/dist/assets/react-DRhoX0kS.js +9 -0
- package/web/dist/assets/{usePoll-DD8VjtSg.js → usePoll-BmR631BB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/src/skills/bruno-collection-generator.md +0 -52
- package/src/skills/bruno-test-writer.md +0 -51
- package/src/skills/ci-fix.md +0 -49
- package/src/skills/consolidate-memory.md +0 -50
- package/src/skills/deps-audit.md +0 -46
- package/src/skills/domain-modeling.md +0 -27
- package/src/skills/git-archaeology.md +0 -36
- package/src/skills/git-rescue.md +0 -58
- package/src/skills/migrate.md +0 -34
- package/src/skills/opencode-theme.md +0 -42
- package/src/skills/repo-map.md +0 -25
- package/src/skills/tickets.md +0 -33
- package/web/dist/assets/FilterInput-CVyqmYWM.js +0 -1
- package/web/dist/assets/Library-x8Nzg6yq.js +0 -1
- package/web/dist/assets/Markdown-X2a3qvOh.js +0 -38
- package/web/dist/assets/react-B9Z_tXRV.js +0 -8
package/js/hosts/installs.mjs
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import { existsSync, readdirSync } from 'node:fs';
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
|
-
import { CONFIG, PACK_ORDER, THEMES, discoverNames,
|
|
38
|
+
import { CONFIG, PACK_ORDER, THEMES, discoverNames, resolveSkillNames } from '../build/source.mjs';
|
|
39
39
|
import { CLAUDE_STYLE, GLOBAL_MANIFEST, HOSTS, resolvePath } from './hosts.mjs';
|
|
40
40
|
import { registryRoots } from '../inspect/registry.mjs';
|
|
41
41
|
import { printErr, readText, isFile, isDir } from '../lib/fs.mjs';
|
|
@@ -408,7 +408,7 @@ export function excludedRulesOfDir(d) {
|
|
|
408
408
|
}, []);
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
/** `Excluded skills: bruno
|
|
411
|
+
/** `Excluded skills: bruno, daydream` — written only when something is excluded. */
|
|
412
412
|
const EXCLUDED_SKILLS_RE = /^Excluded skills:[ \t]*(.+?)[ \t]*$/m;
|
|
413
413
|
|
|
414
414
|
/**
|
|
@@ -416,17 +416,23 @@ const EXCLUDED_SKILLS_RE = /^Excluded skills:[ \t]*(.+?)[ \t]*$/m;
|
|
|
416
416
|
* re-render the same set. Same semantics as `excludedRulesOfDir`: a missing line is `[]`
|
|
417
417
|
* (nothing excluded), and a name this checkout does not ship condemns the whole line to `[]`
|
|
418
418
|
* — re-admitting a skill is the safe direction, and the flag would refuse the name anyway.
|
|
419
|
+
*
|
|
420
|
+
* A name that only got OLDER is not unknown: an alias reads back as its target and a retired
|
|
421
|
+
* name drops out (`resolveSkillNames`), so a skill merge does not quietly re-admit everything
|
|
422
|
+
* else the install left out. Silent unless the caller passes `notify` — status, diff and the
|
|
423
|
+
* console call this on every read; the replays that rebuild an install (`rebuild-all`,
|
|
424
|
+
* `upgrade`, `migrate`) pass it.
|
|
419
425
|
*/
|
|
420
|
-
export function excludedSkillsOfDir(d) {
|
|
426
|
+
export function excludedSkillsOfDir(d, notify = null) {
|
|
421
427
|
return firstCarrier(d, (carrierPath) => {
|
|
422
428
|
const text = readMaybe(carrierPath);
|
|
423
429
|
if (text === null) return undefined;
|
|
424
430
|
const m = EXCLUDED_SKILLS_RE.exec(text);
|
|
425
431
|
if (!m) return undefined;
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
return
|
|
432
|
+
const { ids, notices, unknown } = resolveSkillNames(m[1].split(',').map((s) => s.trim()).filter(Boolean));
|
|
433
|
+
if (unknown.length) return [];
|
|
434
|
+
if (notify) for (const n of notices) notify(n);
|
|
435
|
+
return ids;
|
|
430
436
|
}, []);
|
|
431
437
|
}
|
|
432
438
|
|
package/js/hosts/mcp.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* at "~90 new, already in js/: `_mcp_load`/`readJsonc` (P6c), `settings.mjs`'s atomic write".
|
|
6
6
|
* Measured against `js/`: `readJsonc` had crossed and NOTHING ELSE HAD. `js/web/api.mjs`
|
|
7
7
|
* carries a five-line `mcpLoad` that is the no-host, comment-tolerant reader P6c needed for
|
|
8
|
-
* `wiki.jsonc`, which is one arm of one of these thirteen functions. `_MCP_PRESETS`,
|
|
8
|
+
* `geneseed-wiki.jsonc`, which is one arm of one of these thirteen functions. `_MCP_PRESETS`,
|
|
9
9
|
* `_mcp_servers_key`, `_mcp_preset_block`, `_mcp_apply`, `_mcp_state`, `_mcp_set_enabled`,
|
|
10
10
|
* `_mcp_load`'s host fork, `_mcp_commented`, `_mcp_save`, `_mcp_config_for`,
|
|
11
11
|
* `_mcp_install_targets`, `_mcp_known_names` and `_mcp_meta` are 168 lines of Python, all
|
package/js/hosts/native.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import path from 'node:path';
|
|
|
32
32
|
import { writeText, copyFile, readText } from '../lib/fs.mjs';
|
|
33
33
|
import { jsonDumps, parseJson, formatValue, formatRepr, isTruthy, isDict } from '../lib/json.mjs';
|
|
34
34
|
import { relPosix } from '../lib/text.mjs';
|
|
35
|
+
import { aliasesOf } from '../build/source.mjs';
|
|
35
36
|
|
|
36
37
|
/** What a folder skill writes where it needs its own directory — see `writeNativeLayer`. */
|
|
37
38
|
const SKILL_DIR_PLACEHOLDER = '<this-skill-directory>';
|
|
@@ -253,6 +254,25 @@ export function stripSkillBodyLinks(body) {
|
|
|
253
254
|
return body.replace(SKILL_BODY_LINK_RE, '$1');
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* A first-party skill's SIDE FILES (`src/skills/<stem>/<file>`, beside the flat spec) are
|
|
259
|
+
* linked from the spec as `[…](<stem>/<file>)` — the path that resolves in `src/` and in the
|
|
260
|
+
* flat bundle, where doctor's dead-link scan proves it. A native skill sits one level deeper
|
|
261
|
+
* (`skills/<stem>/SKILL.md`), and an alias or a command copies the body somewhere else again,
|
|
262
|
+
* so there the link becomes a code-span path under `dir`: the placeholder a host that
|
|
263
|
+
* announces the skill's base directory resolves, or the answer for one that does not.
|
|
264
|
+
*
|
|
265
|
+
* A link with an `#anchor` (which `sideFileProblems` accepts) gets the same pointer with the
|
|
266
|
+
* anchor DROPPED: the pointer is a path the agent hands to a file-read tool, and
|
|
267
|
+
* `deep.md#two` names no file. Left unmatched, `stripSkillBodyLinks` would reduce the link to
|
|
268
|
+
* its label and the agent would never learn where the file is.
|
|
269
|
+
*/
|
|
270
|
+
export function pointSideFiles(body, stem, dir) {
|
|
271
|
+
// A function, not a `$1` string: `dir` is a real path and may itself hold a `$`.
|
|
272
|
+
return body.replace(new RegExp(`\\[[^\\]]+\\]\\(${stem}/([^)\\s#]+)(?:#[^)\\s]*)?\\)`, 'g'),
|
|
273
|
+
(_m, f) => `\`${dir}/${f}\``);
|
|
274
|
+
}
|
|
275
|
+
|
|
256
276
|
/** `_build_emit._agent_color_map` — validated, always a valid OpenCode slot. */
|
|
257
277
|
function agentColorMap(theme) {
|
|
258
278
|
let raw = null;
|
|
@@ -479,8 +499,8 @@ export function claimer(oldOwned, cfg, manifestExisted = true) {
|
|
|
479
499
|
* `<this-skill-directory>/…`; Claude Code resolves that because it announces each skill's
|
|
480
500
|
* base directory on load, while on Bob the model got the text with a hole in it and went
|
|
481
501
|
* hunting through the filesystem. With the option, every rendered `.md` of a vendored folder
|
|
482
|
-
* has the placeholder replaced by the answer.
|
|
483
|
-
*
|
|
502
|
+
* has the placeholder replaced by the answer. A flat skill with side files points at them
|
|
503
|
+
* through the same option (`pointSideFiles`), placeholder when it is absent.
|
|
484
504
|
*
|
|
485
505
|
* `opts.claim` — a `claimer` closure the caller shares with its other writers; built from
|
|
486
506
|
* `oldOwned`/`cfg`/`manifestExisted` when absent.
|
|
@@ -498,13 +518,15 @@ export function claimer(oldOwned, cfg, manifestExisted = true) {
|
|
|
498
518
|
*/
|
|
499
519
|
export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null, {
|
|
500
520
|
host = 'opencode', oldOwned = null, cfg = null, manifestExisted = true,
|
|
501
|
-
theme = null, src, skillDirOf = null, claim = claimer(oldOwned, cfg, manifestExisted),
|
|
521
|
+
theme = null, src = undefined, skillDirOf = null, claim = claimer(oldOwned, cfg, manifestExisted),
|
|
502
522
|
} = {}) {
|
|
503
523
|
const ov = overrides || {};
|
|
504
524
|
if (host === 'claude') warnClaudeIgnored(ov);
|
|
505
525
|
let nAgents = 0;
|
|
506
526
|
let nSkills = 0;
|
|
507
527
|
const written = [];
|
|
528
|
+
const sideFiles = []; // [stem, dest, text], written after the loop
|
|
529
|
+
const claimedSkills = new Set(); // stems whose own SKILL.md claim held
|
|
508
530
|
|
|
509
531
|
const write = (dest, text) => {
|
|
510
532
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -534,6 +556,15 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
534
556
|
continue;
|
|
535
557
|
}
|
|
536
558
|
if (text === null) continue;
|
|
559
|
+
// A first-party side file rides next to its skill's SKILL.md, read only when the body
|
|
560
|
+
// points the agent at it; not a skill, so not counted. `sideFileProblems` holds the
|
|
561
|
+
// layout (a flat spec beside it, one level, `.md`, linked from the body). Held until the
|
|
562
|
+
// loop ends: written only once its skill's own SKILL.md claim held (the alias rule), so a
|
|
563
|
+
// user's file at the target never gets a geneseed side file dropped beside it.
|
|
564
|
+
if (sparts.length === 3 && sparts[0] === 'skills') {
|
|
565
|
+
sideFiles.push([sparts[1], path.join(skillsDir, sparts[1], sparts[2]), text]);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
537
568
|
if (sparts.length !== 2 || !sparts[1].endsWith('.md')) continue;
|
|
538
569
|
const [folder, fname] = sparts;
|
|
539
570
|
const targetDir = { agents: agentsDir, skills: skillsDir }[folder];
|
|
@@ -567,18 +598,42 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
567
598
|
} else {
|
|
568
599
|
// Skills are BYTE-IDENTICAL across hosts: name + description, body link-stripped.
|
|
569
600
|
// The user-only key is emitted for every host too — Claude Code and Bob honour it,
|
|
570
|
-
// OpenCode ignores an unknown key — so the identity holds.
|
|
601
|
+
// OpenCode ignores an unknown key — so the identity holds. ALIASES are the one
|
|
602
|
+
// exception: OpenCode has no user-only skill, so an alias skill there would land in the
|
|
603
|
+
// model's catalogue as a duplicate; it gets a command instead (`writeAliasCommands`).
|
|
571
604
|
fm = [`name: ${stem}`, `description: ${jsonDumps(skillDescription(text))}`];
|
|
572
605
|
if (isUserInvokedOnly(text)) fm.push('disable-model-invocation: true');
|
|
573
|
-
body = stripSkillBodyLinks(body)
|
|
606
|
+
body = stripSkillBodyLinks(pointSideFiles(body, stem, skillDirOf ? skillDirOf(stem)
|
|
607
|
+
: SKILL_DIR_PLACEHOLDER));
|
|
574
608
|
dest = path.join(skillsDir, stem, 'SKILL.md');
|
|
575
609
|
kind = 'skill';
|
|
576
610
|
}
|
|
577
611
|
if (!claim(dest)) continue;
|
|
578
612
|
write(dest, `---\n${fm.join('\n')}\n---\n\n${body}`);
|
|
613
|
+
if (kind === 'skill') claimedSkills.add(stem);
|
|
614
|
+
// An old name keeps answering `/name`: user-only, so the model's catalogue lists the
|
|
615
|
+
// skill once. Read off the already-filtered items, so excluding the target drops its
|
|
616
|
+
// aliases; through `claim`, so manifest, prune and uninstall own them; not counted.
|
|
617
|
+
// Only once the target's own claim held: a user's file at the target gets no alias
|
|
618
|
+
// pointing at a skill this install never wrote.
|
|
619
|
+
if (kind === 'skill' && host === 'claude') {
|
|
620
|
+
// The host announces the ALIAS's folder as the base directory, so a side-file pointer
|
|
621
|
+
// climbs back to the target's.
|
|
622
|
+
const aliasBody = skillDirOf ? body : stripSkillBodyLinks(
|
|
623
|
+
pointSideFiles(lstripNewlines(text), stem, `${SKILL_DIR_PLACEHOLDER}/../${stem}`));
|
|
624
|
+
for (const alias of aliasesOf(text)) {
|
|
625
|
+
const aliasDest = path.join(skillsDir, alias, 'SKILL.md');
|
|
626
|
+
if (!claim(aliasDest)) continue;
|
|
627
|
+
write(aliasDest, `---\nname: ${alias}\ndescription: ${jsonDumps(`Alias of ${stem}.`)}\n`
|
|
628
|
+
+ `disable-model-invocation: true\n---\n\n${aliasBody}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
579
631
|
if (kind === 'agent') nAgents += 1;
|
|
580
632
|
else nSkills += 1;
|
|
581
633
|
}
|
|
634
|
+
for (const [stem, dest, text] of sideFiles) {
|
|
635
|
+
if (claimedSkills.has(stem) && claim(dest)) write(dest, stripSkillBodyLinks(text));
|
|
636
|
+
}
|
|
582
637
|
return { nAgents, nSkills, written };
|
|
583
638
|
}
|
|
584
639
|
|
package/js/hosts/opencode.mjs
CHANGED
|
@@ -27,7 +27,8 @@ import { writeText, readText, copyFile } from '../lib/fs.mjs';
|
|
|
27
27
|
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
|
-
import {
|
|
30
|
+
import { aliasesOf } from '../build/source.mjs';
|
|
31
|
+
import { firstBlockquote, stripSkillBodyLinks, pointSideFiles, pushOverrideLines } from './native.mjs';
|
|
31
32
|
|
|
32
33
|
// Re-exported for `js/web/api.mjs` and the tests, which import it from here.
|
|
33
34
|
export { sourceReleaseVersion };
|
|
@@ -104,7 +105,7 @@ const AGENT_OVERRIDES_STUB = {
|
|
|
104
105
|
* `/code-review`. As a list the renamed entry missed its skill and was dropped in silence, so a
|
|
105
106
|
* missing skill is now a thrown error in `writeCommandLayer`.
|
|
106
107
|
*/
|
|
107
|
-
const COMMAND_SET = {
|
|
108
|
+
export const COMMAND_SET = {
|
|
108
109
|
commit: 'commit', plan: 'plan', 'code-review': 'geneseed-code-review',
|
|
109
110
|
'review-response': 'review-response', ship: 'ship', debug: 'debug', research: 'research',
|
|
110
111
|
};
|
|
@@ -276,7 +277,7 @@ export function ensureAgentOverridesStub(cfg, base) {
|
|
|
276
277
|
* `command/commit.md` without the check made uninstall delete it. Default: claim everything,
|
|
277
278
|
* for a caller with no manifest to consult.
|
|
278
279
|
*/
|
|
279
|
-
export function writePrimaryAgent(cfg, agentsDir, overrides, claim = () => true) {
|
|
280
|
+
export function writePrimaryAgent(cfg, agentsDir, overrides, claim = (_dest) => true) {
|
|
280
281
|
if (!truthyEnv('GENESEED_PRIMARY') || !existsSync(cfg.primaryAgentSrc)
|
|
281
282
|
|| !statSync(cfg.primaryAgentSrc).isFile()) {
|
|
282
283
|
return null;
|
|
@@ -296,7 +297,8 @@ export function writePrimaryAgent(cfg, agentsDir, overrides, claim = () => true)
|
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
/** `_build_emit._write_command_layer` — the opt-in /slash commands. */
|
|
299
|
-
export function writeCommandLayer(cfg, items, commandDir, claim = () => true
|
|
300
|
+
export function writeCommandLayer(cfg, items, commandDir, claim = (_dest) => true,
|
|
301
|
+
skillDirOf = (n) => path.join(path.dirname(commandDir), 'skills', n).split(path.sep).join('/')) {
|
|
300
302
|
if (!truthyEnv('GENESEED_COMMANDS')) return [];
|
|
301
303
|
const byName = new Map();
|
|
302
304
|
for (const { text, src } of items) {
|
|
@@ -318,7 +320,8 @@ export function writeCommandLayer(cfg, items, commandDir, claim = () => true) {
|
|
|
318
320
|
const dest = path.join(commandDir, `${name}.md`);
|
|
319
321
|
if (!claim(dest)) continue;
|
|
320
322
|
const desc = firstBlockquote(text);
|
|
321
|
-
|
|
323
|
+
// A command has no base directory, so a side-file pointer names the skill's own.
|
|
324
|
+
const body = stripSkillBodyLinks(pointSideFiles(text.replace(/^\n+/, ''), skill, skillDirOf(skill)));
|
|
322
325
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
323
326
|
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${body}`);
|
|
324
327
|
written.push(dest);
|
|
@@ -326,8 +329,36 @@ export function writeCommandLayer(cfg, items, commandDir, claim = () => true) {
|
|
|
326
329
|
return written;
|
|
327
330
|
}
|
|
328
331
|
|
|
332
|
+
/**
|
|
333
|
+
* A merged skill's OLD names as `/alias` commands — OpenCode's form of the user-only alias
|
|
334
|
+
* skill `writeNativeLayer` writes for the Claude dialect. A command, not a skill, because
|
|
335
|
+
* OpenCode has no user-only skill: an alias skill would sit in the model's catalogue as a
|
|
336
|
+
* second copy of its target. UNCONDITIONAL, unlike the `GENESEED_COMMANDS` set — an old name
|
|
337
|
+
* that only works behind an opt-in is a name that broke. Read off the already-filtered
|
|
338
|
+
* `items`, so excluding the target drops its aliases.
|
|
339
|
+
*/
|
|
340
|
+
export function writeAliasCommands(cfg, items, commandDir, claim = (_dest) => true,
|
|
341
|
+
skillDirOf = (n) => path.join(path.dirname(commandDir), 'skills', n).split(path.sep).join('/')) {
|
|
342
|
+
const written = [];
|
|
343
|
+
for (const { text, src } of items) {
|
|
344
|
+
if (text === null) continue;
|
|
345
|
+
const sp = path.relative(cfg.src, src).split(path.sep);
|
|
346
|
+
if (sp.length !== 2 || sp[0] !== 'skills' || !sp[1].endsWith('.md') || sp[1].startsWith('_')) continue;
|
|
347
|
+
const stem = sp[1].slice(0, -3);
|
|
348
|
+
const body = stripSkillBodyLinks(pointSideFiles(text.replace(/^\n+/, ''), stem, skillDirOf(stem)));
|
|
349
|
+
for (const alias of aliasesOf(text)) {
|
|
350
|
+
const dest = path.join(commandDir, `${alias}.md`);
|
|
351
|
+
if (!claim(dest)) continue;
|
|
352
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
353
|
+
writeText(dest, `---\ndescription: ${jsonDumps(`Alias of ${sp[1].slice(0, -3)}.`)}\n---\n\n${body}`);
|
|
354
|
+
written.push(dest);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return written;
|
|
358
|
+
}
|
|
359
|
+
|
|
329
360
|
/** `_build_emit._write_ponytail_command` — registered UNCONDITIONALLY; null when the user's. */
|
|
330
|
-
export function writePonytailCommand(commandDir, claim = () => true) {
|
|
361
|
+
export function writePonytailCommand(commandDir, claim = (_dest) => true) {
|
|
331
362
|
const dest = path.join(commandDir, 'ponytail.md');
|
|
332
363
|
if (!claim(dest)) return null;
|
|
333
364
|
mkdirSync(path.dirname(dest), { recursive: true });
|
package/js/hosts/settings.mjs
CHANGED
|
@@ -165,6 +165,7 @@ const OWNED_BASH = ['rm -rf *', 'git commit*', 'git push*', ...LAW_IV_BASH];
|
|
|
165
165
|
* caller warns on it. This is the one branch where the boundary can end up with NO Geneseed
|
|
166
166
|
* gate at all while the constitution states the rule — the unsafe direction — so it must not
|
|
167
167
|
* also be the silent one.
|
|
168
|
+
* @returns {[boolean, string[], string | null]}
|
|
168
169
|
*/
|
|
169
170
|
function reconcileOpencodePermission(perm, want) {
|
|
170
171
|
// A `permission` that is not an object, or whose `bash` is not one — OpenCode also accepts a
|
package/js/hosts/shim.mjs
CHANGED
|
@@ -215,6 +215,7 @@ function shimIsLive(p) {
|
|
|
215
215
|
* The fallback is strictly no worse than the old behaviour and far better than emitting
|
|
216
216
|
* commands naming a shim that does not exist — those fail on every hook (9009 under
|
|
217
217
|
* cmd.exe, 127 under sh) and take both gates down with them.
|
|
218
|
+
* @param {{ runner?: string, entry?: string, platform?: NodeJS.Platform }} [opts]
|
|
218
219
|
*/
|
|
219
220
|
export function hookPrefix({ runner, entry, platform = process.platform } = {}) {
|
|
220
221
|
// NOT a default. `runner`/`entry` have no computable fallback on this side — this process's
|
|
@@ -19,7 +19,8 @@ import { readText } from '../lib/fs.mjs';
|
|
|
19
19
|
import { which } from '../lib/paths.mjs';
|
|
20
20
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
21
21
|
import {
|
|
22
|
-
folderSkillProblems, registryProblems, secretProblems,
|
|
22
|
+
aliasProblems, folderSkillProblems, registryProblems, secretProblems, sideFileProblems,
|
|
23
|
+
vendorPinProblems,
|
|
23
24
|
} from './checks-repo.mjs';
|
|
24
25
|
import { constitutionProblems, leanBlockProblems } from './checks-constitution.mjs';
|
|
25
26
|
import { countTableProblems } from './checks-counts.mjs';
|
|
@@ -109,6 +110,8 @@ export function authoringProblems() {
|
|
|
109
110
|
problems.push(...secretProblems());
|
|
110
111
|
problems.push(...vendorPinProblems());
|
|
111
112
|
problems.push(...folderSkillProblems());
|
|
113
|
+
problems.push(...aliasProblems());
|
|
114
|
+
problems.push(...sideFileProblems());
|
|
112
115
|
problems.push(...constitutionProblems());
|
|
113
116
|
problems.push(...leanBlockProblems());
|
|
114
117
|
problems.push(...countTableProblems());
|
|
@@ -7,15 +7,20 @@
|
|
|
7
7
|
* itself and compares it against what is on disk.
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ALIASES_RE, PLUGIN_SRC, RETIRED_SKILL_IDS, ROOT, SRC, THEMES, WORKFLOW_SRC, aliasesOf, knownSkillIds,
|
|
12
|
+
} from '../build/source.mjs';
|
|
11
13
|
import { VENDORED_SKILL_DIRS } from '../hosts/native.mjs';
|
|
14
|
+
import { COMMAND_SET } from '../hosts/opencode.mjs';
|
|
12
15
|
import { hookShimPath, shimDeadPaths } from '../hosts/shim.mjs';
|
|
13
16
|
import { readText } from '../lib/fs.mjs';
|
|
14
17
|
import { formatRepr, formatValue } from '../lib/json.mjs';
|
|
15
18
|
import { splitLines } from '../lib/udiff.mjs';
|
|
16
19
|
import { ENTITY_STATUSES } from './inventory.mjs';
|
|
20
|
+
import { assessRepo } from './scorecard.mjs';
|
|
21
|
+
import { badgeUrl, renderSvg } from './scorecard-svg.mjs';
|
|
17
22
|
import { isDir, isFile, rglob, srcStems } from './scan.mjs';
|
|
18
|
-
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* `_harness_build._registry_keys` — every entity the registry must describe.
|
|
@@ -106,6 +111,7 @@ export function registryProblems() {
|
|
|
106
111
|
*
|
|
107
112
|
* `(?i)` becomes the `i` flag on the last one only, which is where the Python puts it.
|
|
108
113
|
*/
|
|
114
|
+
/** @type {Array<[string, RegExp]>} */
|
|
109
115
|
const SECRET_PATTERNS = [
|
|
110
116
|
['Anthropic API key', /sk-ant-[A-Za-z0-9_-]{20,}/],
|
|
111
117
|
['OpenAI-style API key', /\bsk-[A-Za-z0-9]{32,}/],
|
|
@@ -240,6 +246,83 @@ export function vendorPinProblems() {
|
|
|
240
246
|
return problems;
|
|
241
247
|
}
|
|
242
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Every `<!-- aliases: … -->` name is a clean, unclaimed, single-owner path segment.
|
|
251
|
+
*
|
|
252
|
+
* An alias becomes a folder (`skills/<alias>/`) and a command file (`command/<alias>.md`) on
|
|
253
|
+
* every install, so a name that collides overwrites something: another skill, another
|
|
254
|
+
* skill's alias, or an OpenCode command (`COMMAND_SET`, `/ponytail`). A retired skill id is
|
|
255
|
+
* refused as an alias AND as a live skill — `RETIRED_SKILL_IDS` is the list of names that
|
|
256
|
+
* already meant something on someone's machine. The emit trusts all of this; this is where
|
|
257
|
+
* it is checked.
|
|
258
|
+
*/
|
|
259
|
+
export function aliasProblems(src = SRC) {
|
|
260
|
+
const problems = [];
|
|
261
|
+
const known = knownSkillIds(src);
|
|
262
|
+
const reserved = new Set([...Object.keys(COMMAND_SET), 'ponytail']);
|
|
263
|
+
for (const id of known) {
|
|
264
|
+
if (Object.hasOwn(RETIRED_SKILL_IDS, id)) {
|
|
265
|
+
problems.push(`[authoring] skill '${id}' reuses a retired skill id (RETIRED_SKILL_IDS)`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const owner = new Map();
|
|
269
|
+
for (const id of known) {
|
|
270
|
+
let text;
|
|
271
|
+
try { text = readText(path.join(src, 'skills', `${id}.md`)); } catch { continue; } // a folder skill
|
|
272
|
+
// `aliasesOf` reads the FIRST marker line only, so a second one would be dropped in silence.
|
|
273
|
+
if ((text.match(new RegExp(ALIASES_RE.source, 'gm')) || []).length > 1) {
|
|
274
|
+
problems.push(`[authoring] skills/${id}.md declares aliases on more than one line — merge them`);
|
|
275
|
+
}
|
|
276
|
+
for (const alias of aliasesOf(text)) {
|
|
277
|
+
const where = `skills/${id}.md alias '${alias}'`;
|
|
278
|
+
if (!/^[a-z0-9-]+$/.test(alias)) problems.push(`[authoring] ${where} is not [a-z0-9-]+`);
|
|
279
|
+
else if (known.includes(alias)) problems.push(`[authoring] ${where} is already a skill`);
|
|
280
|
+
else if (Object.hasOwn(RETIRED_SKILL_IDS, alias)) {
|
|
281
|
+
problems.push(`[authoring] ${where} is a retired skill id (RETIRED_SKILL_IDS)`);
|
|
282
|
+
} else if (reserved.has(alias)) problems.push(`[authoring] ${where} is an OpenCode command name`);
|
|
283
|
+
else if (owner.has(alias)) {
|
|
284
|
+
problems.push(`[authoring] ${where} is already declared by skills/${owner.get(alias)}.md`);
|
|
285
|
+
} else owner.set(alias, id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return problems;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* A first-party skill's side files (`src/skills/<name>/<file>.md`, emitted beside its SKILL.md
|
|
293
|
+
* and read only when the body points at them) keep the one layout every writer assumes: a flat
|
|
294
|
+
* `<name>.md` owns the folder, files sit one level down, each is `.md` (rendered and themed),
|
|
295
|
+
* none is `SKILL.md` (the emit writes that one), and the spec links every one as
|
|
296
|
+
* `[…](<name>/<file>)` — an unlinked side file ships to every install and is never read.
|
|
297
|
+
* Vendored folders are exempt: they carry their own SKILL.md and their own layout.
|
|
298
|
+
*/
|
|
299
|
+
export function sideFileProblems(src = SRC) {
|
|
300
|
+
const problems = [];
|
|
301
|
+
const dir = path.join(src, 'skills');
|
|
302
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
303
|
+
if (!e.isDirectory() || e.name.startsWith('_') || VENDORED_SKILL_DIRS.has(e.name)) continue;
|
|
304
|
+
const name = e.name;
|
|
305
|
+
let spec;
|
|
306
|
+
try { spec = readText(path.join(dir, `${name}.md`)); } catch {
|
|
307
|
+
problems.push(`[authoring] skills/${name}/ has no skills/${name}.md — a side-file folder `
|
|
308
|
+
+ 'belongs to a flat spec (or list a folder skill in VENDORED_SKILL_DIRS)');
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
for (const f of readdirSync(path.join(dir, name), { withFileTypes: true })) {
|
|
312
|
+
const where = `skills/${name}/${f.name}`;
|
|
313
|
+
if (!f.isFile() || !f.name.endsWith('.md')) {
|
|
314
|
+
problems.push(`[authoring] ${where} — a side file is a .md one level down`);
|
|
315
|
+
} else if (f.name === 'SKILL.md') {
|
|
316
|
+
problems.push(`[authoring] ${where} collides with the SKILL.md the emit writes`);
|
|
317
|
+
} else if (!spec.includes(`](${name}/${f.name})`) && !spec.includes(`](${name}/${f.name}#`)) {
|
|
318
|
+
problems.push(`[authoring] ${where} is not linked from skills/${name}.md — `
|
|
319
|
+
+ `link it as [${name}/${f.name}](${name}/${f.name}) or delete it`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return problems.sort();
|
|
324
|
+
}
|
|
325
|
+
|
|
243
326
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
244
327
|
|
|
245
328
|
/**
|
|
@@ -359,3 +442,82 @@ export function moduleMapProblems() {
|
|
|
359
442
|
}
|
|
360
443
|
return problems;
|
|
361
444
|
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The AI Harness Scorecard checks this repo passes, written out — the ratchet's floor.
|
|
448
|
+
*
|
|
449
|
+
* Taken from `geneseed scorecard` on main after #209-#213 (security policy, PR template, ARCHITECTURE
|
|
450
|
+
* and ADRs, the audit and commitlint jobs, Dependabot, coverage, property tests, fuzzing): 26 of 31,
|
|
451
|
+
* 81.2/100, matching upstream's Python tool check for check. Then the weekly Stryker run and the
|
|
452
|
+
* offline link check added mutation_testing and stale_doc_detection: 28 of 31, 88.7 (A); the checkJs
|
|
453
|
+
* pass added type_safety (partial: `strict` is off): 29 of 31, 90.4. Grouped by pillar, in upstream
|
|
454
|
+
* order.
|
|
455
|
+
* The ratchet is on `passed`, which upstream sets for any score above zero: `test_suite_exists`
|
|
456
|
+
* and `error_handling_policy` pass at 1.5 of 3, and a full check sliding to a partial one is
|
|
457
|
+
* invisible here. A check this repo starts to pass is added by hand; one it stops passing is a
|
|
458
|
+
* doctor problem until fixed or deliberately removed from this list, in the same commit.
|
|
459
|
+
*/
|
|
460
|
+
export const SCORECARD_FLOOR = [
|
|
461
|
+
'architecture_doc',
|
|
462
|
+
'agent_instructions',
|
|
463
|
+
'adr_presence',
|
|
464
|
+
'module_boundary_docs',
|
|
465
|
+
'ci_pipeline_exists',
|
|
466
|
+
'linter_enforcement',
|
|
467
|
+
'type_safety',
|
|
468
|
+
'dependency_auditing',
|
|
469
|
+
'conventional_commits',
|
|
470
|
+
'unsafe_code_policy',
|
|
471
|
+
'test_suite_exists',
|
|
472
|
+
'feature_matrix_testing',
|
|
473
|
+
'coverage_measurement',
|
|
474
|
+
'mutation_testing',
|
|
475
|
+
'property_based_testing',
|
|
476
|
+
'fuzz_testing',
|
|
477
|
+
'contract_tests',
|
|
478
|
+
'tests_blocking_ci',
|
|
479
|
+
'code_review_required',
|
|
480
|
+
'scheduled_ci',
|
|
481
|
+
'stale_doc_detection',
|
|
482
|
+
'mr_template',
|
|
483
|
+
'automated_review',
|
|
484
|
+
'doc_sync_check',
|
|
485
|
+
'ai_usage_norms',
|
|
486
|
+
'small_batch_enforcement',
|
|
487
|
+
'multiple_approach_culture',
|
|
488
|
+
'error_handling_policy',
|
|
489
|
+
'security_critical_marking',
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The scorecard ratchet — one problem per `SCORECARD_FLOOR` check that no longer passes.
|
|
494
|
+
*
|
|
495
|
+
* DEVELOPMENT CHECKOUT ONLY: it scores `.github/` (CI, CODEOWNERS, the PR template), and
|
|
496
|
+
* `.github/` is WITHHELD from the npm package (`tests/unit/package_manifest.test.mjs`), so an npm
|
|
497
|
+
* install would fail half the floor for files it was never meant to carry. A `.git` entry (a
|
|
498
|
+
* directory in a clone, a FILE in a worktree — hence `existsSync`, not `isDir`) plus
|
|
499
|
+
* `.github/workflows` is what only a checkout of this repository has. Anything else skips: [].
|
|
500
|
+
*/
|
|
501
|
+
export function scorecardProblems(root = ROOT) {
|
|
502
|
+
if (!existsSync(path.join(root, '.git')) || !isDir(path.join(root, '.github', 'workflows'))) return [];
|
|
503
|
+
const a = assessRepo(root);
|
|
504
|
+
const checks = new Map(a.categories.flatMap((c) => c.checks).map((k) => [k.id, k]));
|
|
505
|
+
// THE README CARD IS A RENDER OF THIS SAME ASSESSMENT, compared byte for byte: a card that no
|
|
506
|
+
// longer matches the score is the stale-number problem the badges have, caught the same way.
|
|
507
|
+
const card = path.join(root, ...SCORECARD_CARD.split('/'));
|
|
508
|
+
const stale = isFile(card) && readText(card) === renderSvg(a) ? [] : [
|
|
509
|
+
`[scorecard] ${SCORECARD_CARD} does not match the current score — run: geneseed scorecard --svg ${SCORECARD_CARD}`,
|
|
510
|
+
];
|
|
511
|
+
const readme = path.join(root, 'README.md');
|
|
512
|
+
if (isFile(readme) && !readText(readme).includes(`(${badgeUrl(a)})`)) {
|
|
513
|
+
stale.push(`[scorecard] README.md's scorecard badge does not show the current score — set it to ${badgeUrl(a)}`);
|
|
514
|
+
}
|
|
515
|
+
return stale.concat(SCORECARD_FLOOR.filter((id) => !checks.get(id)?.passed).map((id) => {
|
|
516
|
+
const k = checks.get(id);
|
|
517
|
+
return `[scorecard] ${id} no longer passes — it is in SCORECARD_FLOOR (js/inspect/checks-repo.mjs)`
|
|
518
|
+
+ (k ? `: ${k.evidence}. Fix: ${k.remediation}` : ', and the scorecard has no such check');
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Where the README's scorecard card lives, and what doctor compares against a fresh render. */
|
|
523
|
+
export const SCORECARD_CARD = 'docs/assets/scorecard.svg';
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -189,7 +189,10 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
189
189
|
// reference render names the temp tree. Mapped back to the deployed dir before comparing,
|
|
190
190
|
// or every such skill reads as edited. A deployment that has since MOVED still differs,
|
|
191
191
|
// and that is real drift — its skills point at a directory that is no longer theirs.
|
|
192
|
+
// A side-file pointer (and a global OpenCode command's) spells the same directory
|
|
193
|
+
// `/`-separated, so that spelling is mapped too.
|
|
192
194
|
const deployedDir = resolvePath(dir);
|
|
195
|
+
const posix = (p) => p.split(path.sep).join('/');
|
|
193
196
|
const rels = [...new Set([...ownedSet(dir), ...ownedSet(expected)])].sort();
|
|
194
197
|
for (const rel of rels) {
|
|
195
198
|
const a = path.join(dir, rel);
|
|
@@ -198,7 +201,8 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
198
201
|
// `read_text(errors="replace")` — Node's utf8 decoder substitutes U+FFFD for the
|
|
199
202
|
// same bytes, and `readText` folds the line endings the way `Path.read_text` does.
|
|
200
203
|
const ta = readText(a);
|
|
201
|
-
const tb = readText(b).split(expected).join(deployedDir)
|
|
204
|
+
const tb = readText(b).split(expected).join(deployedDir)
|
|
205
|
+
.split(posix(expected)).join(posix(deployedDir));
|
|
202
206
|
if (cmpKey(rel, ta) !== cmpKey(rel, tb)) {
|
|
203
207
|
const diff = unifiedDiff(splitLines(tb), splitLines(ta), {
|
|
204
208
|
fromfile: `source/${rel}`, tofile: `deployed/${rel}`, lineterm: '',
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { authoringProblems, loopProblems } from './checks-authoring.mjs';
|
|
|
40
40
|
import {
|
|
41
41
|
checkBuild, colorThemeProblems, renderedProblems, themeParityProblems,
|
|
42
42
|
} from './checks-build.mjs';
|
|
43
|
-
import { moduleMapProblems, shimProblems } from './checks-repo.mjs';
|
|
43
|
+
import { moduleMapProblems, scorecardProblems, shimProblems } from './checks-repo.mjs';
|
|
44
44
|
import { isDoctorNote, sortedProblems, sortedUnique, stemOf, withTempDir } from './scan.mjs';
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -220,6 +220,7 @@ export function doctorCollect({
|
|
|
220
220
|
problems = problems.concat(ran('loops', 'Loop catalogue', loopProblems()));
|
|
221
221
|
problems = problems.concat(ran('shim', 'Hook shim', shimProbs));
|
|
222
222
|
problems = problems.concat(ran('map', 'Module map', moduleMapProblems()));
|
|
223
|
+
problems = problems.concat(ran('scorecard', 'Scorecard floor', scorecardProblems()));
|
|
223
224
|
// P10c's `cli` check is GONE, and the reason is not that it stopped mattering. It hashed
|
|
224
225
|
// `rituals/harness.py` and compared that against a digest baked into `cli.json`, to catch a
|
|
225
226
|
// parser edited without regenerating the table. P2 made the table the OWNED document
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -92,23 +92,21 @@ export const SKILL_CLASS = {
|
|
|
92
92
|
brainstorm: 'design', plan: 'design', council: 'design',
|
|
93
93
|
workflow: 'design', 'parallel-agents': 'design', pipeline: 'design',
|
|
94
94
|
loop: 'design', 'brick-forge': 'design',
|
|
95
|
-
'codebase-design': 'design',
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'
|
|
99
|
-
'forge-mcp': 'build', 'bruno-collection-generator': 'build', 'bruno-test-writer': 'build',
|
|
100
|
-
worktree: 'build', 'ci-fix': 'build', 'deps-audit': 'build',
|
|
95
|
+
'codebase-design': 'design', wayfinder: 'design',
|
|
96
|
+
develop: 'build', refactor: 'build', debug: 'build', dependencies: 'build',
|
|
97
|
+
'frontend-design': 'build', prototype: 'build',
|
|
98
|
+
'forge-mcp': 'build', bruno: 'build', worktree: 'build',
|
|
101
99
|
'geneseed-code-review': 'review', 'fresh-eyes': 'review', 'security-audit': 'review',
|
|
102
100
|
'roast-me': 'review', 'review-response': 'review', ponytail: 'review',
|
|
103
|
-
commit: 'ship', ship: 'ship', release: 'ship', handoff: 'ship',
|
|
104
|
-
'
|
|
101
|
+
commit: 'ship', ship: 'ship', release: 'ship', handoff: 'ship',
|
|
102
|
+
'git-history': 'understand',
|
|
105
103
|
research: 'understand', ingest: 'understand', 'document-project': 'understand',
|
|
106
104
|
prose: 'understand',
|
|
107
105
|
// `harness` — skills about the agent's own apparatus (its harness, memory, rules,
|
|
108
106
|
// knowledge base, terminal) rather than about the user's code. They sat under
|
|
109
107
|
// `understand` before, which made that class a grab-bag.
|
|
110
108
|
wiki: 'harness', geneseed: 'harness', rule: 'harness', profile: 'harness',
|
|
111
|
-
herdr: 'harness', 'skill-forge': 'harness',
|
|
109
|
+
herdr: 'harness', 'skill-forge': 'harness',
|
|
112
110
|
teach: 'learn', quiz: 'learn', 'learn-mode': 'learn',
|
|
113
111
|
};
|
|
114
112
|
|