geneseed 3.16.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 +45 -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 +31 -21
- package/adapters/opencode/plugins/geneseed-guard.js +35 -7
- package/bin/geneseed-cli.mjs +5 -0
- package/bin/geneseed.mjs +17 -0
- package/docs/concepts/exclusions.md +1 -1
- package/docs/concepts/hooks.md +1 -0
- package/docs/concepts/loop-notes.md +3 -0
- package/docs/concepts/loops.md +11 -0
- package/docs/concepts/skills.md +4 -0
- package/docs/extending.md +33 -0
- package/docs/guides/headless.md +1 -1
- package/docs/guides/project-context.md +2 -2
- package/docs/guides/read-non-markdown.md +2 -2
- 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 +4 -4
- 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 +40 -27
- package/js/hosts/hooks.mjs +34 -1
- 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 +3 -1
- package/js/hosts/shim.mjs +1 -0
- package/js/inspect/checks-authoring.mjs +4 -1
- package/js/inspect/checks-constitution.mjs +2 -2
- 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/loop/graph.mjs +2 -1
- package/js/loop/state.mjs +30 -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 +5 -0
- package/src/agents/researcher.md +1 -1
- package/src/bricks/apply.md +3 -0
- 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/docs-update.md +2 -0
- package/src/bricks/migration-plan.md +2 -2
- package/src/bricks/test.md +17 -7
- package/src/doctrines/process.md +11 -7
- package/src/laws/universal.md +2 -2
- package/src/loops/api-endpoint.json +1 -1
- package/src/loops/bugfix.json +1 -1
- package/src/loops/ci-repair.json +1 -1
- package/src/loops/deps-upgrade.json +1 -1
- package/src/loops/enforce-architecture-rule.json +1 -1
- package/src/loops/feature.json +1 -1
- package/src/loops/fix-flaky-tests.json +1 -1
- package/src/loops/legacy-refactor.json +1 -1
- package/src/loops/legacy-tests.json +1 -1
- package/src/loops/refactor.json +1 -1
- package/src/loops/remove-dead-code.json +1 -1
- package/src/loops/spec-first-feature.json +1 -1
- package/src/loops/tdd.json +1 -1
- package/src/loops/update-contract.json +1 -1
- 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 +106 -32
- 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-kpWxd70F.js → Activity-4M4VQJOh.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Ccr942VL.js → ActivityDetail-CnmDs43F.js} +1 -1
- package/web/dist/assets/{Dashboard-D1lE7jcg.js → Dashboard-lTaH58R9.js} +2 -2
- package/web/dist/assets/{Docs-BHyaggFy.js → Docs-DU5znu5f.js} +1 -1
- package/web/dist/assets/FilterInput-BSfpStOZ.js +1 -0
- package/web/dist/assets/{GreenhouseView-Sc_UDMOE.js → GreenhouseView-BxO84bKX.js} +1 -1
- package/web/dist/assets/{Installs-Gw-pJ3Zi.js → Installs-B1WcObxK.js} +1 -1
- package/web/dist/assets/{JournalView-_gxpsxXi.js → JournalView-0YNKcaaD.js} +1 -1
- package/web/dist/assets/{Laws-S9fq1r9T.js → Laws-DnDpRwpE.js} +1 -1
- package/web/dist/assets/Library-DBkKsBMe.js +1 -0
- package/web/dist/assets/{Loops-Da_JhlzM.js → Loops-P-6RCdzS.js} +1 -1
- package/web/dist/assets/Markdown-C6fRd4A4.js +67 -0
- package/web/dist/assets/{OperatorHudView--TkvDybH.js → OperatorHudView-BZZ3uJPH.js} +1 -1
- package/web/dist/assets/{Personal-CTXKHVbV.js → Personal-BoVw6OCW.js} +1 -1
- package/web/dist/assets/{Tabs-B0cdf8k6.js → Tabs-VYLhUTiX.js} +1 -1
- package/web/dist/assets/{activity-Dr7ZMBI3.js → activity-dcg5vF9j.js} +1 -1
- package/web/dist/assets/{drawable-BbWQxe82.js → drawable-CdNARIKC.js} +1 -1
- package/web/dist/assets/{index-ujKG1ZNf.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-MBxadRzn.js +0 -1
- package/web/dist/assets/Markdown-X2a3qvOh.js +0 -38
- package/web/dist/assets/react-B9Z_tXRV.js +0 -8
package/js/build/stubs.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { writeText } from '../lib/fs.mjs';
|
|
|
12
12
|
import { isDir } from '../lib/fs.mjs';
|
|
13
13
|
import { jsonDumpsIndent } from '../lib/json.mjs';
|
|
14
14
|
import { EXCLUDES_FILE } from '../hosts/hosts.mjs';
|
|
15
|
-
import { existsSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Seeded stubs. Written once into the bundle and NEVER overwritten — each holds
|
|
@@ -34,7 +34,7 @@ const CONTEXT_STUB = {
|
|
|
34
34
|
|
|
35
35
|
/** `_build_render.WIKI_STUB`. */
|
|
36
36
|
const WIKI_STUB = `\
|
|
37
|
-
//
|
|
37
|
+
// geneseed-wiki.jsonc — declare your machine-wide knowledge base(s) here, typically
|
|
38
38
|
// an Obsidian vault (AGENT.md: the Wiki section). Comments are allowed in this file
|
|
39
39
|
// (JSONC). It is host-specific — never commit it. The build created it once, empty,
|
|
40
40
|
// and will never overwrite it.
|
|
@@ -183,7 +183,8 @@ const BUNDLE_GITIGNORE = `\
|
|
|
183
183
|
context.json
|
|
184
184
|
|
|
185
185
|
# Knowledge-base manifest — holds private machine paths; never commit.
|
|
186
|
-
# (wiki.json
|
|
186
|
+
# (wiki.jsonc and wiki.json are its legacy names from earlier seeds.)
|
|
187
|
+
geneseed-wiki.jsonc
|
|
187
188
|
wiki.jsonc
|
|
188
189
|
wiki.json
|
|
189
190
|
|
|
@@ -220,9 +221,37 @@ export function ensureContextStub(out) {
|
|
|
220
221
|
seed(path.join(out, 'context.json'), `${jsonDumpsIndent(CONTEXT_STUB, { ensureAscii: false })}\n`);
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
/**
|
|
224
|
+
/** The wiki manifest's name. `geneseed-` says who owns it in a shared `.claude/`/`.opencode/`. */
|
|
225
|
+
export const WIKI_FILE = 'geneseed-wiki.jsonc';
|
|
226
|
+
/** Its earlier names, newest first. Readers still fall back to them for one release. */
|
|
227
|
+
const LEGACY_WIKI_FILES = ['wiki.jsonc', 'wiki.json'];
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `_build_render.ensure_wiki_stub`, plus the rename. A legacy manifest is the user's file: it is
|
|
231
|
+
* RENAMED to `WIKI_FILE` (bytes preserved by construction), never copied and never seeded beside.
|
|
232
|
+
* A rename that fails (the file held open on Windows) leaves it where it is and seeds nothing —
|
|
233
|
+
* a seed beside it would fork the user's declarations; the readers' fallback still finds it.
|
|
234
|
+
* The sibling `.gitignore` follows the rename, or the private paths become committable.
|
|
235
|
+
*/
|
|
224
236
|
export function ensureWikiStub(out) {
|
|
225
|
-
|
|
237
|
+
const dest = path.join(out, WIKI_FILE);
|
|
238
|
+
if (existsSync(dest)) return;
|
|
239
|
+
const legacy = LEGACY_WIKI_FILES.find((n) => existsSync(path.join(out, n)));
|
|
240
|
+
if (!legacy) return seed(dest, WIKI_STUB);
|
|
241
|
+
try { renameSync(path.join(out, legacy), dest); } catch { return; }
|
|
242
|
+
ignoreRenamedWiki(path.join(out, '.gitignore'), legacy);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Add `WIKI_FILE` to a `.gitignore` that ignores the legacy name and not the new one, in the
|
|
246
|
+
* file's own line ending. Anything else — no file, no legacy line — is left alone. */
|
|
247
|
+
function ignoreRenamedWiki(gi, legacy) {
|
|
248
|
+
let text;
|
|
249
|
+
try { text = readFileSync(gi, 'utf8'); } catch { return; }
|
|
250
|
+
const lines = text.split(/\r?\n/);
|
|
251
|
+
if (!lines.includes(legacy) || lines.includes(WIKI_FILE)) return;
|
|
252
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
253
|
+
const sep = text.endsWith('\n') ? '' : eol;
|
|
254
|
+
writeFileSync(gi, `${text}${sep}${WIKI_FILE}${eol}`);
|
|
226
255
|
}
|
|
227
256
|
|
|
228
257
|
/** `_build_render.ensure_rules_stub`. */
|
|
@@ -264,7 +293,7 @@ export const ensureNotebookIndex = storeIndexWriter('NOTEBOOK.md', NOTEBOOK_INDE
|
|
|
264
293
|
* the two copies must move with it.
|
|
265
294
|
*/
|
|
266
295
|
export const SESSION_SEEDS = {
|
|
267
|
-
'user-rules.md': RULES_STUB, 'PROFILE.md': PROFILE_STUB,
|
|
296
|
+
'user-rules.md': RULES_STUB, 'PROFILE.md': PROFILE_STUB, [WIKI_FILE]: WIKI_STUB,
|
|
268
297
|
'MEMORY.md': MEMORY_INDEX, 'NOTEBOOK.md': NOTEBOOK_INDEX,
|
|
269
298
|
};
|
|
270
299
|
|
package/js/build/themes.mjs
CHANGED
|
@@ -144,9 +144,7 @@ export function syncThemes(themesDir = THEMES) {
|
|
|
144
144
|
} catch {
|
|
145
145
|
process.stdout.write(`[sync-themes] ${path.basename(tmplPath)} is missing or unreadable `
|
|
146
146
|
+ '— there is nothing to sync against.\n');
|
|
147
|
-
|
|
148
|
-
e.exitCode = 2;
|
|
149
|
-
throw e;
|
|
147
|
+
throw Object.assign(new Error('template unreadable'), { exitCode: 2 });
|
|
150
148
|
}
|
|
151
149
|
const tmplKeys = Object.keys(tmpl);
|
|
152
150
|
let changed = 0;
|
package/js/cli-table.json
CHANGED
|
@@ -1079,6 +1079,59 @@
|
|
|
1079
1079
|
],
|
|
1080
1080
|
"mutex": []
|
|
1081
1081
|
},
|
|
1082
|
+
{
|
|
1083
|
+
"name": "scorecard",
|
|
1084
|
+
"help": "grade a repo with the AI Harness Scorecard (31 checks, five pillars) — scores the repo itself (CI, tests, docs), not the harness files; a reading, never a gate",
|
|
1085
|
+
"description": "",
|
|
1086
|
+
"positionals": [
|
|
1087
|
+
{
|
|
1088
|
+
"names": [],
|
|
1089
|
+
"dest": "path",
|
|
1090
|
+
"metavar": null,
|
|
1091
|
+
"help": "the repo to score (default: the current directory)",
|
|
1092
|
+
"choices": null,
|
|
1093
|
+
"default": null,
|
|
1094
|
+
"required": false,
|
|
1095
|
+
"nargs": "?",
|
|
1096
|
+
"is_flag": false,
|
|
1097
|
+
"type": null,
|
|
1098
|
+
"hidden": false
|
|
1099
|
+
}
|
|
1100
|
+
],
|
|
1101
|
+
"options": [
|
|
1102
|
+
{
|
|
1103
|
+
"names": [
|
|
1104
|
+
"--json"
|
|
1105
|
+
],
|
|
1106
|
+
"dest": "json",
|
|
1107
|
+
"metavar": null,
|
|
1108
|
+
"help": "print the full assessment as JSON — every check with its score, evidence and fix",
|
|
1109
|
+
"choices": null,
|
|
1110
|
+
"default": false,
|
|
1111
|
+
"required": false,
|
|
1112
|
+
"nargs": "0",
|
|
1113
|
+
"is_flag": true,
|
|
1114
|
+
"type": null,
|
|
1115
|
+
"hidden": false
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
"names": [
|
|
1119
|
+
"--svg"
|
|
1120
|
+
],
|
|
1121
|
+
"dest": "svg",
|
|
1122
|
+
"metavar": "PATH",
|
|
1123
|
+
"help": "write the scorecard as an SVG card (grade + one bar per pillar) to PATH instead of printing — the README card is docs/assets/scorecard.svg",
|
|
1124
|
+
"choices": null,
|
|
1125
|
+
"default": null,
|
|
1126
|
+
"required": false,
|
|
1127
|
+
"nargs": null,
|
|
1128
|
+
"is_flag": false,
|
|
1129
|
+
"type": null,
|
|
1130
|
+
"hidden": false
|
|
1131
|
+
}
|
|
1132
|
+
],
|
|
1133
|
+
"mutex": []
|
|
1134
|
+
},
|
|
1082
1135
|
{
|
|
1083
1136
|
"name": "setup",
|
|
1084
1137
|
"help": "interactive install wizard (dependency-free, all OSes)",
|
|
@@ -32,7 +32,8 @@ const _ERRNO = {
|
|
|
32
32
|
function asOsError(e, filename) {
|
|
33
33
|
const hit = _ERRNO[e && e.code];
|
|
34
34
|
if (!hit) return String((e && e.message) || e);
|
|
35
|
-
//
|
|
35
|
+
// The `[Errno N] text: 'file'` shape this hook has always printed: the filename is quoted,
|
|
36
|
+
// so a Windows path comes out with its backslashes doubled.
|
|
36
37
|
return `[Errno ${hit[0]}] ${hit[1]}: ${JSON.stringify(filename).replace(/^"|"$/g, "'")
|
|
37
38
|
.replace(/\\"/g, '"')}`;
|
|
38
39
|
}
|
|
@@ -57,7 +58,7 @@ function fnTranslate(pat) {
|
|
|
57
58
|
if (pat[j] === '!') j += 1;
|
|
58
59
|
if (pat[j] === ']') j += 1;
|
|
59
60
|
while (j < pat.length && pat[j] !== ']') j += 1;
|
|
60
|
-
// An unterminated `[` is a LITERAL bracket
|
|
61
|
+
// An unterminated `[` is a LITERAL bracket (fnmatch semantics), not an error.
|
|
61
62
|
if (j >= pat.length) { out += '\\['; continue; }
|
|
62
63
|
let body = pat.slice(i + 1, j).replaceAll('\\', '\\\\');
|
|
63
64
|
if (body.startsWith('!')) body = `^${body.slice(1)}`;
|
|
@@ -77,9 +78,14 @@ function fnTranslate(pat) {
|
|
|
77
78
|
*/
|
|
78
79
|
let HOST = 'claude';
|
|
79
80
|
|
|
80
|
-
// Kept in step with adapters/opencode/plugins/geneseed-context.js
|
|
81
|
+
// Kept in step with adapters/opencode/plugins/geneseed-context.js.
|
|
81
82
|
const EAGER_ROOT = ['AGENTS.md', 'AGENT.md', 'CLAUDE.md', '.cursorrules',
|
|
82
|
-
'README.md', 'CONTRIBUTING.md', 'user-rules.md', 'PROFILE.md'];
|
|
83
|
+
'README.md', 'README.adoc', 'CONTRIBUTING.md', 'CONTRIBUTING.adoc', 'user-rules.md', 'PROFILE.md'];
|
|
84
|
+
// Project docs are Markdown or AsciiDoc: the document-project skill writes AsciiDoc when the
|
|
85
|
+
// user asks for it or the docs already are, so a loader blind to `.adoc` would hide those docs.
|
|
86
|
+
// Agent-runtime files (AGENTS.md, CLAUDE.md, specs) stay Markdown whatever the project uses.
|
|
87
|
+
const DOC_EXT = new Set(['.md', '.adoc']);
|
|
88
|
+
const isDoc = (name) => DOC_EXT.has(path.extname(name).toLowerCase());
|
|
83
89
|
const LAZY_DIRS = ['docs', 'doc', 'documentation', 'architecture', 'adr', 'ADR'];
|
|
84
90
|
const EXCLUDE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'vendor', '.next',
|
|
85
91
|
'target', '.venv', '__pycache__', '.opencode', '.harness']);
|
|
@@ -120,7 +126,10 @@ const cost = (s) => s.length + (s.match(/\n/g) || []).length + 2;
|
|
|
120
126
|
// honoured by `resolveContextSets` (the harness dir is its last candidate). The OpenCode
|
|
121
127
|
// context plugin carries the same list.
|
|
122
128
|
const SESSION_FILES = ['user-rules.md', 'PROFILE.md', 'memory/MEMORY.md', 'anamnesis/MEMORY.md',
|
|
123
|
-
'notebook/NOTEBOOK.md', 'wiki.jsonc'];
|
|
129
|
+
'notebook/NOTEBOOK.md', 'geneseed-wiki.jsonc'];
|
|
130
|
+
// The wiki manifest's earlier name, read when the new one is absent (an install not yet re-emitted,
|
|
131
|
+
// or a rename that failed). One release; `wiki.json`, older still, is the build's to rename.
|
|
132
|
+
const LEGACY_WIKI = 'wiki.jsonc';
|
|
124
133
|
// SHA-256 of each seed body in `js/build/stubs.mjs` (`SESSION_SEEDS`), CRLF folded. A file still
|
|
125
134
|
// byte-identical to its seed says nothing, so it is skipped rather than injected as noise.
|
|
126
135
|
// Hashes, not an import: stubs.mjs pulls in the build's writers, and this module loads on every
|
|
@@ -128,7 +137,9 @@ const SESSION_FILES = ['user-rules.md', 'PROFILE.md', 'memory/MEMORY.md', 'anamn
|
|
|
128
137
|
const SEED_SHA256 = new Set([
|
|
129
138
|
'5c2e92fb1acde041e02d9ebf158460d8ff7a07cbc2b447859631604610130721', // user-rules.md
|
|
130
139
|
'29e012c3c4349ea62a9082cbd04bb25599a0ec280d62c33f6e10742809471816', // PROFILE.md
|
|
131
|
-
'
|
|
140
|
+
'3a82c78ccc5d745033c16c354f5eda9d79350c704e9da8d605d380fb1e108d7a', // geneseed-wiki.jsonc
|
|
141
|
+
// The wiki seed before the rename: the build renames an untouched one byte for byte.
|
|
142
|
+
'cccc917c34e6b990e821620bdb4739090151885ab71b6e85046f30a3b71fa5e8', // wiki.jsonc (legacy)
|
|
132
143
|
'99c786049c260f6baf7aec68a5c0f59907861afe9b867da009440613bdddb907', // MEMORY.md
|
|
133
144
|
'9acb5c9d9cb5dac572104480f05b48cc144e676869bd60e86d8a15b1f6308184', // NOTEBOOK.md
|
|
134
145
|
]);
|
|
@@ -144,8 +155,9 @@ export function sessionFiles(hookRoot) {
|
|
|
144
155
|
const found = [];
|
|
145
156
|
for (const rel of SESSION_FILES) {
|
|
146
157
|
let abs = path.join(hookRoot, rel);
|
|
147
|
-
if (rel === 'wiki.jsonc'
|
|
148
|
-
abs = process.env.GENESEED_WIKI;
|
|
158
|
+
if (rel === 'geneseed-wiki.jsonc') {
|
|
159
|
+
if (process.env.GENESEED_WIKI && isFile(process.env.GENESEED_WIKI)) abs = process.env.GENESEED_WIKI;
|
|
160
|
+
else if (!isFile(abs)) abs = path.join(hookRoot, LEGACY_WIKI);
|
|
149
161
|
}
|
|
150
162
|
if (!isFile(abs)) continue;
|
|
151
163
|
let text;
|
|
@@ -182,9 +194,8 @@ export function globalHookStandingDown(hookRoot, cwd) {
|
|
|
182
194
|
|
|
183
195
|
/** `_disp` — relative to the repo root when it sits under it, else verbatim. */
|
|
184
196
|
function disp(pathStr, root) {
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// print a DIFFERENT string rather than the same one.
|
|
197
|
+
// A path on another drive has no relative form, so it is printed as given. `path.relative`
|
|
198
|
+
// would silently return an absolute path instead — a different string, not the same one.
|
|
188
199
|
if (path.parse(path.resolve(pathStr)).root.toLowerCase()
|
|
189
200
|
!== path.parse(path.resolve(root)).root.toLowerCase()) {
|
|
190
201
|
return pathStr;
|
|
@@ -193,18 +204,18 @@ function disp(pathStr, root) {
|
|
|
193
204
|
}
|
|
194
205
|
|
|
195
206
|
|
|
196
|
-
function
|
|
207
|
+
function rglobDocs(dir, acc = []) {
|
|
197
208
|
for (const name of listDir(dir)) {
|
|
198
209
|
const full = path.join(dir, name);
|
|
199
|
-
if (isDir(full))
|
|
200
|
-
else if (isFile(full) &&
|
|
210
|
+
if (isDir(full)) rglobDocs(full, acc);
|
|
211
|
+
else if (isFile(full) && isDoc(name)) acc.push(full);
|
|
201
212
|
}
|
|
202
213
|
return acc;
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
/**
|
|
206
217
|
* `_discover_context` — the no-manifest path, mirroring the OpenCode context plugin.
|
|
207
|
-
* Root entry docs are eager; other root
|
|
218
|
+
* Root entry docs are eager; other root docs (.md or .adoc), the doc trees and monorepo package
|
|
208
219
|
* READMEs are lazy.
|
|
209
220
|
*/
|
|
210
221
|
export function discoverContext(root, host = HOST) {
|
|
@@ -217,12 +228,12 @@ export function discoverContext(root, host = HOST) {
|
|
|
217
228
|
const name = path.basename(full);
|
|
218
229
|
if (native.includes(name)) continue;
|
|
219
230
|
if (EAGER_ROOT.includes(name)) eager.set(full, null);
|
|
220
|
-
else if (
|
|
231
|
+
else if (isDoc(name)) lazy.set(full, null);
|
|
221
232
|
}
|
|
222
233
|
for (const d of LAZY_DIRS) {
|
|
223
234
|
const sub = path.join(root, d);
|
|
224
235
|
if (!isDir(sub)) continue;
|
|
225
|
-
for (const md of sortPaths(
|
|
236
|
+
for (const md of sortPaths(rglobDocs(sub))) {
|
|
226
237
|
const parts = path.relative(root, md).split(/[\\/]/);
|
|
227
238
|
if (parts.some((part) => EXCLUDE_DIRS.has(part))) continue;
|
|
228
239
|
if (!eager.has(md) && !lazy.has(md)) lazy.set(md, null);
|
|
@@ -233,8 +244,10 @@ export function discoverContext(root, host = HOST) {
|
|
|
233
244
|
if (!isDir(base)) continue;
|
|
234
245
|
for (const pkg of sortPaths(listDir(base).map((n) => path.join(base, n)))) {
|
|
235
246
|
if (!isDir(pkg) || EXCLUDE_DIRS.has(path.basename(pkg))) continue;
|
|
236
|
-
const
|
|
237
|
-
|
|
247
|
+
for (const name of ['README.md', 'README.adoc']) {
|
|
248
|
+
const readme = path.join(pkg, name);
|
|
249
|
+
if (isFile(readme) && !eager.has(readme) && !lazy.has(readme)) lazy.set(readme, null);
|
|
250
|
+
}
|
|
238
251
|
}
|
|
239
252
|
}
|
|
240
253
|
return [
|
|
@@ -249,6 +262,7 @@ export function discoverContext(root, host = HOST) {
|
|
|
249
262
|
*
|
|
250
263
|
* `recs` is a Map, not an object: JS reorders integer-like keys on a plain object, and
|
|
251
264
|
* these keys are absolute paths whose iteration order becomes the printed order.
|
|
265
|
+
* @returns {[Array<{path: string, description: string}>, Array<{path: string, description: string}>, string]}
|
|
252
266
|
*/
|
|
253
267
|
export function resolveContextSets(root, hookRoot = null) {
|
|
254
268
|
let manifest = null;
|
|
@@ -281,19 +295,18 @@ export function resolveContextSets(root, hookRoot = null) {
|
|
|
281
295
|
let extend;
|
|
282
296
|
try {
|
|
283
297
|
let data = JSON.parse(raw);
|
|
284
|
-
// Valid JSON of the wrong SHAPE,
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
298
|
+
// Valid JSON of the wrong SHAPE (null, a number, an array) is an empty manifest, not a
|
|
299
|
+
// syntax error. The guard is written out rather than left to fall out of the language:
|
|
300
|
+
// without it, `null.context` throws into the catch below and a file a user really does
|
|
301
|
+
// hand-edit is reported as invalid JSON when it is not.
|
|
288
302
|
data = data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
289
303
|
entries = data.context || [];
|
|
290
304
|
if (!Array.isArray(entries)) entries = [];
|
|
291
305
|
extend = Boolean(data.extend);
|
|
292
306
|
} catch {
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// WHICH decoder found it is what makes two implementations impossible.
|
|
307
|
+
// The decoder's own message is not quoted: its wording and offset are engine details,
|
|
308
|
+
// and V8 supplies no offset at all for the commonest shapes. Reporting the file is what
|
|
309
|
+
// the user needs.
|
|
297
310
|
err(`[context] ${manifest} is not valid JSON — no context was loaded `
|
|
298
311
|
+ '(fix the syntax, then re-run).\n');
|
|
299
312
|
return [[], [], String(manifest)];
|
package/js/hosts/hooks.mjs
CHANGED
|
@@ -201,7 +201,7 @@ const BOB_DENY_EXIT = 2;
|
|
|
201
201
|
* logs. `gate-error` is a warning too: a crashed gate that blocked every tool call would be
|
|
202
202
|
* a lockout, not a safeguard, and there is no prompt through which the user could clear it.
|
|
203
203
|
*/
|
|
204
|
-
const BLOCK_RULES = new Set(['law-1', 'law-4']);
|
|
204
|
+
const BLOCK_RULES = new Set(['law-1', 'law-4', 'rigor-5']);
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
207
|
* THE GATE LEDGER — one JSON line per ask, nothing per defer.
|
|
@@ -358,6 +358,33 @@ const SECRET_RE =
|
|
|
358
358
|
/\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|sk-ant-[A-Za-z0-9_-]{20,}|xox[abprs]-[0-9A-Za-z-]{10,})\b|-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
359
359
|
const DOTENV_RE = /(^|[\\/])\.env(\.[^\\/]*)?$/;
|
|
360
360
|
|
|
361
|
+
/**
|
|
362
|
+
* External Gate's boundary half: the checks an agent is judged by must sit outside what it can
|
|
363
|
+
* edit, or a failing check is one edit away from a passing one. A project opts in by listing
|
|
364
|
+
* them, one repo-relative path per line (`#` comments), in `.geneseed/protected-checks.txt` at
|
|
365
|
+
* its git root — found by walking up from the WRITE TARGET, not the cwd, so a session in one repo
|
|
366
|
+
* writing into another reads that other repo's list. The list protects itself. Returns the entry
|
|
367
|
+
* the path falls under, or null. ponytail: a write tool only — a shell `sed -i` or redirect is
|
|
368
|
+
* not seen here; CI running the checks is the boundary that holds against that.
|
|
369
|
+
*/
|
|
370
|
+
export const SENSOR_LIST = '.geneseed/protected-checks.txt';
|
|
371
|
+
function protectedCheck(p) {
|
|
372
|
+
try {
|
|
373
|
+
const target = resolvePath(p);
|
|
374
|
+
const root = gitRootOf(path.dirname(target));
|
|
375
|
+
if (!root) return null;
|
|
376
|
+
const text = readFileSync(path.join(root, SENSOR_LIST), 'utf8');
|
|
377
|
+
// `normcase` folds case AND slashes on Windows, so both sides go through it, the trailing
|
|
378
|
+
// separator of the prefix included.
|
|
379
|
+
const rel = normcase(path.relative(root, target));
|
|
380
|
+
const entries = [SENSOR_LIST, ...text.split(/\r?\n/).map((l) => l.trim().replace(/[\\/]+$/, ''))
|
|
381
|
+
.filter((l) => l && !l.startsWith('#'))];
|
|
382
|
+
return entries.find((e) => rel === normcase(e) || rel.startsWith(normcase(`${e}/`))) ?? null;
|
|
383
|
+
} catch {
|
|
384
|
+
return null; // no list (the common case) or an unreadable one: nothing is protected
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
361
388
|
function ruleGate(args) {
|
|
362
389
|
if (sovereignBypass(args.root)) return 0;
|
|
363
390
|
return ruleDecide(args, readPayload());
|
|
@@ -378,6 +405,12 @@ function ruleDecide(args, payload) {
|
|
|
378
405
|
+ 'credential-shaped string. Secrets live in .env or a secret manager, never in a '
|
|
379
406
|
+ 'tracked file.');
|
|
380
407
|
}
|
|
408
|
+
const sensor = protectedCheck(p);
|
|
409
|
+
if (sensor) {
|
|
410
|
+
return ask(args, 'rule-gate', 'rigor-5', `Geneseed (External Gate) — ${p} is a protected `
|
|
411
|
+
+ `check (${sensor}, listed in ${SENSOR_LIST}). A check the agent can edit is not a `
|
|
412
|
+
+ 'check: fix the code, not the check, unless the user asks for this edit.');
|
|
413
|
+
}
|
|
381
414
|
const target = ruleGateTarget(p, args.root);
|
|
382
415
|
if (!target) return 0;
|
|
383
416
|
return ask(args, 'rule-gate', 'process-1', `Geneseed (Persist Insight) \u2014 writing to `
|
package/js/hosts/hosts.mjs
CHANGED
|
@@ -128,9 +128,7 @@ export function expanduser(p) {
|
|
|
128
128
|
+ "ntpath/posixpath would guess a home directory for 'user' — this port does not); "
|
|
129
129
|
+ 'pass an absolute path, or "~" / "~/…" for your own home directory';
|
|
130
130
|
printErr(`${msg}\n`);
|
|
131
|
-
|
|
132
|
-
e.exitCode = 1;
|
|
133
|
-
throw e;
|
|
131
|
+
throw Object.assign(new Error(msg), { exitCode: 1 });
|
|
134
132
|
}
|
|
135
133
|
return p;
|
|
136
134
|
}
|
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
|
|