@uniqbit/mate-core 0.15.4-canary.8 → 0.15.4
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/claude-plugin/hooks/artifact-finish-nudge.mjs +5 -3
- package/claude-plugin/hooks/session-banner.mjs +5 -3
- package/claude-plugin/hooks/ts-loader.mjs +28 -0
- package/claude-plugin/hooks/validate-artifact-path.mjs +5 -3
- package/package.json +1 -1
- package/src/cli/commands/cap/openspec.ts +20 -1
- package/src/cli/commands/companion/hub.ts +1 -0
- package/src/cli/commands/plugin/install.ts +15 -3
- package/src/hooks/artifact-finish-nudge.ts +35 -3
- package/src/lib/context-mode-package.ts +5 -3
- package/src/lib/orchestrator/adapters/opencode.ts +10 -60
- package/src/lib/orchestrator/companion-git-sync.ts +67 -8
- package/src/lib/orchestrator/companion-hub.ts +28 -6
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/package-paths.ts +1 -0
- package/src/opencode/companion-hooks.ts +33 -12
- package/src/playbooks/companion-guidance.ts +1 -1
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-openspec-backfill/SKILL.md +65 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +1 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3612 -0
- package/src/tools/setup/capabilities/context-mode.ts +57 -83
- package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
- package/src/tools/setup/capabilities/graphify.ts +86 -295
- package/src/tools/setup/capabilities/openspec.ts +34 -1
- package/src/tools/setup/capabilities/react-doctor.ts +37 -37
- package/src/tools/setup/capabilities/rtk.ts +4 -1
- package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
- package/src/tools/setup/capabilities/tokensave.ts +51 -72
- package/src/tools/setup/context-services.ts +29 -0
- package/src/tools/setup/dynamic-plugins/hydrate.ts +15 -1
- package/src/tools/setup/engine.ts +68 -1
- package/src/tools/setup/mate.ts +10 -2
- package/src/tools/setup/plugin.ts +105 -0
- package/src/tools/setup/plugins/gitignore.ts +7 -4
- package/src/tools/setup/plugins/guidance.ts +1 -1
- package/src/tools/setup/providers/agent-file-sections.ts +78 -0
- package/src/tools/setup/providers/claude-format.ts +159 -0
- package/src/tools/setup/providers/claude.ts +281 -227
- package/src/tools/setup/providers/opencode-format.ts +146 -0
- package/src/tools/setup/providers/opencode.ts +209 -60
- package/src/tools/setup/providers/skill-tree.ts +38 -0
- package/src/tools/setup.ts +7 -56
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import type { CapabilityPlugin, SetupContext } from "../plugin";
|
|
4
|
+
import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
|
|
5
5
|
import type { InstallRequirement } from "../install-contract";
|
|
6
6
|
import {
|
|
7
7
|
isCommandOnPath,
|
|
@@ -11,7 +11,23 @@ import {
|
|
|
11
11
|
} from "../utils";
|
|
12
12
|
import { confirm } from "../../../cli/confirm";
|
|
13
13
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
14
|
+
import { getWrapperBinPath } from "../../../lib/package-paths";
|
|
14
15
|
import { isInstalledViaUvTool } from "../package-managers/uv";
|
|
16
|
+
import { stripSectionFromFile } from "../providers/agent-file-sections";
|
|
17
|
+
import { graphifySectionOptions } from "./graphify-shared";
|
|
18
|
+
export { removeGraphifySection } from "./graphify-shared";
|
|
19
|
+
import {
|
|
20
|
+
mergeClaudeSettingsJsonHooks,
|
|
21
|
+
patchClaudeSkillTree,
|
|
22
|
+
removeClaudeHookGroupsWhere,
|
|
23
|
+
stripClaudeForeignSections,
|
|
24
|
+
} from "../providers/claude";
|
|
25
|
+
import type { ClaudeHookGroup } from "../providers/claude-format";
|
|
26
|
+
import {
|
|
27
|
+
patchOpenCodeSkillTree,
|
|
28
|
+
removeOpenCodeForeignPluginReferences,
|
|
29
|
+
stripOpenCodeForeignSections,
|
|
30
|
+
} from "../providers/opencode";
|
|
15
31
|
|
|
16
32
|
// Storage contract: <companionPath>/.graphify/<repositoryId>/graphify-out/
|
|
17
33
|
export const GRAPHIFY_STORE_SEGMENT = ".graphify";
|
|
@@ -43,31 +59,8 @@ const GRAPHIFY_PROVIDER_DIRS: Record<string, string> = {
|
|
|
43
59
|
export const GRAPHIFY_COMPANION_OUT_PREFIX =
|
|
44
60
|
"$MATE_ARTIFACT_PATH/.graphify/$MATE_REPO_ID/graphify-out/";
|
|
45
61
|
|
|
46
|
-
// Marker names derive from the framework identity, never the invocation name.
|
|
47
|
-
const GRAPHIFY_START = () => `<!-- ${FRAMEWORK_NAME.toUpperCase()}:GRAPHIFY:START -->`;
|
|
48
|
-
const GRAPHIFY_END = () => `<!-- ${FRAMEWORK_NAME.toUpperCase()}:GRAPHIFY:END -->`;
|
|
49
|
-
|
|
50
|
-
// Per-provider root agent instruction files used for graphify cleanup on teardown.
|
|
51
|
-
const GRAPHIFY_AGENT_FILES: Record<string, string> = {
|
|
52
|
-
claude: "CLAUDE.md",
|
|
53
|
-
opencode: "AGENTS.md",
|
|
54
|
-
};
|
|
55
|
-
|
|
56
62
|
export const GRAPHIFY_SUPPORTED_PROVIDERS = Object.keys(GRAPHIFY_PROVIDER_DIRS);
|
|
57
63
|
|
|
58
|
-
interface ClaudeHookCommand {
|
|
59
|
-
type?: string;
|
|
60
|
-
command?: string;
|
|
61
|
-
args?: string[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface ClaudeHookGroup {
|
|
65
|
-
matcher?: string;
|
|
66
|
-
hooks?: ClaudeHookCommand[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
type ClaudeHookMap = Record<string, ClaudeHookGroup[]>;
|
|
70
|
-
|
|
71
64
|
const CLAUDE_MANAGED_HOOK_COMMAND_SUFFIXES = new Set([
|
|
72
65
|
"/.claude/hooks/validate-artifact-path",
|
|
73
66
|
"/.claude/hooks/mate-session-banner",
|
|
@@ -79,29 +72,6 @@ export function deriveGraphifyProviders(activeProviders: string[]): string[] {
|
|
|
79
72
|
return GRAPHIFY_SUPPORTED_PROVIDERS.filter((p) => activeProviders.includes(p));
|
|
80
73
|
}
|
|
81
74
|
|
|
82
|
-
function isGraphifyHeading(line: string): boolean {
|
|
83
|
-
return /^#{1,6}\s+graphify\s*$/i.test(line);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function stripGraphifyMarkers(lines: string[]): string[] {
|
|
87
|
-
return lines.filter((line) => line !== GRAPHIFY_START() && line !== GRAPHIFY_END());
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function findGraphifySectionBounds(lines: string[]): { start: number; end: number } | null {
|
|
91
|
-
const start = lines.findIndex(isGraphifyHeading);
|
|
92
|
-
if (start === -1) return null;
|
|
93
|
-
|
|
94
|
-
let end = lines.length;
|
|
95
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
96
|
-
if (lines[i].startsWith("## ") || lines[i].startsWith("# ")) {
|
|
97
|
-
end = i;
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { start, end };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
75
|
// graphifyy's `graphify install` emits a skill whose pipeline writes to a
|
|
106
76
|
// cwd-relative graphify-out/ using literal path strings (inline Python + bash)
|
|
107
77
|
// that graphifyy's own GRAPHIFY_OUT/paths.py override cannot redirect. Rewrite
|
|
@@ -133,99 +103,7 @@ export function rewriteGraphifySkillOutputPaths(content: string): string {
|
|
|
133
103
|
// Reference files whose graphify-out/ paths are intentionally cwd-relative or
|
|
134
104
|
// point at external/cloned repos (e.g. multi-repo merge) and must NOT be
|
|
135
105
|
// rewritten to the companion store.
|
|
136
|
-
const GRAPHIFY_SKILL_REWRITE_EXCLUDE =
|
|
137
|
-
|
|
138
|
-
async function patchGraphifySkillFile(skillPath: string): Promise<void> {
|
|
139
|
-
let content: string;
|
|
140
|
-
try {
|
|
141
|
-
content = await fs.readFile(skillPath, "utf8");
|
|
142
|
-
} catch {
|
|
143
|
-
return; // file absent (graphify binary not installed) — nothing to patch
|
|
144
|
-
}
|
|
145
|
-
const rewritten = rewriteGraphifySkillOutputPaths(content);
|
|
146
|
-
if (rewritten !== content) await fs.writeFile(skillPath, rewritten, "utf8");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Rewrite output paths across the installed skill: SKILL.md plus every
|
|
150
|
-
// references/*.md (excluding GRAPHIFY_SKILL_REWRITE_EXCLUDE). Directory-driven
|
|
151
|
-
// so reference files graphifyy adds in future releases are covered automatically.
|
|
152
|
-
async function patchGraphifySkillTree(skillDir: string): Promise<void> {
|
|
153
|
-
await patchGraphifySkillFile(path.join(skillDir, "SKILL.md"));
|
|
154
|
-
|
|
155
|
-
const refsDir = path.join(skillDir, "references");
|
|
156
|
-
let entries: string[];
|
|
157
|
-
try {
|
|
158
|
-
entries = await fs.readdir(refsDir);
|
|
159
|
-
} catch {
|
|
160
|
-
return; // no references/ dir
|
|
161
|
-
}
|
|
162
|
-
const targets = entries.filter(
|
|
163
|
-
(name) => name.endsWith(".md") && !GRAPHIFY_SKILL_REWRITE_EXCLUDE.has(name),
|
|
164
|
-
);
|
|
165
|
-
await Promise.all(targets.map((name) => patchGraphifySkillFile(path.join(refsDir, name))));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Removes the graphify section from content, collapsing leftover blank lines.
|
|
169
|
-
export function removeGraphifySection(content: string): string {
|
|
170
|
-
const normalizedLines = stripGraphifyMarkers(content.split("\n"));
|
|
171
|
-
const bounds = findGraphifySectionBounds(normalizedLines);
|
|
172
|
-
if (!bounds) {
|
|
173
|
-
const strippedMarkersOnly = normalizedLines.join("\n");
|
|
174
|
-
return strippedMarkersOnly === content ? content : strippedMarkersOnly;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const remaining = [
|
|
178
|
-
...normalizedLines.slice(0, bounds.start),
|
|
179
|
-
...normalizedLines.slice(bounds.end),
|
|
180
|
-
].join("\n");
|
|
181
|
-
const collapsed = remaining.replace(/\n{3,}/g, "\n\n").trim();
|
|
182
|
-
return collapsed ? collapsed + "\n" : "";
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function stripAgentFile(filePath: string): Promise<void> {
|
|
186
|
-
let content: string;
|
|
187
|
-
try {
|
|
188
|
-
content = await fs.readFile(filePath, "utf8");
|
|
189
|
-
} catch {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const stripped = removeGraphifySection(content);
|
|
193
|
-
if (stripped === content) return;
|
|
194
|
-
if (!stripped.trim()) {
|
|
195
|
-
await fs.unlink(filePath);
|
|
196
|
-
} else {
|
|
197
|
-
await fs.writeFile(filePath, stripped, "utf8");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function stripLegacyClaudeGraphifyFile(companionPath: string): Promise<void> {
|
|
202
|
-
const legacyPath = path.join(companionPath, ".claude", "CLAUDE.md");
|
|
203
|
-
await stripAgentFile(legacyPath);
|
|
204
|
-
await pruneEmptyAncestors(path.join(companionPath, ".claude"), companionPath);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function hookEntryKey(entry: ClaudeHookGroup): string {
|
|
208
|
-
return JSON.stringify(entry);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function mergeClaudeHookGroups(
|
|
212
|
-
existingHooks: ClaudeHookMap,
|
|
213
|
-
incomingHooks: ClaudeHookMap,
|
|
214
|
-
): ClaudeHookMap {
|
|
215
|
-
const merged: ClaudeHookMap = { ...existingHooks };
|
|
216
|
-
for (const [event, entries] of Object.entries(incomingHooks)) {
|
|
217
|
-
const current = merged[event] ?? [];
|
|
218
|
-
const seen = new Set(current.map(hookEntryKey));
|
|
219
|
-
merged[event] = [...current];
|
|
220
|
-
for (const entry of entries ?? []) {
|
|
221
|
-
const key = hookEntryKey(entry);
|
|
222
|
-
if (seen.has(key)) continue;
|
|
223
|
-
merged[event].push(entry);
|
|
224
|
-
seen.add(key);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return merged;
|
|
228
|
-
}
|
|
106
|
+
const GRAPHIFY_SKILL_REWRITE_EXCLUDE = ["github-and-merge.md"];
|
|
229
107
|
|
|
230
108
|
function isMateManagedClaudeHookCommand(command: string): boolean {
|
|
231
109
|
for (const suffix of CLAUDE_MANAGED_HOOK_COMMAND_SUFFIXES) {
|
|
@@ -249,65 +127,8 @@ function isGraphifyClaudeHookGroup(entry: ClaudeHookGroup): boolean {
|
|
|
249
127
|
});
|
|
250
128
|
}
|
|
251
129
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const raw = await fs.readFile(settingsLocalPath, "utf8");
|
|
257
|
-
const settings = JSON.parse(raw) as { hooks?: ClaudeHookMap } & Record<string, unknown>;
|
|
258
|
-
const existingHooks = settings.hooks ?? {};
|
|
259
|
-
let changed = false;
|
|
260
|
-
|
|
261
|
-
for (const [event, existingEntries] of Object.entries(existingHooks)) {
|
|
262
|
-
if (!existingEntries?.length) continue;
|
|
263
|
-
const nextEntries = existingEntries.filter((entry) => !isGraphifyClaudeHookGroup(entry));
|
|
264
|
-
|
|
265
|
-
if (nextEntries.length !== existingEntries.length) {
|
|
266
|
-
changed = true;
|
|
267
|
-
if (nextEntries.length > 0) {
|
|
268
|
-
existingHooks[event] = nextEntries;
|
|
269
|
-
} else {
|
|
270
|
-
delete existingHooks[event];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (Object.keys(existingHooks).length === 0) {
|
|
276
|
-
if (Object.prototype.hasOwnProperty.call(settings, "hooks")) changed = true;
|
|
277
|
-
delete settings.hooks;
|
|
278
|
-
} else {
|
|
279
|
-
settings.hooks = existingHooks;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (changed) {
|
|
283
|
-
await fs.writeFile(settingsLocalPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
284
|
-
}
|
|
285
|
-
} catch {
|
|
286
|
-
/* settings absent or malformed */
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function removeOpenCodePlugin(filePath: string): Promise<void> {
|
|
291
|
-
let raw: string;
|
|
292
|
-
try {
|
|
293
|
-
raw = await fs.readFile(filePath, "utf8");
|
|
294
|
-
} catch {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
299
|
-
if (Array.isArray(parsed.plugin)) {
|
|
300
|
-
parsed.plugin = (parsed.plugin as string[]).filter((p) => !p.includes("graphify"));
|
|
301
|
-
if ((parsed.plugin as string[]).length === 0) delete parsed.plugin;
|
|
302
|
-
}
|
|
303
|
-
if (Object.keys(parsed).length === 0) {
|
|
304
|
-
await fs.unlink(filePath);
|
|
305
|
-
} else {
|
|
306
|
-
await fs.writeFile(filePath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
307
|
-
}
|
|
308
|
-
} catch {
|
|
309
|
-
// JSON parse error — leave file untouched
|
|
310
|
-
}
|
|
130
|
+
function isGraphifyPluginReference(entry: unknown): boolean {
|
|
131
|
+
return typeof entry === "string" && entry.includes("graphify");
|
|
311
132
|
}
|
|
312
133
|
|
|
313
134
|
type GraphifyRunCommand = typeof runShellCommand;
|
|
@@ -336,6 +157,22 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
336
157
|
"Enable Graphify code graph analysis with companion-managed wrappers for supported agent sessions.",
|
|
337
158
|
defaultSelected: false,
|
|
338
159
|
isEnabled: (config) => (config.capabilities ?? []).some((c) => c.name === "graphify"),
|
|
160
|
+
// Permission pre-seeds are declared and reconciled by the Claude Runtime
|
|
161
|
+
// Surface. The graphify skill trees and agent-file sections are written by
|
|
162
|
+
// the external `graphify install` CLI, so they are reconciled through the
|
|
163
|
+
// Runtime Surface escape hatch below instead of declarations.
|
|
164
|
+
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
165
|
+
return {
|
|
166
|
+
claude: {
|
|
167
|
+
permissionEntries: [
|
|
168
|
+
"Skill(graphify)",
|
|
169
|
+
"Bash(graphify:*)",
|
|
170
|
+
`Bash(${FRAMEWORK_NAME} cap graphify:*)`,
|
|
171
|
+
`Bash(${path.join(getWrapperBinPath(), "graphify")}:*)`,
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
},
|
|
339
176
|
gitignoreEntries: () => GRAPHIFY_GITIGNORE_ENTRIES,
|
|
340
177
|
persistGitignoreEntries: true,
|
|
341
178
|
getInstallRequirements: (): InstallRequirement[] => [
|
|
@@ -372,7 +209,10 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
372
209
|
},
|
|
373
210
|
|
|
374
211
|
async teardown(ctx: SetupContext) {
|
|
375
|
-
await
|
|
212
|
+
await stripSectionFromFile(
|
|
213
|
+
path.join(ctx.companionPath, "AGENTS.md"),
|
|
214
|
+
graphifySectionOptions(),
|
|
215
|
+
);
|
|
376
216
|
// Don't uninstall the global graphify binary — companion-managed files are
|
|
377
217
|
// cleaned up per-provider in forProvider[].teardown. Graph data under
|
|
378
218
|
// .graphify/ is retained as user data.
|
|
@@ -383,8 +223,6 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
383
223
|
providerId,
|
|
384
224
|
{
|
|
385
225
|
async apply(ctx: SetupContext) {
|
|
386
|
-
const providerDir = GRAPHIFY_PROVIDER_DIRS[providerId];
|
|
387
|
-
|
|
388
226
|
// Reconcile Graphify-managed provider assets from the companion root
|
|
389
227
|
const isInstalled =
|
|
390
228
|
checkPath("graphify", process.env.PATH ?? "") || checkUvTool("graphifyy");
|
|
@@ -395,93 +233,54 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
395
233
|
{ cwd: ctx.companionPath },
|
|
396
234
|
);
|
|
397
235
|
|
|
398
|
-
// graphify install writes a ## graphify section to the agent
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
await stripAgentFile(path.join(ctx.repoPath, agentFile));
|
|
412
|
-
}
|
|
236
|
+
// graphify install writes a ## graphify section to the agent
|
|
237
|
+
// files (companion root, per-provider dir, and — when syncing
|
|
238
|
+
// from a linked repo — the repo file). Strip them through the
|
|
239
|
+
// escape hatch since guidance is delivered via system prompt.
|
|
240
|
+
if (providerId === "claude") {
|
|
241
|
+
await stripClaudeForeignSections(ctx.companionPath, {
|
|
242
|
+
...graphifySectionOptions(),
|
|
243
|
+
repoPath: ctx.repoPath,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// graphify install writes .claude/settings.json with hook
|
|
247
|
+
// guards; absorb them into the Mate-owned settings document.
|
|
248
|
+
await mergeClaudeSettingsJsonHooks(ctx.companionPath);
|
|
413
249
|
}
|
|
414
250
|
|
|
415
|
-
// graphify install also writes "plugin": [".opencode/plugins/graphify.js"]
|
|
416
|
-
// to opencode.json. Because the config lives inside companionPath/.opencode/,
|
|
417
|
-
// the relative path resolves to companionPath/.opencode/.opencode/plugins/
|
|
418
|
-
// (double .opencode) — a broken path. Opencode auto-discovers plugins from
|
|
419
|
-
// plugins/ anyway, so strip the explicit entry to avoid duplicate loading.
|
|
420
251
|
if (providerId === "opencode") {
|
|
421
|
-
await
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
252
|
+
await stripOpenCodeForeignSections(ctx.companionPath, {
|
|
253
|
+
...graphifySectionOptions(),
|
|
254
|
+
repoPath: ctx.repoPath,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// graphify install also writes "plugin": [".opencode/plugins/graphify.js"]
|
|
258
|
+
// to opencode.json. Because the config lives inside companionPath/.opencode/,
|
|
259
|
+
// the relative path resolves to companionPath/.opencode/.opencode/plugins/
|
|
260
|
+
// (double .opencode) — a broken path. Opencode auto-discovers plugins from
|
|
261
|
+
// plugins/ anyway, so strip the explicit entry to avoid duplicate loading.
|
|
262
|
+
await removeOpenCodeForeignPluginReferences(
|
|
432
263
|
ctx.companionPath,
|
|
433
|
-
|
|
434
|
-
"settings.local.json",
|
|
264
|
+
isGraphifyPluginReference,
|
|
435
265
|
);
|
|
436
|
-
try {
|
|
437
|
-
const settingsJsonRaw = await fs.readFile(settingsJsonPath, "utf8");
|
|
438
|
-
const settingsJson = JSON.parse(settingsJsonRaw) as {
|
|
439
|
-
hooks?: ClaudeHookMap;
|
|
440
|
-
} & Record<string, unknown>;
|
|
441
|
-
|
|
442
|
-
const settingsLocalRaw = await fs.readFile(settingsLocalPath, "utf8");
|
|
443
|
-
const settingsLocal = JSON.parse(settingsLocalRaw) as {
|
|
444
|
-
hooks?: ClaudeHookMap;
|
|
445
|
-
} & Record<string, unknown>;
|
|
446
|
-
|
|
447
|
-
if (settingsJson.hooks && Object.keys(settingsJson.hooks).length > 0) {
|
|
448
|
-
settingsLocal.hooks = mergeClaudeHookGroups(
|
|
449
|
-
settingsLocal.hooks ?? {},
|
|
450
|
-
settingsJson.hooks,
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
await fs.writeFile(
|
|
455
|
-
settingsLocalPath,
|
|
456
|
-
JSON.stringify(settingsLocal, null, 2) + "\n",
|
|
457
|
-
"utf8",
|
|
458
|
-
);
|
|
459
|
-
} catch {
|
|
460
|
-
/* settings files absent or malformed — just remove settings.json */
|
|
461
|
-
}
|
|
462
|
-
try {
|
|
463
|
-
await fs.unlink(settingsJsonPath);
|
|
464
|
-
} catch {
|
|
465
|
-
/* not present */
|
|
466
|
-
}
|
|
467
266
|
}
|
|
468
267
|
}
|
|
469
268
|
|
|
470
269
|
// Redirect the freshly-installed skill's cwd-relative graphify-out/
|
|
471
270
|
// paths (SKILL.md + references) to the companion-local store. Runs
|
|
472
271
|
// every setup/sync so it survives graphifyy regenerating the skill.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
272
|
+
const patchSkillTree =
|
|
273
|
+
providerId === "claude" ? patchClaudeSkillTree : patchOpenCodeSkillTree;
|
|
274
|
+
await patchSkillTree(ctx.companionPath, "graphify", rewriteGraphifySkillOutputPaths, {
|
|
275
|
+
excludeFiles: GRAPHIFY_SKILL_REWRITE_EXCLUDE,
|
|
276
|
+
});
|
|
476
277
|
},
|
|
477
278
|
|
|
478
279
|
async teardown(ctx: SetupContext) {
|
|
479
280
|
const providerDir = GRAPHIFY_PROVIDER_DIRS[providerId];
|
|
480
281
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
// Remove Graphify-managed skills directory
|
|
282
|
+
// Remove the Graphify-managed skills directory (written by the
|
|
283
|
+
// external CLI, so removed here rather than via a declaration).
|
|
485
284
|
try {
|
|
486
285
|
await fs.rm(path.join(ctx.companionPath, providerDir, "skills", "graphify"), {
|
|
487
286
|
recursive: true,
|
|
@@ -495,23 +294,17 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
495
294
|
ctx.companionPath,
|
|
496
295
|
);
|
|
497
296
|
|
|
498
|
-
// Strip the ## graphify section
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
await stripAgentFile(path.join(ctx.companionPath, agentFile));
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Provider-specific cleanup for files beyond skills/
|
|
514
|
-
if (providerId === "opencode") {
|
|
297
|
+
// Strip the ## graphify section and provider-specific foreign
|
|
298
|
+
// config through the escape hatch. The shared AGENTS.md is kept
|
|
299
|
+
// while another active runtime still uses it.
|
|
300
|
+
if (providerId === "claude") {
|
|
301
|
+
await stripClaudeForeignSections(ctx.companionPath, graphifySectionOptions());
|
|
302
|
+
await removeClaudeHookGroupsWhere(ctx.companionPath, isGraphifyClaudeHookGroup);
|
|
303
|
+
} else if (providerId === "opencode") {
|
|
304
|
+
await stripOpenCodeForeignSections(ctx.companionPath, {
|
|
305
|
+
...graphifySectionOptions(),
|
|
306
|
+
guardSharedFile: { activeProviders: ctx.activeProviders },
|
|
307
|
+
});
|
|
515
308
|
try {
|
|
516
309
|
await fs.unlink(
|
|
517
310
|
path.join(ctx.companionPath, ".opencode", "plugins", "graphify.js"),
|
|
@@ -519,16 +312,14 @@ export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityP
|
|
|
519
312
|
} catch {
|
|
520
313
|
/* not present */
|
|
521
314
|
}
|
|
522
|
-
await
|
|
523
|
-
|
|
315
|
+
await removeOpenCodeForeignPluginReferences(
|
|
316
|
+
ctx.companionPath,
|
|
317
|
+
isGraphifyPluginReference,
|
|
524
318
|
);
|
|
525
319
|
await pruneEmptyAncestors(
|
|
526
320
|
path.join(ctx.companionPath, ".opencode", "plugins"),
|
|
527
321
|
ctx.companionPath,
|
|
528
322
|
);
|
|
529
|
-
} else if (providerId === "claude") {
|
|
530
|
-
await removeGraphifyClaudeHooks(ctx.companionPath);
|
|
531
|
-
await stripLegacyClaudeGraphifyFile(ctx.companionPath);
|
|
532
323
|
}
|
|
533
324
|
},
|
|
534
325
|
},
|
|
@@ -4,10 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
|
|
6
6
|
import { getOpenSpecSchemaSelection } from "../../../lib/orchestrator/setup-compatibilities";
|
|
7
|
+
import { getWrapperBinPath } from "../../../lib/package-paths";
|
|
7
8
|
import { fetchPublicPackageVersion } from "../../../lib/public-npm";
|
|
8
9
|
import { isNewer } from "../../../lib/update-checker";
|
|
10
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
9
11
|
import { confirm } from "../../../cli/confirm";
|
|
10
|
-
import type { CapabilityPlugin, SetupContext } from "../plugin";
|
|
12
|
+
import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
|
|
11
13
|
import type { InstallRequirement } from "../install-contract";
|
|
12
14
|
import {
|
|
13
15
|
isCommandOnPath,
|
|
@@ -132,6 +134,16 @@ async function reconcileOpenSpecTools(
|
|
|
132
134
|
): Promise<void> {
|
|
133
135
|
const tools = deriveOpenSpecTools(ctx.activeProviders);
|
|
134
136
|
if (tools.length === 0) return;
|
|
137
|
+
/** Best-effort: `config reset` is missing on older openspec releases. */
|
|
138
|
+
try {
|
|
139
|
+
await runCommand("openspec", ["config", "reset", "--all", "-y"], {
|
|
140
|
+
cwd: ctx.companionPath,
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
process.stderr.write(
|
|
144
|
+
`openspec config reset skipped: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
135
147
|
|
|
136
148
|
await runCommand("openspec", ["init", "--tools", tools.join(","), "--force", ctx.companionPath], {
|
|
137
149
|
cwd: ctx.companionPath,
|
|
@@ -309,6 +321,27 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
|
|
|
309
321
|
description: "Route OpenSpec commands to the companion root and enable planning workflows.",
|
|
310
322
|
defaultSelected: true,
|
|
311
323
|
isEnabled: (config) => (config.capabilities ?? []).some((c) => c.name === "openspec"),
|
|
324
|
+
// Permission pre-seeds are declared and reconciled by the Claude Runtime
|
|
325
|
+
// Surface. Skill trees stay imperative on purpose: the workflow skills are
|
|
326
|
+
// written by the external `openspec` CLI (foreign trees), and the
|
|
327
|
+
// mate-authored skills are git-mode-conditional with per-tool sources.
|
|
328
|
+
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
329
|
+
return {
|
|
330
|
+
claude: {
|
|
331
|
+
permissionEntries: [
|
|
332
|
+
"Skill(openspec-explore)",
|
|
333
|
+
"Skill(openspec-propose)",
|
|
334
|
+
"Skill(openspec-apply-change)",
|
|
335
|
+
"Skill(openspec-archive-change)",
|
|
336
|
+
"Skill(mate-artifact-finish)",
|
|
337
|
+
"Skill(mate-openspec-backfill)",
|
|
338
|
+
"Bash(openspec:*)",
|
|
339
|
+
`Bash(${FRAMEWORK_NAME} cap graphify:*)`,
|
|
340
|
+
`Bash(${path.join(getWrapperBinPath(), "openspec")}:*)`,
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
},
|
|
312
345
|
getInstallRequirements: (): InstallRequirement[] => [
|
|
313
346
|
{
|
|
314
347
|
id: "capability:openspec",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import type { CapabilityPlugin, SetupContext } from "../plugin";
|
|
5
|
-
import {
|
|
4
|
+
import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
|
|
5
|
+
import { pruneEmptyAncestors } from "../utils";
|
|
6
6
|
|
|
7
7
|
export const SKILL_SRC = path.join(
|
|
8
8
|
import.meta.dirname,
|
|
@@ -13,22 +13,6 @@ export const CLAUDE_HOOK_SRC = path.join(
|
|
|
13
13
|
"../../../templates/capabilities/react-doctor/claude/hooks/react-doctor.sh",
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
-
export async function applyReactDoctorSkills(skillsDir: string): Promise<void> {
|
|
17
|
-
await mergeDir(SKILL_SRC, skillsDir);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function teardownReactDoctorSkills(
|
|
21
|
-
skillsDir: string,
|
|
22
|
-
companionPath: string,
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
try {
|
|
25
|
-
await fs.rm(skillsDir, { recursive: true, force: true });
|
|
26
|
-
} catch {
|
|
27
|
-
/* not present */
|
|
28
|
-
}
|
|
29
|
-
await pruneEmptyAncestors(path.dirname(skillsDir), companionPath);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
16
|
export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
33
17
|
return {
|
|
34
18
|
id: "react-doctor",
|
|
@@ -37,6 +21,41 @@ export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
|
37
21
|
description: "Install React Doctor skills and Claude post-edit scans.",
|
|
38
22
|
defaultSelected: true,
|
|
39
23
|
isEnabled: (config) => (config.capabilities ?? []).some((c) => c.name === "react-doctor"),
|
|
24
|
+
// Skills, session hooks, and permission pre-seeds are declared and
|
|
25
|
+
// reconciled by each runtime's Runtime Surface. Only the hook script file
|
|
26
|
+
// itself is copied imperatively below (script files are not a config
|
|
27
|
+
// format the surface owns).
|
|
28
|
+
getRuntimeContributions(ctx: SetupContext): RuntimeContributionsByRuntime {
|
|
29
|
+
const hookCommand = `sh "${ctx.companionPath}/.claude/hooks/react-doctor.sh"`;
|
|
30
|
+
const skillTrees = [{ name: "react-doctor", sourceDir: SKILL_SRC }];
|
|
31
|
+
return {
|
|
32
|
+
claude: {
|
|
33
|
+
hookGroups: [
|
|
34
|
+
// Record edits cheaply, then scan once when the edited turn finishes.
|
|
35
|
+
{
|
|
36
|
+
event: "PostToolUse",
|
|
37
|
+
marker: "react-doctor.sh",
|
|
38
|
+
group: {
|
|
39
|
+
matcher: "Write|Edit|MultiEdit|NotebookEdit|ApplyPatch",
|
|
40
|
+
hooks: [{ type: "command", command: hookCommand, timeout: 5 }],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
event: "Stop",
|
|
45
|
+
marker: "react-doctor.sh",
|
|
46
|
+
group: { hooks: [{ type: "command", command: hookCommand, timeout: 45 }] },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
permissionEntries: [
|
|
50
|
+
"Skill(react-doctor)",
|
|
51
|
+
"Bash(npx react-doctor:*)",
|
|
52
|
+
"Bash(npx react-doctor@latest *)",
|
|
53
|
+
],
|
|
54
|
+
skillTrees,
|
|
55
|
+
},
|
|
56
|
+
opencode: { skillTrees },
|
|
57
|
+
};
|
|
58
|
+
},
|
|
40
59
|
async apply(ctx: SetupContext) {
|
|
41
60
|
// Migrate: remove legacy shared .agents/skills/react-doctor/ from previous installs.
|
|
42
61
|
try {
|
|
@@ -57,18 +76,12 @@ export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
|
57
76
|
forProvider: {
|
|
58
77
|
claude: {
|
|
59
78
|
async apply(ctx: SetupContext) {
|
|
60
|
-
const skillsDir = path.join(ctx.companionPath, ".claude", "skills", "react-doctor");
|
|
61
|
-
await applyReactDoctorSkills(skillsDir);
|
|
62
79
|
const hookDest = path.join(ctx.companionPath, ".claude", "hooks", "react-doctor.sh");
|
|
63
80
|
await fs.mkdir(path.dirname(hookDest), { recursive: true });
|
|
64
81
|
await fs.copyFile(CLAUDE_HOOK_SRC, hookDest);
|
|
65
82
|
await fs.chmod(hookDest, 0o755);
|
|
66
83
|
},
|
|
67
84
|
async teardown(ctx: SetupContext) {
|
|
68
|
-
await teardownReactDoctorSkills(
|
|
69
|
-
path.join(ctx.companionPath, ".claude", "skills", "react-doctor"),
|
|
70
|
-
ctx.companionPath,
|
|
71
|
-
);
|
|
72
85
|
try {
|
|
73
86
|
await fs.unlink(path.join(ctx.companionPath, ".claude", "hooks", "react-doctor.sh"));
|
|
74
87
|
} catch {
|
|
@@ -80,19 +93,6 @@ export function createReactDoctorPlugin(): CapabilityPlugin {
|
|
|
80
93
|
);
|
|
81
94
|
},
|
|
82
95
|
},
|
|
83
|
-
opencode: {
|
|
84
|
-
async apply(ctx: SetupContext) {
|
|
85
|
-
await applyReactDoctorSkills(
|
|
86
|
-
path.join(ctx.companionPath, ".opencode", "skills", "react-doctor"),
|
|
87
|
-
);
|
|
88
|
-
},
|
|
89
|
-
async teardown(ctx: SetupContext) {
|
|
90
|
-
await teardownReactDoctorSkills(
|
|
91
|
-
path.join(ctx.companionPath, ".opencode", "skills", "react-doctor"),
|
|
92
|
-
ctx.companionPath,
|
|
93
|
-
);
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
96
|
},
|
|
97
97
|
};
|
|
98
98
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CapabilityPlugin, SetupContext } from "../plugin";
|
|
1
|
+
import type { CapabilityPlugin, RuntimeContributionsByRuntime, SetupContext } from "../plugin";
|
|
2
2
|
import type { InstallRequirement } from "../install-contract";
|
|
3
3
|
import { isCommandOnPath, runShellCommand, runShellCommandSilently } from "../utils";
|
|
4
4
|
import { confirm } from "../../../cli/confirm";
|
|
@@ -52,6 +52,9 @@ export function createRtkPlugin(deps: RtkDeps = {}): CapabilityPlugin {
|
|
|
52
52
|
defaultSelected: false,
|
|
53
53
|
isEnabled: (config) =>
|
|
54
54
|
(config.capabilities ?? []).some((capability) => capability.name === "rtk"),
|
|
55
|
+
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
56
|
+
return { claude: { permissionEntries: ["Bash(rtk:*)"] } };
|
|
57
|
+
},
|
|
55
58
|
getInstallRequirements: (): InstallRequirement[] => {
|
|
56
59
|
const command = checkBrew() ? RTK_INSTALL_CMD_BREW : RTK_INSTALL_CMD_FALLBACK;
|
|
57
60
|
return [
|