geneseed 3.9.0 → 3.10.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 +32 -1
- package/QUICKSTART.md +1 -1
- package/README.md +27 -28
- package/SETUP.md +7 -42
- package/SHIPPED.md +2 -2
- package/adapters/bob/README.md +17 -1
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +78 -8
- package/bin/geneseed-hook.mjs +1 -1
- package/docs/candidate-hosts.md +12 -5
- package/docs/token-footprint.md +26 -21
- package/docs/web/claude-hooks.md +1 -1
- package/docs/web/collaboration.md +1 -1
- package/docs/web/exclusions.md +0 -1
- package/docs/web/footprint.md +1 -1
- package/docs/web/harnesses.md +13 -13
- package/docs/web/install-quick.md +1 -1
- package/docs/web-ui.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +4 -4
- package/js/build/driver.mjs +26 -95
- package/js/build/emit-claude.mjs +76 -81
- package/js/build/generate.mjs +0 -1
- package/js/build/render.mjs +4 -0
- package/js/build/stubs.mjs +18 -2
- package/js/cli-table.json +5 -5
- package/js/hosts/hooks.mjs +86 -54
- package/js/hosts/hosts.mjs +5 -21
- package/js/hosts/installs.mjs +4 -6
- package/js/hosts/mcp.mjs +3 -13
- package/js/hosts/native.mjs +27 -50
- package/js/hosts/opencode.mjs +1 -1
- package/js/hosts/settings.mjs +4 -78
- package/js/inspect/checks-authoring.mjs +4 -1
- package/js/inspect/checks-repo.mjs +45 -1
- package/js/inspect/diff.mjs +11 -3
- package/js/inspect/doctor.mjs +3 -3
- package/js/inspect/excludes.mjs +0 -4
- package/js/inspect/status.mjs +4 -4
- package/js/maintain/migrate.mjs +5 -5
- package/js/maintain/setup.mjs +3 -5
- package/js/maintain/uninstall.mjs +8 -42
- package/js/web/actions.mjs +4 -5
- package/js/web/api.mjs +4 -4
- package/package.json +1 -2
- package/src/AGENT.md.tmpl +69 -52
- package/src/laws/universal.md +3 -0
- package/src/ontology/universal.md +1 -1
- package/src/skills/daydream/SKILL.md +10 -3
- package/src/skills/daydream/VENDOR.md +10 -2
- package/src/skills/daydream/instructions.md +2 -2
- package/src/skills/explain-changes/SKILL.md +1 -1
- package/src/skills/explain-changes/VENDOR.md +1 -1
- package/src/skills/geneseed.md +1 -1
- package/src/skills/react-view-transitions/SKILL.md +1 -1
- package/src/skills/react-view-transitions/VENDOR.md +7 -2
- package/src/skills/token-report/SKILL.md +2 -3
- package/src/skills/token-report/VENDOR.md +2 -3
- package/src/skills/token-report/scripts/token_report.mjs +7 -121
- package/src/skills/workflow.md +1 -1
- package/web/dist/assets/{Activity-DAzV-qQj.js → Activity-BjVp522q.js} +1 -1
- package/web/dist/assets/{ActivityDetail-CwEwMtKl.js → ActivityDetail-8ep1bspW.js} +1 -1
- package/web/dist/assets/{Dashboard-C2ELBW-X.js → Dashboard-OkC_PCH6.js} +2 -2
- package/web/dist/assets/{Docs-iZrvQP8J.js → Docs-CKPWz00o.js} +1 -1
- package/web/dist/assets/{GreenhouseView-Bq1QmaGO.js → GreenhouseView-D0cbCJ5m.js} +1 -1
- package/web/dist/assets/{Installs-BiqR8xdO.js → Installs-BtGGdoTt.js} +1 -1
- package/web/dist/assets/{JournalView-6wilG5Zd.js → JournalView-Ckl4dmzR.js} +1 -1
- package/web/dist/assets/{Laws-D8YXD_XE.js → Laws-CLpdRRk8.js} +1 -1
- package/web/dist/assets/{Library-DwuTZ2-L.js → Library-wPPuKQrI.js} +1 -1
- package/web/dist/assets/{OperatorHudView-B-HVBqfY.js → OperatorHudView-CiAkAkQL.js} +1 -1
- package/web/dist/assets/{Personal-BbUTmZQR.js → Personal-D8CnDo9v.js} +1 -1
- package/web/dist/assets/{Tabs-_pjUFIEI.js → Tabs-B4HNrrCs.js} +1 -1
- package/web/dist/assets/{drawable-CoIulEH2.js → drawable-BiwaOo6x.js} +1 -1
- package/web/dist/assets/{index-BMwSp8BP.js → index-C1yil5j1.js} +2 -2
- package/web/dist/index.html +1 -1
- package/adapters/copilot/README.md +0 -105
package/js/hosts/native.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A faithful translation of `_build_emit._write_native_layer` and the frontmatter
|
|
5
5
|
* builders it dispatches to. Capability specs become host-native subagents and skills:
|
|
6
|
-
* one dialect per host (OpenCode / Claude Code
|
|
6
|
+
* one dialect per host (OpenCode / Claude Code), vendored skill folders copied
|
|
7
7
|
* through verbatim, authoring templates shipped flat.
|
|
8
8
|
*
|
|
9
9
|
* WHY THIS IS THE HARD HALF. `js/build/render.mjs` is a pure function of `src/` and `themes/`;
|
|
@@ -33,6 +33,9 @@ import path from 'node:path';
|
|
|
33
33
|
import { writeText, copyFile, readText } from '../lib/fs.mjs';
|
|
34
34
|
import { jsonDumps, parseJson, formatValue, formatRepr, isTruthy, isDict } from '../lib/json.mjs';
|
|
35
35
|
|
|
36
|
+
/** What a folder skill writes where it needs its own directory — see `writeNativeLayer`. */
|
|
37
|
+
export const SKILL_DIR_PLACEHOLDER = '<this-skill-directory>';
|
|
38
|
+
|
|
36
39
|
/** Mirrors `_build_core.VENDORED_SKILL_DIRS`. */
|
|
37
40
|
export const VENDORED_SKILL_DIRS = new Set(['react-view-transitions', 'daydream', 'token-report', 'explain-changes']);
|
|
38
41
|
|
|
@@ -58,7 +61,7 @@ export function isVendoredPath(rel) {
|
|
|
58
61
|
* `build._validate_is_vendored` — the same question, tolerant of `skills` at ANY depth.
|
|
59
62
|
*
|
|
60
63
|
* The per-repo native layers nest one level deeper than a `files`/opencode-global bundle
|
|
61
|
-
* (`.claude/skills/<name>/…`, `.bob/skills/<name>/…`, `.
|
|
64
|
+
* (`.claude/skills/<name>/…`, `.bob/skills/<name>/…`, `.openclaude/skills/<name>/…`), so
|
|
62
65
|
* `doctor`'s scan of those trees needs the loose form or every vendored folder's own
|
|
63
66
|
* upstream cross-links read as dead links. Never the last segment — `parts[:-1]`, because a
|
|
64
67
|
* FILE named `skills` is not a directory of them.
|
|
@@ -79,8 +82,6 @@ const AGENT_COLORS = {
|
|
|
79
82
|
const VALID_AGENT_COLOR_SLOTS = new Set(
|
|
80
83
|
['primary', 'secondary', 'accent', 'success', 'warning', 'error', 'info']);
|
|
81
84
|
|
|
82
|
-
/** `_build_emit._SIBLING_AGENT_LINK_RE` — a bare same-dir agent filename in a link. */
|
|
83
|
-
const SIBLING_AGENT_LINK_RE = /\]\(([A-Za-z0-9_-]+)\.md\)/g;
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
87
|
* `_build_emit._strip_skill_body_links`'s pattern: every RELATIVE markdown link to a
|
|
@@ -296,51 +297,23 @@ const BASH_MARKER = '<!-- bash: allow -->';
|
|
|
296
297
|
const WEBFETCH_MARKER = '<!-- webfetch: allow -->';
|
|
297
298
|
|
|
298
299
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* the
|
|
302
|
-
* row is `[tool, marker, listedWhenMarked]`: `false` for Claude (the tool is denied UNLESS
|
|
303
|
-
* the marker allows it) and `true` for Copilot (the tool is allowed ONLY IF the marker is
|
|
304
|
-
* present) — the same marker, read in the opposite sense a denylist and an allowlist
|
|
305
|
-
* require. Row order is emit order: WebFetch before Bash keeps the unmarked Claude line
|
|
306
|
-
* byte-identical to what it was before the webfetch marker existed.
|
|
300
|
+
* `_build_emit._claude_agent_frontmatter`. A read-only spec gets a `disallowedTools` DENYLIST;
|
|
301
|
+
* each opt-in marker takes its tool back off the list. Order is emit order: WebFetch before
|
|
302
|
+
* Bash keeps the unmarked line byte-identical to what it was before the webfetch marker existed.
|
|
307
303
|
*/
|
|
308
|
-
|
|
309
|
-
claude: {
|
|
310
|
-
key: 'disallowedTools', tools: ['Write', 'Edit', 'NotebookEdit'], brackets: false,
|
|
311
|
-
gated: [['WebFetch', WEBFETCH_MARKER, false], ['Bash', BASH_MARKER, false]],
|
|
312
|
-
},
|
|
313
|
-
copilot: {
|
|
314
|
-
key: 'tools', tools: ['read', 'search', 'todo', 'agent'], brackets: true,
|
|
315
|
-
gated: [['fetch', WEBFETCH_MARKER, true], ['execute', BASH_MARKER, true]],
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
/** `_build_emit._claude_agent_frontmatter` / `._copilot_agent_frontmatter`. */
|
|
320
|
-
function claudeCopilotAgentFrontmatter(host, stem, text, overrides) {
|
|
304
|
+
function claudeAgentFrontmatter(stem, text, overrides) {
|
|
321
305
|
const fm = [`name: ${stem}`, `description: ${jsonDumps(descOf(text))}`];
|
|
322
306
|
const ov = agentOverride(overrides, stem);
|
|
323
307
|
if (isTruthy(ov.model)) fm.push(`model: ${formatValue(ov.model)}`);
|
|
324
308
|
if (isReadonly(text)) {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
fm.push(`${key}: ${brackets ? `[${list.join(', ')}]` : list.join(', ')}`);
|
|
309
|
+
const list = ['Write', 'Edit', 'NotebookEdit'];
|
|
310
|
+
if (!text.includes(WEBFETCH_MARKER)) list.push('WebFetch');
|
|
311
|
+
if (!text.includes(BASH_MARKER)) list.push('Bash');
|
|
312
|
+
fm.push(`disallowedTools: ${list.join(', ')}`);
|
|
331
313
|
}
|
|
332
314
|
return fm;
|
|
333
315
|
}
|
|
334
316
|
|
|
335
|
-
function claudeAgentFrontmatter(stem, text, overrides) {
|
|
336
|
-
return claudeCopilotAgentFrontmatter('claude', stem, text, overrides);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** `tools:` is an ALLOWLIST, not a denylist. */
|
|
340
|
-
function copilotAgentFrontmatter(stem, text, overrides) {
|
|
341
|
-
return claudeCopilotAgentFrontmatter('copilot', stem, text, overrides);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
317
|
/** `_build_emit._opencode_agent_frontmatter`. */
|
|
345
318
|
function opencodeAgentFrontmatter(stem, text, overrides, theme = null) {
|
|
346
319
|
const fm = [`description: ${jsonDumps(descOf(text))}`, 'mode: subagent'];
|
|
@@ -387,6 +360,14 @@ function relPosix(base, target) {
|
|
|
387
360
|
* source root it is relativised against (Python reads `_build_core.SRC` directly, which
|
|
388
361
|
* is the module-level single owner; here it travels explicitly, as in `js/build/render.mjs`).
|
|
389
362
|
*
|
|
363
|
+
* `opts.skillDirOf(name)` — set for a host that hands the model a skill's TEXT but not where
|
|
364
|
+
* the skill lives (Bob). A folder skill names its scripts and prompts
|
|
365
|
+
* `<this-skill-directory>/…`; Claude Code resolves that because it announces each skill's
|
|
366
|
+
* base directory on load, while on Bob the model got the text with a hole in it and went
|
|
367
|
+
* hunting through the filesystem. With the option, every rendered `.md` of a vendored folder
|
|
368
|
+
* has the placeholder replaced by the answer. Only vendored folders carry the placeholder — a
|
|
369
|
+
* flat skill has no siblings to point at.
|
|
370
|
+
*
|
|
390
371
|
* Returns `{ nAgents, nSkills, written }` — Python's 3-tuple, with `written` as absolute
|
|
391
372
|
* paths in write order.
|
|
392
373
|
*
|
|
@@ -400,7 +381,7 @@ function relPosix(base, target) {
|
|
|
400
381
|
*/
|
|
401
382
|
export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null, {
|
|
402
383
|
host = 'opencode', oldOwned = null, cfg = null, manifestExisted = true,
|
|
403
|
-
theme = null, src,
|
|
384
|
+
theme = null, src, skillDirOf = null,
|
|
404
385
|
} = {}) {
|
|
405
386
|
const ov = overrides || {};
|
|
406
387
|
const oldSet = oldOwned !== null && oldOwned !== undefined ? new Set(oldOwned) : null;
|
|
@@ -445,8 +426,10 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
445
426
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
446
427
|
// The same vendored folder mixes both writers: a rendered `.md` goes through
|
|
447
428
|
// writeText (and picks up CRLF on Windows), a binary through copy2 (and keeps LF).
|
|
448
|
-
if (text !== null)
|
|
449
|
-
|
|
429
|
+
if (text !== null) {
|
|
430
|
+
writeText(dest, skillDirOf === null ? text
|
|
431
|
+
: text.replaceAll(SKILL_DIR_PLACEHOLDER, skillDirOf(sparts[1])));
|
|
432
|
+
} else copyFile(source, dest);
|
|
450
433
|
written.push(dest);
|
|
451
434
|
continue;
|
|
452
435
|
}
|
|
@@ -480,12 +463,6 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
480
463
|
if (host === 'claude') {
|
|
481
464
|
fm = claudeAgentFrontmatter(stem, text, ov);
|
|
482
465
|
dest = path.join(agentsDir, `${stem}.md`);
|
|
483
|
-
} else if (host === 'copilot') {
|
|
484
|
-
fm = copilotAgentFrontmatter(stem, text, ov);
|
|
485
|
-
dest = path.join(agentsDir, `${stem}.agent.md`);
|
|
486
|
-
// The filename change drags the links with it: a sibling `](skeptic.md)` would
|
|
487
|
-
// point at a file this dialect never writes.
|
|
488
|
-
body = body.replace(SIBLING_AGENT_LINK_RE, ']($1.agent.md)');
|
|
489
466
|
} else {
|
|
490
467
|
fm = opencodeAgentFrontmatter(stem, text, ov, theme);
|
|
491
468
|
dest = path.join(agentsDir, `${stem}.md`);
|
|
@@ -494,7 +471,7 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
494
471
|
} else {
|
|
495
472
|
// Skills are BYTE-IDENTICAL across hosts: name + description, body link-stripped.
|
|
496
473
|
// The user-only key is emitted for every host too — Claude Code and Bob honour it,
|
|
497
|
-
// OpenCode
|
|
474
|
+
// OpenCode ignores an unknown key — so the identity holds.
|
|
498
475
|
fm = [`name: ${stem}`, `description: ${jsonDumps(skillDescription(text))}`];
|
|
499
476
|
if (isUserInvokedOnly(text)) fm.push('disable-model-invocation: true');
|
|
500
477
|
body = stripSkillBodyLinks(body);
|
package/js/hosts/opencode.mjs
CHANGED
|
@@ -84,7 +84,7 @@ const AGENT_OVERRIDES_STUB = {
|
|
|
84
84
|
_recommended: {
|
|
85
85
|
_comment: (
|
|
86
86
|
'Cheap-tier seats: copy the ones you want into "agents" with your host\'s cheap '
|
|
87
|
-
+ 'model id (OpenCode: "anthropic/claude-haiku-4-5"; Claude Code
|
|
87
|
+
+ 'model id (OpenCode: "anthropic/claude-haiku-4-5"; Claude Code: "haiku"). '
|
|
88
88
|
+ 'Read-and-summarise work — a council convenes several at once, so this is where '
|
|
89
89
|
+ 'the savings compound. Nothing applies until you copy it.'
|
|
90
90
|
),
|
package/js/hosts/settings.mjs
CHANGED
|
@@ -649,7 +649,7 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
649
649
|
* rule is no longer BINDING — so a gate that stopped every commit to ask for consent to a rule
|
|
650
650
|
* the install did not adopt is the disagreement the three-tier split forbids. The group stays
|
|
651
651
|
* and gains `--no-consent`, which skips only the process-5 branch. It once DROPPED the whole
|
|
652
|
-
* group instead, and Law IV went with it — on Claude only, since Bob
|
|
652
|
+
* group instead, and Law IV went with it — on Claude only, since Bob carries it
|
|
653
653
|
* through `tool-gate`. The command changing is also what re-wires an existing install:
|
|
654
654
|
* `mergeClaudeSettings` prunes every previously-managed group that is not in this return
|
|
655
655
|
* value.
|
|
@@ -700,22 +700,6 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
700
700
|
Stop: [{ hooks: [{ type: 'command', command: `${run} learn ${mem} || exit 0` }] }],
|
|
701
701
|
};
|
|
702
702
|
}
|
|
703
|
-
if (host === 'copilot') {
|
|
704
|
-
// COPILOT'S OWN SCHEMA (docs.github.com/en/copilot/reference/hooks-configuration): camelCase
|
|
705
|
-
// events, each an ARRAY of `{type: "command", command}` entries — no matcher groups, so the
|
|
706
|
-
// array elements ARE the groups this merge appends and prunes. `preToolUse` answers with
|
|
707
|
-
// a top-level `permissionDecision` (`--host copilot`), and `agentStop`/`preCompact` carry
|
|
708
|
-
// `transcriptPath`, so `learn` runs here as on Claude. No matcher, so `tool-gate` reads
|
|
709
|
-
// the payload's shape. Unverified live: no Copilot CLI on the authoring machine.
|
|
710
|
-
const c = ' --host copilot';
|
|
711
|
-
const entry = (command) => [{ type: 'command', command }];
|
|
712
|
-
return {
|
|
713
|
-
sessionStart: entry(`${run} context --root "${cfg}"${c} || exit 0`),
|
|
714
|
-
preToolUse: entry(`${run} tool-gate --root "${cfg}"${c}`),
|
|
715
|
-
agentStop: entry(`${run} learn ${mem} || exit 0`),
|
|
716
|
-
preCompact: entry(`${run} learn ${mem} || exit 0`),
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
703
|
// OpenClaude takes Claude's whole group set and verdicts unchanged; only `context` needs
|
|
720
704
|
// the host, to know which root file OpenClaude already loads by itself.
|
|
721
705
|
const h = host === 'openclaude' ? ' --host openclaude' : '';
|
|
@@ -753,63 +737,6 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
753
737
|
};
|
|
754
738
|
}
|
|
755
739
|
|
|
756
|
-
/**
|
|
757
|
-
* LEGACY: the single-slot Copilot shape (`hooks.<event>` = ONE object, events `sessionStart` /
|
|
758
|
-
* `toolCall`) an emit wrote before Copilot's hooks became event → array. Copilot has no
|
|
759
|
-
* `toolCall` event any more, so those gates never ran. Nothing writes this shape now; these
|
|
760
|
-
* two only take an older install's recorded `copilot_hooks` claims back out.
|
|
761
|
-
*/
|
|
762
|
-
/** Remove exactly the recorded Copilot hooks from `p`. True when the file was rewritten. */
|
|
763
|
-
export function unwireCopilotSettings(p, recorded) {
|
|
764
|
-
if (!existsSync(p) || !recorded || !recorded.length) return false;
|
|
765
|
-
let loaded;
|
|
766
|
-
let hadComments;
|
|
767
|
-
try {
|
|
768
|
-
[loaded, hadComments] = readJsonc(readText(p));
|
|
769
|
-
} catch (e) {
|
|
770
|
-
if (!isOsError(e)) throw e;
|
|
771
|
-
return false;
|
|
772
|
-
}
|
|
773
|
-
if (hadComments || !isDict(loaded)) return false;
|
|
774
|
-
const hooks = get(loaded, 'hooks');
|
|
775
|
-
if (!isDict(hooks)) return false;
|
|
776
|
-
for (const rec of recorded) {
|
|
777
|
-
const event = get(rec, 'event');
|
|
778
|
-
if (deepEquals(get(hooks, event), get(rec, 'hook'))) delete hooks[event];
|
|
779
|
-
}
|
|
780
|
-
if (!Object.keys(hooks).length) delete loaded.hooks;
|
|
781
|
-
try {
|
|
782
|
-
atomicWriteJson(p, loaded);
|
|
783
|
-
} catch (e) {
|
|
784
|
-
if (!isOsError(e)) throw e;
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
return true;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* The Copilot twin of `settingsIntegrityCheck`: every recorded hook present (or absent) in
|
|
792
|
-
* the file it was claimed against. Same contract — problems as strings, each also WARNed on
|
|
793
|
-
* stderr, empty when clean — so a merge that did not stick is loud at emit time rather than
|
|
794
|
-
* a hook that quietly never fires.
|
|
795
|
-
*/
|
|
796
|
-
export function copilotIntegrityCheck(p, recorded, expect = 'present') {
|
|
797
|
-
const problems = [];
|
|
798
|
-
const hooks = existsSync(p) ? (() => {
|
|
799
|
-
try { return get(readJsonc(readText(p))[0], 'hooks'); } catch { return null; }
|
|
800
|
-
})() : null;
|
|
801
|
-
for (const rec of (recorded || []).filter(isDict)) {
|
|
802
|
-
const hit = deepEquals(get(hooks, rec.event), rec.hook);
|
|
803
|
-
if (expect === 'present' && !hit) {
|
|
804
|
-
problems.push(`${p}: recorded Copilot hook missing — event=${rec.event}`);
|
|
805
|
-
} else if (expect === 'absent' && hit) {
|
|
806
|
-
problems.push(`${p}: recorded Copilot hook still present after unwire — event=${rec.event}`);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
for (const x of problems) process.stderr.write(`[geneseed] WARN: ${x}\n`);
|
|
810
|
-
return problems;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
740
|
/**
|
|
814
741
|
* `_build_settings._merge_claude_settings` — returns `[target, managed]`.
|
|
815
742
|
*
|
|
@@ -1045,8 +972,8 @@ const SHIM_ENTRY_MARK = 'geneseed-hook.mjs';
|
|
|
1045
972
|
*
|
|
1046
973
|
* PURE — and currently reached only END TO END, by the `migrate/a-node-baked-shim-reads-as-current`
|
|
1047
974
|
* cell. No unit corpus drives it directly, which is worth knowing before trusting it.
|
|
1048
|
-
* 'none' is not a fault: a
|
|
1049
|
-
*
|
|
975
|
+
* 'none' is not a fault: a user's own settings.json may carry three hand-written hooks
|
|
976
|
+
* and no Geneseed entry. That must read as
|
|
1050
977
|
* "nothing to migrate" rather than "unrecognised", or `migrate` refuses on the commonest
|
|
1051
978
|
* config on any machine.
|
|
1052
979
|
*/
|
|
@@ -1181,8 +1108,7 @@ export function settingsIntegrityCheck(p, managed, expect = 'present') {
|
|
|
1181
1108
|
for (const [event, group] of presentGroups) {
|
|
1182
1109
|
const key = `${formatRepr(event)}\u0000${jsonDumpsCompact(group, { sortKeys: true })}`;
|
|
1183
1110
|
if (recordedSet.has(key)) continue;
|
|
1184
|
-
|
|
1185
|
-
const cmds = [...(get(group, 'hooks') || []).filter(isDict), group]
|
|
1111
|
+
const cmds = (get(group, 'hooks') || []).filter(isDict)
|
|
1186
1112
|
.map((h) => (has(h, 'command') ? h.command : ''));
|
|
1187
1113
|
if (cmds.some((c) => typeof c === 'string'
|
|
1188
1114
|
&& GENESEED_HOOK_SNIFF.some((mk) => c.includes(mk)))) {
|
|
@@ -22,7 +22,9 @@ import { readText } from '../lib/fs.mjs';
|
|
|
22
22
|
import { parseJson, formatRepr } from '../lib/json.mjs';
|
|
23
23
|
import { which } from '../lib/paths.mjs';
|
|
24
24
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
folderSkillProblems, registryProblems, secretProblems, vendorPinProblems,
|
|
27
|
+
} from './checks-repo.mjs';
|
|
26
28
|
import { LAW_CLASS, LAW_CLASSES, SKILL_CLASS } from './inventory.mjs';
|
|
27
29
|
import { NOTE, globSorted, has, isDir, isFile, rglob, srcStems } from './scan.mjs';
|
|
28
30
|
import { spawnSync } from 'node:child_process';
|
|
@@ -901,6 +903,7 @@ export function authoringProblems() {
|
|
|
901
903
|
problems.push(...registryProblems());
|
|
902
904
|
problems.push(...secretProblems());
|
|
903
905
|
problems.push(...vendorPinProblems());
|
|
906
|
+
problems.push(...folderSkillProblems());
|
|
904
907
|
problems.push(...constitutionProblems());
|
|
905
908
|
problems.push(...leanBlockProblems());
|
|
906
909
|
problems.push(...countTableProblems());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The checks over the REPO's own records — the registry, committed secrets, the hook shim
|
|
3
|
-
* and the vendored
|
|
3
|
+
* and the vendored skill folders (their pins and their SKILL.md frontmatter).
|
|
4
4
|
*
|
|
5
5
|
* Same contract as `checks-build.mjs`: an array of problem strings, empty when clean, one
|
|
6
6
|
* planted fault per check in the gate. This is the group that reads what the repo SAYS about
|
|
@@ -240,6 +240,50 @@ export function vendorPinProblems() {
|
|
|
240
240
|
return problems;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Every vendored skill folder's `SKILL.md` opens with a frontmatter whose `name` IS the
|
|
247
|
+
* folder and which carries a `description`.
|
|
248
|
+
*
|
|
249
|
+
* A flat skill gets that frontmatter from `writeNativeLayer`; a folder is copied through as
|
|
250
|
+
* written, so its own SKILL.md is all a host has to register it from. Two shipped broken:
|
|
251
|
+
* daydream had no frontmatter at all and react-view-transitions named itself after its
|
|
252
|
+
* upstream repo — the Agent Skills format requires the name to match the folder. Claude Code
|
|
253
|
+
* tolerated both, so nothing showed; a stricter host is free not to register them. Only the
|
|
254
|
+
* two keys every host reads are checked, at the top level of the block (an indented
|
|
255
|
+
* `name:` under `metadata:` does not count).
|
|
256
|
+
*/
|
|
257
|
+
export function folderSkillProblems() {
|
|
258
|
+
const problems = [];
|
|
259
|
+
for (const name of VENDORED_SKILL_DIRS) {
|
|
260
|
+
const folder = path.join(SRC, 'skills', name);
|
|
261
|
+
if (!isDir(folder)) continue; // vendorPinProblems reports the missing folder
|
|
262
|
+
let text;
|
|
263
|
+
try { text = readText(path.join(folder, 'SKILL.md')); } catch {
|
|
264
|
+
problems.push(`[authoring] skills/${name}/ has no SKILL.md — no host can load it`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const fm = FRONTMATTER_RE.exec(text);
|
|
268
|
+
if (!fm) {
|
|
269
|
+
problems.push(`[authoring] skills/${name}/SKILL.md opens with no frontmatter — a host `
|
|
270
|
+
+ 'registers a skill from its name and description');
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const declared = /^name:[ \t]*["']?([^"'\r\n]*?)["']?[ \t]*$/m.exec(fm[1]);
|
|
274
|
+
if (!declared) {
|
|
275
|
+
problems.push(`[authoring] skills/${name}/SKILL.md frontmatter has no 'name'`);
|
|
276
|
+
} else if (declared[1] !== name) {
|
|
277
|
+
problems.push(`[authoring] skills/${name}/SKILL.md is named '${declared[1]}' — the name `
|
|
278
|
+
+ 'must match the folder');
|
|
279
|
+
}
|
|
280
|
+
if (!/^description:[ \t]*\S/m.test(fm[1])) {
|
|
281
|
+
problems.push(`[authoring] skills/${name}/SKILL.md frontmatter has no 'description'`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return problems;
|
|
285
|
+
}
|
|
286
|
+
|
|
243
287
|
|
|
244
288
|
/**
|
|
245
289
|
* `js/README.md` describes every module under `js/`, and only modules that exist.
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -41,7 +41,9 @@ import os from 'node:os';
|
|
|
41
41
|
import path from 'node:path';
|
|
42
42
|
|
|
43
43
|
import { emitGlobalInto } from '../build/driver.mjs';
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
46
|
+
} from '../hosts/hosts.mjs';
|
|
45
47
|
import {
|
|
46
48
|
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, footprintOfDir, modeOfDir, postureOfDir,
|
|
47
49
|
readJsonMaybe, readMaybe, themeOfDir,
|
|
@@ -174,6 +176,12 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
174
176
|
// makes explicit rather than leaving to `makeCfg`'s parameter default two layers down.
|
|
175
177
|
posture: postureOfDir(dir), mode: modeOfDir(dir), doctrines: doctrinesForBuild(dir),
|
|
176
178
|
}));
|
|
179
|
+
// The one place a render depends on WHERE it is installed: a global Bob emit writes each
|
|
180
|
+
// folder skill's absolute directory into its SKILL.md (see `writeNativeLayer`), so the
|
|
181
|
+
// reference render names the temp tree. Mapped back to the deployed dir before comparing,
|
|
182
|
+
// or every such skill reads as edited. A deployment that has since MOVED still differs,
|
|
183
|
+
// and that is real drift — its skills point at a directory that is no longer theirs.
|
|
184
|
+
const deployedDir = resolvePath(dir);
|
|
177
185
|
const rels = [...new Set([...ownedSet(dir), ...ownedSet(expected)])].sort();
|
|
178
186
|
for (const rel of rels) {
|
|
179
187
|
const a = path.join(dir, rel);
|
|
@@ -182,7 +190,7 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
182
190
|
// `read_text(errors="replace")` — Node's utf8 decoder substitutes U+FFFD for the
|
|
183
191
|
// same bytes, and `readText` folds the line endings the way `Path.read_text` does.
|
|
184
192
|
const ta = readText(a);
|
|
185
|
-
const tb = readText(b);
|
|
193
|
+
const tb = readText(b).split(expected).join(deployedDir);
|
|
186
194
|
if (cmpKey(rel, ta) !== cmpKey(rel, tb)) {
|
|
187
195
|
const diff = unifiedDiff(splitLines(tb), splitLines(ta), {
|
|
188
196
|
fromfile: `source/${rel}`, tofile: `deployed/${rel}`, lineterm: '',
|
|
@@ -349,7 +357,7 @@ export function cmdDiff(args) {
|
|
|
349
357
|
+ 'installs against a fresh render (a project layer is diffed against '
|
|
350
358
|
+ 'its own repo, which this command does not do). Pass --target at a '
|
|
351
359
|
+ 'global config dir (opencode-global/claude-global/bob-global/'
|
|
352
|
-
+ '
|
|
360
|
+
+ 'openclaude-global), or omit '
|
|
353
361
|
+ '--target to use the default.\n');
|
|
354
362
|
} else {
|
|
355
363
|
printErr(`[diff] no global Geneseed install at ${target} (no ${GLOBAL_MANIFEST}). `
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -134,7 +134,7 @@ export function globalEmitProblems(themeName) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
* `_harness_build._claude_bob_emit_problems` — validate the claude/bob/
|
|
137
|
+
* `_harness_build._claude_bob_emit_problems` — validate the claude/bob/openclaude PER-REPO
|
|
138
138
|
* emits, which were never checked before.
|
|
139
139
|
*
|
|
140
140
|
* That is exactly why the CLAUDE.md/AGENTS.md skill-table dead links shipped unnoticed: the
|
|
@@ -143,7 +143,7 @@ export function globalEmitProblems(themeName) {
|
|
|
143
143
|
* skills one level deeper than a bundle does.
|
|
144
144
|
*/
|
|
145
145
|
export function claudeBobEmitProblems(themeName) {
|
|
146
|
-
return emitProblemsOver(['claude', 'bob', '
|
|
146
|
+
return emitProblemsOver(['claude', 'bob', 'openclaude'], (tmp, label) => {
|
|
147
147
|
const root = path.join(tmp, 'root');
|
|
148
148
|
return {
|
|
149
149
|
label: `${themeName} ${label}`,
|
|
@@ -211,7 +211,7 @@ export function doctorCollect({
|
|
|
211
211
|
problems = problems.concat(ran('global', `Global install (${themeName})`,
|
|
212
212
|
globalEmitProblems(themeName)));
|
|
213
213
|
problems = problems.concat(ran('claude_bob',
|
|
214
|
-
`Claude/Bob/
|
|
214
|
+
`Claude/Bob/OpenClaude per-repo emit (${themeName})`, claudeBobEmitProblems(themeName)));
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
problems = problems.concat(ran('parity', 'Theme parity', themeParityProblems()));
|
package/js/inspect/excludes.mjs
CHANGED
|
@@ -143,10 +143,6 @@ export function excludeAdd(target) {
|
|
|
143
143
|
wired.bob_rules_stub = true;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
if (host === 'copilot') {
|
|
147
|
-
messages.push('copilot: no native suppression exists — the global '
|
|
148
|
-
+ 'copilot-instructions.md still loads there (documented limitation)');
|
|
149
|
-
}
|
|
150
146
|
const entries = data.excludes.filter((e) => !(isDict(e) && same(get(e, 'path'), repo)));
|
|
151
147
|
entries.push({ path: repo.split(path.sep).join('/'), wired });
|
|
152
148
|
data.excludes = entries;
|
package/js/inspect/status.mjs
CHANGED
|
@@ -61,7 +61,7 @@ import { THEMES, ROOT, makeCfg } from '../build/source.mjs';
|
|
|
61
61
|
import { tuiInventory } from './inventory.mjs';
|
|
62
62
|
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
63
63
|
import {
|
|
64
|
-
claudeConfigDir, bobConfigDir,
|
|
64
|
+
claudeConfigDir, bobConfigDir, openclaudeConfigDir, opencodeConfigDir,
|
|
65
65
|
resolvePath, resolveMemoryDir, sovereignBypass, GATE_LEDGER,
|
|
66
66
|
} from '../hosts/hosts.mjs';
|
|
67
67
|
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
@@ -99,9 +99,9 @@ export function cmdVersion(args) {
|
|
|
99
99
|
let target = args.target ? resolvePath(args.target) : opencodeConfigDir();
|
|
100
100
|
let installed = readVersion(target);
|
|
101
101
|
if (installed === null) {
|
|
102
|
-
// A claude/bob/
|
|
102
|
+
// A claude/bob/openclaude-only machine must not report "no install detected". The bundle
|
|
103
103
|
// candidates are cwd-relative only — see the Python for why `ROOT / "Harness"` left.
|
|
104
|
-
for (const base of [claudeConfigDir(), bobConfigDir(),
|
|
104
|
+
for (const base of [claudeConfigDir(), bobConfigDir(), openclaudeConfigDir(),
|
|
105
105
|
path.join(process.cwd(), 'Harness'), process.cwd()]) {
|
|
106
106
|
const v = readVersion(base);
|
|
107
107
|
if (v) { installed = v; target = base; break; }
|
|
@@ -226,7 +226,7 @@ export function statusData() {
|
|
|
226
226
|
let installedFp = null;
|
|
227
227
|
let verTarget = null;
|
|
228
228
|
const otherCfg = [];
|
|
229
|
-
for (const fn of [claudeConfigDir, bobConfigDir,
|
|
229
|
+
for (const fn of [claudeConfigDir, bobConfigDir, openclaudeConfigDir]) {
|
|
230
230
|
try { otherCfg.push(fn()); } catch { /* a missing host dir must not sink status */ }
|
|
231
231
|
}
|
|
232
232
|
// Same walk as `cmdVersion`'s fallback, and cwd-relative for the same reason.
|
package/js/maintain/migrate.mjs
CHANGED
|
@@ -52,11 +52,11 @@ const KNOWN_EMITS = new Set(EMIT_HOST_SCOPE.keys());
|
|
|
52
52
|
/**
|
|
53
53
|
* The settings file a host wires hooks into, or null for a host that has none.
|
|
54
54
|
*
|
|
55
|
-
* Only Claude
|
|
56
|
-
* emits JS that runs in the host's own process and names neither the
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
55
|
+
* Only the Claude-style hosts carry hook commands. OpenCode's hooks are a PLUGIN
|
|
56
|
+
* (`js/hosts/opencode.mjs` emits JS that runs in the host's own process and names neither the
|
|
57
|
+
* shim nor `harness.py`), so it classifies as `none` and its whole migration is the re-emit —
|
|
58
|
+
* the same re-emit, in the same change, on every host, which is what the project's
|
|
59
|
+
* host-parity rule asks for.
|
|
60
60
|
*/
|
|
61
61
|
function hookSettingsFile(root, host, scope) {
|
|
62
62
|
if (!['claude', 'bob', 'openclaude'].includes(host)) return null;
|
package/js/maintain/setup.mjs
CHANGED
|
@@ -280,7 +280,7 @@ export function doctrineOptions() {
|
|
|
280
280
|
/**
|
|
281
281
|
* `_harness_setup.EMIT_OPTIONS`.
|
|
282
282
|
*
|
|
283
|
-
* Exported since P6b: `/api/themes` returns the same
|
|
283
|
+
* Exported since P6b: `/api/themes` returns the same nine as its `emits` list, and a copy
|
|
284
284
|
* of a table under test silently stops being the table under test.
|
|
285
285
|
*/
|
|
286
286
|
export const EMIT_OPTIONS = [
|
|
@@ -290,8 +290,6 @@ export const EMIT_OPTIONS = [
|
|
|
290
290
|
['claude', 'Per-repo CLAUDE.md + .claude/ committed into one repository.'],
|
|
291
291
|
['bob-global', 'IBM Bob global config dir (~/.bob) — rules/geneseed.md, agents, skills, settings.json.'],
|
|
292
292
|
['bob', 'Per-repo AGENTS.md + .bob/ for IBM Bob, committed into one repository.'],
|
|
293
|
-
['copilot-global', 'GitHub Copilot personal config dir (~/.copilot) — copilot-instructions.md, agents, skills.'],
|
|
294
|
-
['copilot', 'Per-repo AGENTS.md + .github/ for GitHub Copilot, committed into one repository.'],
|
|
295
293
|
['files', 'Plain bundle for any AGENT.md tool.'],
|
|
296
294
|
// Appended AFTER `files`, not beside the other hosts: the menu is answered by number, and a
|
|
297
295
|
// scripted `setup` that pipes its answers must keep meaning what it meant.
|
|
@@ -377,10 +375,10 @@ export function collectSetupLines() {
|
|
|
377
375
|
inst.footprint || 'lean');
|
|
378
376
|
let out = null;
|
|
379
377
|
let root = null;
|
|
380
|
-
// Every PROJECT emit needs the repo root — claude/bob/
|
|
378
|
+
// Every PROJECT emit needs the repo root — claude/bob/openclaude included: without `--out`
|
|
381
379
|
// their CLAUDE.md/.claude land in the generator's default ./Harness, where the host never
|
|
382
380
|
// looks.
|
|
383
|
-
if (['opencode', 'claude', 'bob', '
|
|
381
|
+
if (['opencode', 'claude', 'bob', 'openclaude'].includes(emit)) {
|
|
384
382
|
root = ask('Repo root to install into', '.');
|
|
385
383
|
out = root;
|
|
386
384
|
} else if (emit === 'files') {
|
|
@@ -59,10 +59,9 @@ import {
|
|
|
59
59
|
} from '../hosts/hosts.mjs';
|
|
60
60
|
import { mcpCommented } from '../hosts/mcp.mjs';
|
|
61
61
|
import {
|
|
62
|
-
|
|
62
|
+
managedBlockRead, managedBlockRemove, managedBlockWrite,
|
|
63
63
|
mergeClaudeSettings, opencodeTarget, readJsonc,
|
|
64
64
|
settingsIntegrityCheck, wireClaudeExcludes, unwireClaudeExcludes, unwireClaudeSettings,
|
|
65
|
-
unwireCopilotSettings,
|
|
66
65
|
} from '../hosts/settings.mjs';
|
|
67
66
|
import { printOut, printErr, readText, writeText, isFile, isDir, isOsError } from '../lib/fs.mjs';
|
|
68
67
|
import { indexOfDeepEqual, jsonDumps, jsonDumpsIndent, deepEquals } from '../lib/json.mjs';
|
|
@@ -71,7 +70,7 @@ import { comparePaths, isAbsolutePath, within } from '../lib/paths.mjs';
|
|
|
71
70
|
const hostSpec = (host) => HOSTS.find((h) => h.host === host);
|
|
72
71
|
|
|
73
72
|
/** The Claude-STYLE hosts — one manifest shape, one reversal. Spelled once. */
|
|
74
|
-
const CLAUDE_STYLE = ['claude', 'bob', '
|
|
73
|
+
const CLAUDE_STYLE = ['claude', 'bob', 'openclaude'];
|
|
75
74
|
|
|
76
75
|
/**
|
|
77
76
|
* `shutil.rmtree(p, ignore_errors=True)`.
|
|
@@ -295,12 +294,8 @@ export function claudeUninstall(cfg, archiveMemory) {
|
|
|
295
294
|
if (failed.length) warnSurvivors(failed);
|
|
296
295
|
const hooks = managed.settings_hooks || [];
|
|
297
296
|
const sf = settingsFile(cfg, managed);
|
|
298
|
-
|
|
299
|
-
// and never `settings_hooks`, so exactly one of these does anything.
|
|
300
|
-
const unwired = unwireClaudeSettings(sf, hooks)
|
|
301
|
-
| unwireCopilotSettings(sf, managed.copilot_hooks || []);
|
|
297
|
+
const unwired = unwireClaudeSettings(sf, hooks);
|
|
302
298
|
unwireClaudeExcludes(sf, managed.settings_excludes || []);
|
|
303
|
-
copilotIntegrityCheck(sf, managed.copilot_hooks || [], 'absent');
|
|
304
299
|
// The unwire is VERIFIED, not assumed: a commented settings file is never rewritten, so a
|
|
305
300
|
// supposedly-uninstalled repo could keep firing Geneseed's hooks. Loud, never fatal.
|
|
306
301
|
settingsIntegrityCheck(sf, managed, 'absent');
|
|
@@ -320,7 +315,7 @@ export function claudeUninstall(cfg, archiveMemory) {
|
|
|
320
315
|
/**
|
|
321
316
|
* `_harness_mcp._uninstall_global` — reverse a global install at `target` via its manifest.
|
|
322
317
|
*
|
|
323
|
-
* Host-aware at the top, and that dispatch is the whole reason a Claude/Bob/
|
|
318
|
+
* Host-aware at the top, and that dispatch is the whole reason a Claude/Bob/OpenClaude global
|
|
324
319
|
* install and an OpenCode one can share one entry point: the Claude family has no
|
|
325
320
|
* opencode.json to unmerge and a settings.json plus a managed block instead.
|
|
326
321
|
*/
|
|
@@ -681,7 +676,7 @@ export function cmdUninstall(args) {
|
|
|
681
676
|
const targetDesc = resolvePath(expanduser(args.target));
|
|
682
677
|
printErr(`[uninstall] no Geneseed install detected at ${targetDesc}.\n`
|
|
683
678
|
+ '[uninstall] pass --target <repo> for a project install (.opencode/.claude/'
|
|
684
|
-
+ '.bob/.
|
|
679
|
+
+ '.bob/.openclaude) or --target <config dir> for a global one.\n');
|
|
685
680
|
return 1;
|
|
686
681
|
}
|
|
687
682
|
const [host, scope, root] = hit;
|
|
@@ -695,12 +690,7 @@ export function cmdUninstall(args) {
|
|
|
695
690
|
const data = installDataDir(root, host, scope);
|
|
696
691
|
const stores = ['memory', 'notebook'].filter((n) => isDir(path.join(data, n)));
|
|
697
692
|
printOut(`[uninstall] target: ${root} (${host}:${scope})\n`);
|
|
698
|
-
if (
|
|
699
|
-
printOut('[uninstall] removes: agents/, skills/, markers, the '
|
|
700
|
-
+ `${hostSpec(host).agentFile} managed block, and Geneseed's `
|
|
701
|
-
+ '~/.copilot/settings.json hooks on a global install (your own keys/hooks and '
|
|
702
|
-
+ '.github files are kept).\n');
|
|
703
|
-
} else if (['claude', 'bob', 'openclaude'].includes(host)) {
|
|
693
|
+
if (['claude', 'bob', 'openclaude'].includes(host)) {
|
|
704
694
|
printOut('[uninstall] removes: agents/, skills/, markers, the '
|
|
705
695
|
+ `${hostSpec(host).agentFile} managed block, and Geneseed's `
|
|
706
696
|
+ 'settings.json hooks/excludes (your own keys/hooks are kept).\n');
|
|
@@ -1024,7 +1014,7 @@ export function installReactivate(root, host = 'opencode', scope = 'global') {
|
|
|
1024
1014
|
return { ok: true, kind, moved };
|
|
1025
1015
|
}
|
|
1026
1016
|
|
|
1027
|
-
// ---- the Claude-style fork (Claude · IBM Bob ·
|
|
1017
|
+
// ---- the Claude-style fork (Claude · IBM Bob · OpenClaude) ---------------------------
|
|
1028
1018
|
//
|
|
1029
1019
|
// A Claude-style install has NO `instructions` array — the harness reaches the host through
|
|
1030
1020
|
// the CLAUDE.md managed block and the settings.json hooks. So the reversal is three moving
|
|
@@ -1057,27 +1047,6 @@ function cleanHostStash(cfg, host = 'claude') {
|
|
|
1057
1047
|
function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
|
|
1058
1048
|
const data = claudeReadManifest(cfg);
|
|
1059
1049
|
const managed = managedOf(data);
|
|
1060
|
-
// A LEGACY Copilot install (single-slot `copilot_hooks`, the dead `toolCall` event):
|
|
1061
|
-
// reactivating it re-wires the CURRENT Copilot shape — event → array, recorded under
|
|
1062
|
-
// `settings_hooks` — rather than restoring hooks Copilot no longer reads. None of the
|
|
1063
|
-
// pack/exclude axes below apply: the Copilot gate is not pack-conditional.
|
|
1064
|
-
if (Array.isArray(managed.copilot_hooks)) {
|
|
1065
|
-
const sf = settingsFile(cfg, managed);
|
|
1066
|
-
unwireCopilotSettings(sf, managed.copilot_hooks);
|
|
1067
|
-
const [, claims] = mergeClaudeSettings(sf, 'global', null, hookRunnerEntry(), null, [],
|
|
1068
|
-
'copilot', cfg);
|
|
1069
|
-
if (Object.keys(data).length > 0) {
|
|
1070
|
-
delete managed.copilot_hooks;
|
|
1071
|
-
managed.settings_hooks = claims;
|
|
1072
|
-
data.managed = managed;
|
|
1073
|
-
const tmp = path.join(cfg, `${GLOBAL_MANIFEST}.tmp`);
|
|
1074
|
-
try {
|
|
1075
|
-
writeText(tmp, `${jsonDumpsIndent(data)}\n`);
|
|
1076
|
-
renameSync(tmp, path.join(cfg, GLOBAL_MANIFEST));
|
|
1077
|
-
} catch { /* except OSError: pass */ }
|
|
1078
|
-
}
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
1050
|
// `hookOpts` is the one argument `mergeClaudeSettings` will not default, and it is right
|
|
1082
1051
|
// not to: there is no computable fallback for the runner/entry the shim bakes. The
|
|
1083
1052
|
// emitter's own originator is imported rather than restated — a reactivate that wired a
|
|
@@ -1090,8 +1059,7 @@ function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
|
|
|
1090
1059
|
// PROJECT install `cfg` is `<repo>/.claude` and the carrier is `<repo>/CLAUDE.md`, so
|
|
1091
1060
|
// reading `cfg` alone answered `null` for every project reactivate and re-wired the gate
|
|
1092
1061
|
// unconditionally (fail-closed, so not a hole — but wrong, and it made the toggle one-way
|
|
1093
|
-
// for project installs). `root` first, `cfg` second:
|
|
1094
|
-
// config dir (`.github/copilot-instructions.md`), and on a global install the two are the
|
|
1062
|
+
// for project installs). `root` first, `cfg` second: on a global install the two are the
|
|
1095
1063
|
// same directory anyway. Both silent ⇒ `null` ⇒ the gate stays.
|
|
1096
1064
|
const doctrines = doctrinesOfDir(root) ?? doctrinesOfDir(cfg);
|
|
1097
1065
|
// Same two-carrier read for the second axis, and its own default: an absent line means
|
|
@@ -1134,12 +1102,10 @@ function claudeDeactivate(root, scope = 'global', host = 'claude') {
|
|
|
1134
1102
|
return { ok: false, failed, rolled_back: done.length };
|
|
1135
1103
|
}
|
|
1136
1104
|
unwireClaudeSettings(settingsFile(cfg, managed), managed.settings_hooks || []);
|
|
1137
|
-
unwireCopilotSettings(settingsFile(cfg, managed), managed.copilot_hooks || []);
|
|
1138
1105
|
unwireClaudeExcludes(settingsFile(cfg, managed), managed.settings_excludes || []);
|
|
1139
1106
|
// The same integrity check the uninstall path runs: a deactivate that silently failed to
|
|
1140
1107
|
// unwire would leave hooks firing in a repo the user believes is off.
|
|
1141
1108
|
settingsIntegrityCheck(settingsFile(cfg, managed), managed, 'absent');
|
|
1142
|
-
copilotIntegrityCheck(settingsFile(cfg, managed), managed.copilot_hooks || [], 'absent');
|
|
1143
1109
|
const cm = claudeMdPath(cfg, managed);
|
|
1144
1110
|
const block = managedBlockRead(cm);
|
|
1145
1111
|
mkdirSync(stash, { recursive: true }); // presence == disabled, even if nothing moved
|