@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 +1,7 @@
|
|
|
1
1
|
export const TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES = [".tokensave/"];
|
|
2
|
+
|
|
3
|
+
// Marker that identifies the tokensave installer's CLAUDE.md append block.
|
|
4
|
+
// Declared here (capability-owned) and consumed by the Claude Runtime Surface
|
|
5
|
+
// when reconciling working-repo files at launch.
|
|
6
|
+
export const TOKENSAVE_CLAUDE_MD_MARKER =
|
|
7
|
+
"## MANDATORY: No Explore Agents When Tokensave Is Available";
|
|
@@ -3,29 +3,20 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { resolveGitInfoExcludePath } from "../git-utils";
|
|
6
|
-
import type { CapabilityPlugin } from "../plugin";
|
|
7
|
-
import { isCommandOnPath, runCommand, runShellCommand } from "../utils";
|
|
6
|
+
import type { CapabilityPlugin, RuntimeContributionsByRuntime } from "../plugin";
|
|
7
|
+
import { isCommandOnPath, resolveCommandOnPath, runCommand, runShellCommand } from "../utils";
|
|
8
|
+
import { TOKENSAVE_CLAUDE_MD_MARKER } from "./tokensave-shared";
|
|
8
9
|
export { TOKENSAVE_WORKING_REPO_EXCLUDE_ENTRIES } from "./tokensave-shared";
|
|
9
10
|
|
|
10
11
|
export const TOKENSAVE_SUPPORTED_AGENTS = new Set(["claude", "opencode"]);
|
|
11
12
|
export const TOKENSAVE_STORE_DIR = ".tokensave";
|
|
12
13
|
export const TOKENSAVE_MIN_RUST_VERSION = "1.91.0";
|
|
13
|
-
const TOKENSAVE_CLAUDE_MD_MARKER = "## MANDATORY: No Explore Agents When Tokensave Is Available";
|
|
14
14
|
const TOKENSAVE_STORE_EXCLUDE_ENTRY = `${TOKENSAVE_STORE_DIR}/`;
|
|
15
15
|
const TOKENSAVE_BREW_INSTALL_CMD = "brew install aovestdipaperino/tap/tokensave";
|
|
16
16
|
const TOKENSAVE_CARGO_INSTALL_CMD = "cargo install --locked tokensave";
|
|
17
17
|
const TOKENSAVE_SCOOP_INSTALL_CMD =
|
|
18
18
|
"scoop bucket add tokensave https://github.com/aovestdipaperino/scoop-bucket && scoop install tokensave";
|
|
19
19
|
|
|
20
|
-
interface CompanionOpenCodeSettings {
|
|
21
|
-
mcp?: Record<string, unknown>;
|
|
22
|
-
[key: string]: unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getCompanionOpenCodeConfigPath(companionPath: string): string {
|
|
26
|
-
return path.join(companionPath, ".opencode", "opencode.json");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
export interface TokensaveRunResult {
|
|
30
21
|
ok: boolean;
|
|
31
22
|
stderr: string;
|
|
@@ -34,10 +25,14 @@ export interface TokensaveRunResult {
|
|
|
34
25
|
|
|
35
26
|
export const tokensaveDeps = {
|
|
36
27
|
run(args: string[], cwd: string): TokensaveRunResult {
|
|
28
|
+
// env is passed explicitly: bun's spawnSync otherwise resolves the binary
|
|
29
|
+
// against the process's original PATH, ignoring in-process PATH changes
|
|
30
|
+
// (which test stubs and shell-integration wrappers rely on).
|
|
37
31
|
const result = spawnSync("tokensave", args, {
|
|
38
32
|
cwd,
|
|
39
33
|
encoding: "utf8",
|
|
40
34
|
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
env: process.env,
|
|
41
36
|
});
|
|
42
37
|
return {
|
|
43
38
|
ok: result.status === 0 && !result.error,
|
|
@@ -58,6 +53,7 @@ export const tokensaveDeps = {
|
|
|
58
53
|
const result = spawnSync("rustc", ["--version"], {
|
|
59
54
|
encoding: "utf8",
|
|
60
55
|
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
env: process.env,
|
|
61
57
|
});
|
|
62
58
|
return {
|
|
63
59
|
ok: result.status === 0 && !result.error,
|
|
@@ -73,41 +69,6 @@ export const tokensaveDeps = {
|
|
|
73
69
|
},
|
|
74
70
|
};
|
|
75
71
|
|
|
76
|
-
async function readCompanionOpenCodeSettings(
|
|
77
|
-
companionPath: string,
|
|
78
|
-
): Promise<CompanionOpenCodeSettings> {
|
|
79
|
-
try {
|
|
80
|
-
const parsed = JSON.parse(
|
|
81
|
-
await fs.readFile(getCompanionOpenCodeConfigPath(companionPath), "utf8"),
|
|
82
|
-
) as unknown;
|
|
83
|
-
if (parsed && typeof parsed === "object") {
|
|
84
|
-
return parsed as CompanionOpenCodeSettings;
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
/* absent or malformed */
|
|
88
|
-
}
|
|
89
|
-
return {};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function removeOpenCodeMcpServer(companionPath: string): Promise<void> {
|
|
93
|
-
const configPath = getCompanionOpenCodeConfigPath(companionPath);
|
|
94
|
-
const settings = await readCompanionOpenCodeSettings(companionPath);
|
|
95
|
-
if (!settings.mcp || typeof settings.mcp !== "object") {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const mcp = { ...settings.mcp };
|
|
100
|
-
delete mcp.tokensave;
|
|
101
|
-
if (Object.keys(mcp).length > 0) {
|
|
102
|
-
settings.mcp = mcp;
|
|
103
|
-
} else {
|
|
104
|
-
delete settings.mcp;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
108
|
-
await fs.writeFile(configPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
72
|
async function cleanupRepoLocalTokensaveArtifacts(repoPath: string): Promise<void> {
|
|
112
73
|
const claudeMdPath = path.join(repoPath, "CLAUDE.md");
|
|
113
74
|
try {
|
|
@@ -314,22 +275,55 @@ export function createTokensavePlugin(): CapabilityPlugin {
|
|
|
314
275
|
},
|
|
315
276
|
];
|
|
316
277
|
},
|
|
278
|
+
// MCP access, session hooks, and permission pre-seeds are declared below
|
|
279
|
+
// and reconciled by each runtime's Runtime Surface. The bare `tokensave`
|
|
280
|
+
// command name in the MCP entry is resolved against PATH at spawn time by
|
|
281
|
+
// each provider, so it never pins a since-moved/upgraded binary.
|
|
282
|
+
getRuntimeContributions(): RuntimeContributionsByRuntime {
|
|
283
|
+
const mcpServers = [{ name: "tokensave", command: "tokensave", args: ["serve"] }];
|
|
284
|
+
// Hook commands pin the resolved binary path (hooks run without the
|
|
285
|
+
// launch environment's PATH guarantees).
|
|
286
|
+
const tokensaveCommandPath =
|
|
287
|
+
resolveCommandOnPath("tokensave", process.env.PATH ?? "") ?? "tokensave";
|
|
288
|
+
const hookGroups = [
|
|
289
|
+
{
|
|
290
|
+
event: "PreToolUse",
|
|
291
|
+
marker: "tokensave",
|
|
292
|
+
group: {
|
|
293
|
+
matcher: "Agent|Grep|Bash",
|
|
294
|
+
hooks: [
|
|
295
|
+
{ type: "command", command: tokensaveCommandPath, args: ["hook-pre-tool-use"] },
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
event: "UserPromptSubmit",
|
|
301
|
+
marker: "tokensave",
|
|
302
|
+
group: {
|
|
303
|
+
hooks: [
|
|
304
|
+
{ type: "command", command: tokensaveCommandPath, args: ["hook-prompt-submit"] },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
event: "Stop",
|
|
310
|
+
marker: "tokensave",
|
|
311
|
+
group: {
|
|
312
|
+
hooks: [{ type: "command", command: tokensaveCommandPath, args: ["hook-stop"] }],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
return {
|
|
317
|
+
claude: { mcpServers, hookGroups, permissionEntries: ["mcp__tokensave__*"] },
|
|
318
|
+
opencode: { mcpServers },
|
|
319
|
+
};
|
|
320
|
+
},
|
|
317
321
|
async apply(ctx) {
|
|
318
322
|
// Presence only: make sure the tokensave binary is installed so the graph can be
|
|
319
323
|
// built later. The graph build itself (init/sync) lives in `mate cap index`, never
|
|
320
324
|
// in setup or launch-time sync — keeping launch fast and indexing an explicit step.
|
|
321
325
|
if (ctx.activeProviders.filter((p) => TOKENSAVE_SUPPORTED_AGENTS.has(p)).length === 0) return;
|
|
322
326
|
|
|
323
|
-
// MCP access is provider-mediated: every active hosting provider gets the
|
|
324
|
-
// server in its native config, and teardown bookkeeping removes it again.
|
|
325
|
-
// The bare command name is resolved against PATH at spawn time by each
|
|
326
|
-
// provider, so the registration never pins a since-moved/upgraded binary.
|
|
327
|
-
await ctx.mcp?.register({
|
|
328
|
-
name: "tokensave",
|
|
329
|
-
command: "tokensave",
|
|
330
|
-
args: ["serve"],
|
|
331
|
-
});
|
|
332
|
-
|
|
333
327
|
const targetPath = ctx.repoPath ?? ctx.companionPath;
|
|
334
328
|
if (!(await ensureTokensaveInstalled(targetPath))) {
|
|
335
329
|
return;
|
|
@@ -342,20 +336,5 @@ export function createTokensavePlugin(): CapabilityPlugin {
|
|
|
342
336
|
if (!ctx.repoPath) return;
|
|
343
337
|
await teardownDriver(ctx.repoPath, ctx.activeProviders);
|
|
344
338
|
},
|
|
345
|
-
forProvider: {
|
|
346
|
-
claude: {
|
|
347
|
-
async apply(_ctx) {},
|
|
348
|
-
async teardown(_ctx) {},
|
|
349
|
-
},
|
|
350
|
-
opencode: {
|
|
351
|
-
// Registration goes through ctx.mcp now. Teardown stays as a legacy
|
|
352
|
-
// prune for entries written by releases that predate the bookkeeping
|
|
353
|
-
// manifest.
|
|
354
|
-
async apply(_ctx) {},
|
|
355
|
-
async teardown(ctx) {
|
|
356
|
-
await removeOpenCodeMcpServer(ctx.companionPath);
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
339
|
};
|
|
361
340
|
}
|
|
@@ -99,6 +99,35 @@ export async function upsertManagedBlock(
|
|
|
99
99
|
await fs.writeFile(filePath, existing + (existing ? separator : "") + block, "utf8");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Remove every managed block a plugin owns in `filePath`, except blocks whose
|
|
104
|
+
* key is in `keepKeys`. Used by contribution reconciliation: current sections
|
|
105
|
+
* are upserted (content-hashed keys), then stale keys are swept here.
|
|
106
|
+
*/
|
|
107
|
+
export async function removeManagedBlocksForPlugin(
|
|
108
|
+
filePath: string,
|
|
109
|
+
frameworkName: string,
|
|
110
|
+
pluginId: string,
|
|
111
|
+
keepKeys: Set<string> = new Set(),
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
let existing: string;
|
|
114
|
+
try {
|
|
115
|
+
existing = await fs.readFile(filePath, "utf8");
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
120
|
+
const keyPattern = new RegExp(
|
|
121
|
+
`<!-- ${escape(frameworkName)}:managed:(${escape(pluginId)}:[0-9a-f]+) start -->`,
|
|
122
|
+
"g",
|
|
123
|
+
);
|
|
124
|
+
const keys = [...existing.matchAll(keyPattern)].map((match) => match[1]);
|
|
125
|
+
for (const key of keys) {
|
|
126
|
+
if (keepKeys.has(key)) continue;
|
|
127
|
+
await removeManagedBlock(filePath, frameworkName, key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
/** Remove a marker-delimited block if present. No-op when file or block is absent. */
|
|
103
132
|
export async function removeManagedBlock(
|
|
104
133
|
filePath: string,
|
|
@@ -9,6 +9,7 @@ import { CompanionResolver } from "../../../lib/orchestrator/companion-resolver"
|
|
|
9
9
|
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
10
10
|
import { PLUGIN_DECLARATION_POLICIES } from "../../../lib/orchestrator/config-store";
|
|
11
11
|
import type { PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
12
|
+
import type { Plugin } from "../plugin";
|
|
12
13
|
import type { PluginRegistry } from "../registry";
|
|
13
14
|
import { loadDynamicPlugin, type DynamicPluginLoadDeps } from "./loader";
|
|
14
15
|
|
|
@@ -89,6 +90,16 @@ function validateDeclaration(entry: unknown): { declaration?: PluginDeclaration;
|
|
|
89
90
|
return { declaration: entry as unknown as PluginDeclaration };
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* A framework declaration is the activation switch for a dynamic plugin.
|
|
95
|
+
* Own-property override rather than a spread: class-instance plugins keep
|
|
96
|
+
* their prototype members.
|
|
97
|
+
*/
|
|
98
|
+
function activateDeclaredPlugin(plugin: Plugin): Plugin {
|
|
99
|
+
plugin.isEnabled = () => true;
|
|
100
|
+
return plugin;
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
/**
|
|
93
104
|
* Registers the companion's declared plugins into the active registry —
|
|
94
105
|
* compiled-in plugins first (they are already registered), declared order
|
|
@@ -135,7 +146,10 @@ export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}
|
|
|
135
146
|
);
|
|
136
147
|
}
|
|
137
148
|
|
|
138
|
-
registry.register({
|
|
149
|
+
registry.register({
|
|
150
|
+
plugin: activateDeclaredPlugin(result.plugin),
|
|
151
|
+
policy: declaration.policy ?? "optional",
|
|
152
|
+
});
|
|
139
153
|
hydratedPackages.add(declaration.package);
|
|
140
154
|
}
|
|
141
155
|
} catch (error) {
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
1
2
|
import { getActiveDistribution } from "../../distribution";
|
|
2
3
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
3
4
|
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
4
5
|
import { collectHostingProviders, ContextServiceMediator } from "./context-services";
|
|
5
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
CapabilityContributionInput,
|
|
8
|
+
CapabilityPlugin,
|
|
9
|
+
Plugin,
|
|
10
|
+
PluginRegistration,
|
|
11
|
+
SetupContext,
|
|
12
|
+
} from "./plugin";
|
|
13
|
+
import { reconcileClaudeContributions } from "./providers/claude";
|
|
14
|
+
import { reconcileOpenCodeContributions } from "./providers/opencode";
|
|
6
15
|
import { normalizeRegistration, type NormalizedRegistration } from "./registry";
|
|
7
16
|
|
|
8
17
|
export interface SetupInstallationPlanAction {
|
|
@@ -117,6 +126,8 @@ export async function executeSetupInstallationPlan(
|
|
|
117
126
|
});
|
|
118
127
|
|
|
119
128
|
for (const action of plan.actions) {
|
|
129
|
+
if (ctx.scope === "hub" && action.phase !== "provider") continue;
|
|
130
|
+
|
|
120
131
|
const plugin = pluginById.get(action.pluginId);
|
|
121
132
|
if (!plugin) {
|
|
122
133
|
continue;
|
|
@@ -151,5 +162,61 @@ export async function executeSetupInstallationPlan(
|
|
|
151
162
|
|
|
152
163
|
await mediator.finalize();
|
|
153
164
|
|
|
165
|
+
await reconcileCapabilityContributions(ctx, plugins, plan);
|
|
166
|
+
|
|
154
167
|
return { executedActions, skippedActions, warnings };
|
|
155
168
|
}
|
|
169
|
+
|
|
170
|
+
// The Runtime Surface of each active Agent Runtime, by runtime id.
|
|
171
|
+
const RUNTIME_SURFACE_RECONCILERS: Record<
|
|
172
|
+
string,
|
|
173
|
+
(ctx: SetupContext, inputs: CapabilityContributionInput[]) => Promise<void>
|
|
174
|
+
> = {
|
|
175
|
+
claude: reconcileClaudeContributions,
|
|
176
|
+
opencode: reconcileOpenCodeContributions,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Reconcile declared Capability contributions through every active runtime's
|
|
181
|
+
* Runtime Surface (spec: plugin-engine). Runs after all plugin phases, so
|
|
182
|
+
* providers are always done first. Disabled capabilities participate with
|
|
183
|
+
* `enabled: false` and drive teardown of their managed entries.
|
|
184
|
+
*/
|
|
185
|
+
async function reconcileCapabilityContributions(
|
|
186
|
+
ctx: SetupContext,
|
|
187
|
+
plugins: Plugin[],
|
|
188
|
+
plan: SetupInstallationPlan,
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
// The plan already resolved enablement (policy, saved selection, package
|
|
191
|
+
// manager requirements); mirror it instead of recomputing.
|
|
192
|
+
const enabledByPluginId = new Map(
|
|
193
|
+
plan.actions
|
|
194
|
+
.filter((action) => action.phase === "capability" && action.providerId === undefined)
|
|
195
|
+
.map((action) => [action.pluginId, action.action === "apply"]),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const inputsByRuntime = new Map<string, CapabilityContributionInput[]>();
|
|
199
|
+
for (const plugin of plugins) {
|
|
200
|
+
if (plugin.kind !== "capability") continue;
|
|
201
|
+
const capability = plugin as CapabilityPlugin;
|
|
202
|
+
if (!capability.getRuntimeContributions) continue;
|
|
203
|
+
const enabled = enabledByPluginId.get(capability.id) ?? false;
|
|
204
|
+
const byRuntime = await capability.getRuntimeContributions(ctx);
|
|
205
|
+
for (const [runtimeId, contributions] of Object.entries(byRuntime)) {
|
|
206
|
+
if (!contributions) continue;
|
|
207
|
+
// Inactive runtimes still reconcile — with everything disabled — so a
|
|
208
|
+
// deselected runtime's managed entries (skill trees, guidance blocks)
|
|
209
|
+
// are torn down. The surfaces never create files for disabled inputs.
|
|
210
|
+
const runtimeActive = plan.activeProviders.includes(runtimeId);
|
|
211
|
+
const inputs = inputsByRuntime.get(runtimeId) ?? [];
|
|
212
|
+
inputs.push({ pluginId: capability.id, enabled: enabled && runtimeActive, contributions });
|
|
213
|
+
inputsByRuntime.set(runtimeId, inputs);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const [runtimeId, inputs] of inputsByRuntime) {
|
|
218
|
+
const reconcile = RUNTIME_SURFACE_RECONCILERS[runtimeId];
|
|
219
|
+
if (!reconcile || inputs.length === 0) continue;
|
|
220
|
+
await reconcile(ctx, inputs);
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/tools/setup/mate.ts
CHANGED
|
@@ -4,7 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
import { pruneEmptyAncestors } from "./utils";
|
|
5
5
|
|
|
6
6
|
export const MATE_ARTIFACT_SKILLS = ["mate-artifact-finish"] as const;
|
|
7
|
-
export const MATE_SKILLS = [
|
|
7
|
+
export const MATE_SKILLS = [
|
|
8
|
+
"mate-artifact-finish",
|
|
9
|
+
"mate-create-report",
|
|
10
|
+
"mate-openspec-backfill",
|
|
11
|
+
] as const;
|
|
12
|
+
const LEGACY_MATE_SKILLS = ["mate-openspec-artifact-finish"] as const;
|
|
8
13
|
|
|
9
14
|
const MATE_SKILLS_SOURCE = path.join(
|
|
10
15
|
import.meta.dirname,
|
|
@@ -98,6 +103,9 @@ async function resolveMateSkillSource(skill: string, tool: string): Promise<stri
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
export async function applyMateSkills(skillsDir: string, tool: string): Promise<void> {
|
|
106
|
+
for (const skill of LEGACY_MATE_SKILLS) {
|
|
107
|
+
await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
|
|
108
|
+
}
|
|
101
109
|
for (const skill of MATE_SKILLS) {
|
|
102
110
|
const destination = path.join(skillsDir, skill);
|
|
103
111
|
if (skill === "mate-create-report") {
|
|
@@ -110,7 +118,7 @@ export async function applyMateSkills(skillsDir: string, tool: string): Promise<
|
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
export async function teardownMateSkills(skillsDir: string, companionPath: string): Promise<void> {
|
|
113
|
-
for (const skill of MATE_SKILLS) {
|
|
121
|
+
for (const skill of [...MATE_SKILLS, ...LEGACY_MATE_SKILLS]) {
|
|
114
122
|
try {
|
|
115
123
|
await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
|
|
116
124
|
} catch {
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { FrameworkConfig, LinkedRepository } from "../../lib/orchestrator/types";
|
|
2
2
|
import type { InstallRequirement, InstallRequirementContext } from "./install-contract";
|
|
3
|
+
import type { ClaudeHookGroup } from "./providers/claude-format";
|
|
4
|
+
|
|
5
|
+
export type SetupScope = "companion" | "hub";
|
|
3
6
|
|
|
4
7
|
export interface SetupContext {
|
|
5
8
|
companionPath: string;
|
|
6
9
|
config: FrameworkConfig;
|
|
7
10
|
mode: "setup" | "sync";
|
|
8
11
|
activeProviders: string[];
|
|
12
|
+
/** Hub setup runs providers but excludes companion-only surfaces. */
|
|
13
|
+
scope?: SetupScope;
|
|
9
14
|
/** Working repository path when syncing from a linked repo (e.g. during launch). */
|
|
10
15
|
repoPath?: string;
|
|
11
16
|
/**
|
|
@@ -112,9 +117,109 @@ export type PluginPolicy = "required" | "default" | "optional";
|
|
|
112
117
|
*/
|
|
113
118
|
export type PluginRegistration = Plugin | { plugin: Plugin; policy: PluginPolicy };
|
|
114
119
|
|
|
120
|
+
/**
|
|
121
|
+
* A hook group a Capability contributes to a runtime's settings. `marker` is
|
|
122
|
+
* the command substring that identifies the group as Mate-managed (D4 marker
|
|
123
|
+
* scheme): reconciliation strips every group whose command contains any
|
|
124
|
+
* declared marker before re-adding the groups of enabled Capabilities.
|
|
125
|
+
*/
|
|
126
|
+
export interface HookGroupContribution {
|
|
127
|
+
event: string;
|
|
128
|
+
marker: string;
|
|
129
|
+
group: ClaudeHookGroup;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A skill directory copied into `<runtime dir>/skills/<name>`. */
|
|
133
|
+
export interface SkillTreeContribution {
|
|
134
|
+
name: string;
|
|
135
|
+
sourceDir: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A managed guidance section in the runtime's agent instruction file
|
|
140
|
+
* (CLAUDE.md / AGENTS.md). Reconciled as a framework-managed block; on
|
|
141
|
+
* teardown a section in a file shared with another active runtime is only
|
|
142
|
+
* stripped together with the last runtime using that file.
|
|
143
|
+
*/
|
|
144
|
+
export interface GuidanceSectionContribution {
|
|
145
|
+
content: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A plugin reference entry in the runtime's config (OpenCode `plugin` array).
|
|
150
|
+
* `isManagedReference` identifies entries this contribution owns so stale
|
|
151
|
+
* variants (e.g. older pins) are replaced and teardown removes only them.
|
|
152
|
+
*/
|
|
153
|
+
export interface PluginReferenceContribution {
|
|
154
|
+
reference: string;
|
|
155
|
+
isManagedReference(entry: unknown): boolean;
|
|
156
|
+
/** Config files to reconcile, relative to the runtime dir. Defaults to both OpenCode configs. */
|
|
157
|
+
configFiles?: string[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A real, provider-native agent definition file, written to
|
|
162
|
+
* `<runtime dir>/agents/<name>.md` and selectable via `--agent <name>`.
|
|
163
|
+
* Unlike a guidance section, the whole file is managed content (no merge with
|
|
164
|
+
* unmanaged text) — the Capability pre-renders `content` per runtime, since
|
|
165
|
+
* frontmatter shape (Claude's `name`/`hidden`, OpenCode's `mode`) differs.
|
|
166
|
+
* Reconciled in both companion and hub scope: an agent definition is exactly
|
|
167
|
+
* the kind of artifact a repo-less hub needs.
|
|
168
|
+
*/
|
|
169
|
+
export interface AgentDefinitionContribution {
|
|
170
|
+
/** Agent name; the file is written to `<runtime dir>/agents/<name>.md`. */
|
|
171
|
+
name: string;
|
|
172
|
+
/** Full file content, including frontmatter, ready to write as-is. */
|
|
173
|
+
content: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Declarative Agent Runtime contributions of one Capability for one runtime.
|
|
178
|
+
* The runtime's Runtime Surface reconciles these symmetrically: applied while
|
|
179
|
+
* the Capability is enabled, removed when it is not, idempotent across runs.
|
|
180
|
+
*/
|
|
181
|
+
export interface RuntimeContributions {
|
|
182
|
+
mcpServers?: McpServerDescriptor[];
|
|
183
|
+
hookGroups?: HookGroupContribution[];
|
|
184
|
+
permissionEntries?: string[];
|
|
185
|
+
guidanceSections?: GuidanceSectionContribution[];
|
|
186
|
+
skillTrees?: SkillTreeContribution[];
|
|
187
|
+
pluginReferences?: PluginReferenceContribution[];
|
|
188
|
+
agentDefinitions?: AgentDefinitionContribution[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Contributions keyed by runtime id ("claude", "opencode"). Keyed-by-runtime
|
|
193
|
+
* because the payload shapes are runtime-specific (hook groups are
|
|
194
|
+
* Claude-shaped, plugin references OpenCode-shaped); a runtime-agnostic
|
|
195
|
+
* declaration would only push translation into every capability.
|
|
196
|
+
*/
|
|
197
|
+
export type RuntimeContributionsByRuntime = Partial<Record<string, RuntimeContributions>>;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* One Capability's contributions for one runtime, as handed to that runtime's
|
|
201
|
+
* Runtime Surface reconciliation. Disabled Capabilities participate too: their
|
|
202
|
+
* declarations define the managed entries to strip.
|
|
203
|
+
*/
|
|
204
|
+
export interface CapabilityContributionInput {
|
|
205
|
+
pluginId: string;
|
|
206
|
+
enabled: boolean;
|
|
207
|
+
contributions: RuntimeContributions;
|
|
208
|
+
}
|
|
209
|
+
|
|
115
210
|
export interface CapabilityPlugin extends Plugin {
|
|
116
211
|
kind: "capability";
|
|
117
212
|
requires?: { packageManagers: string[] };
|
|
213
|
+
/**
|
|
214
|
+
* Declare Agent Runtime contributions as data. Called on every setup/sync
|
|
215
|
+
* pass for all registered Capabilities — enabled ones contribute their
|
|
216
|
+
* entries, disabled ones only widen the managed strip set so their previous
|
|
217
|
+
* entries are removed. May be async (e.g. reading a companion-local
|
|
218
|
+
* override file) — the engine always awaits the result.
|
|
219
|
+
*/
|
|
220
|
+
getRuntimeContributions?(
|
|
221
|
+
ctx: SetupContext,
|
|
222
|
+
): RuntimeContributionsByRuntime | Promise<RuntimeContributionsByRuntime>;
|
|
118
223
|
forProvider?: Record<
|
|
119
224
|
string,
|
|
120
225
|
{
|
|
@@ -61,12 +61,15 @@ export async function writeManagedGitignoreBlock(
|
|
|
61
61
|
|
|
62
62
|
export function collectManagedGitignoreEntries(ctx: SetupContext, plugins: Plugin[]): string[] {
|
|
63
63
|
// Baseline for every companion: node_modules never versions wherever a tool
|
|
64
|
-
// materializes it, and
|
|
65
|
-
// (context-mode, future consumers) is regenerated by
|
|
66
|
-
// version pins live in .mate/config and in code,
|
|
64
|
+
// materializes it, and the machine-local distribution-dependencies workspace
|
|
65
|
+
// (.mate/plugins/.local/ — context-mode, future consumers) is regenerated by
|
|
66
|
+
// setup/install in full — version pins live in .mate/config and in code,
|
|
67
|
+
// never in the tree itself. This is distinct from the committed dynamic-
|
|
68
|
+
// plugins workspace at .mate/plugins/, whose package.json and lockfile stay
|
|
69
|
+
// trackable.
|
|
67
70
|
const entries = [
|
|
68
71
|
"node_modules/",
|
|
69
|
-
`.${FRAMEWORK_NAME}/
|
|
72
|
+
`.${FRAMEWORK_NAME}/plugins/.local/`,
|
|
70
73
|
".mcp.json*",
|
|
71
74
|
...plugins
|
|
72
75
|
.filter((p) => p.kind !== "root" && (p.isEnabled(ctx.config) || p.persistGitignoreEntries))
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
5
|
-
import { removeGraphifySection } from "../capabilities/graphify";
|
|
5
|
+
import { removeGraphifySection } from "../capabilities/graphify-shared";
|
|
6
6
|
|
|
7
7
|
// Marker names derive from the framework identity, never the invocation name.
|
|
8
8
|
const upperName = () => FRAMEWORK_NAME.toUpperCase();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Shared markdown section primitives for agent instruction files (CLAUDE.md,
|
|
2
|
+
// AGENTS.md), plus a thin file-level wrapper for stripping sections in place.
|
|
3
|
+
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
|
|
6
|
+
export interface RemoveHeadingSectionOptions {
|
|
7
|
+
/** Matches the heading line that opens the section to remove. */
|
|
8
|
+
isHeading(line: string): boolean;
|
|
9
|
+
/** Exact lines (e.g. HTML comment markers) stripped wherever they appear. */
|
|
10
|
+
markerLines?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Remove a heading-delimited section (heading line up to the next `#`/`##`
|
|
15
|
+
* heading or EOF), collapsing leftover blank runs. Marker lines are stripped
|
|
16
|
+
* even when no section heading is present.
|
|
17
|
+
*/
|
|
18
|
+
export function removeHeadingSection(
|
|
19
|
+
content: string,
|
|
20
|
+
options: RemoveHeadingSectionOptions,
|
|
21
|
+
): string {
|
|
22
|
+
const markerLines = new Set(options.markerLines ?? []);
|
|
23
|
+
const normalizedLines = content.split("\n").filter((line) => !markerLines.has(line));
|
|
24
|
+
|
|
25
|
+
const start = normalizedLines.findIndex(options.isHeading);
|
|
26
|
+
if (start === -1) {
|
|
27
|
+
const strippedMarkersOnly = normalizedLines.join("\n");
|
|
28
|
+
return strippedMarkersOnly === content ? content : strippedMarkersOnly;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let end = normalizedLines.length;
|
|
32
|
+
for (let i = start + 1; i < normalizedLines.length; i++) {
|
|
33
|
+
if (normalizedLines[i].startsWith("## ") || normalizedLines[i].startsWith("# ")) {
|
|
34
|
+
end = i;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const remaining = [...normalizedLines.slice(0, start), ...normalizedLines.slice(end)].join("\n");
|
|
40
|
+
const collapsed = remaining.replace(/\n{3,}/g, "\n\n").trim();
|
|
41
|
+
return collapsed ? collapsed + "\n" : "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cut everything from `marker` to EOF. Returns the content unchanged when the
|
|
46
|
+
* marker is absent, and "" when nothing but whitespace precedes it (callers
|
|
47
|
+
* typically delete the file then).
|
|
48
|
+
*/
|
|
49
|
+
export function cutFromMarker(content: string, marker: string): string {
|
|
50
|
+
const idx = content.indexOf(marker);
|
|
51
|
+
if (idx === -1) return content;
|
|
52
|
+
|
|
53
|
+
const before = content.slice(0, idx).replace(/\s+$/, "");
|
|
54
|
+
return before.length > 0 ? before + "\n" : "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Strip a heading-delimited section from a file in place. Absent files are a
|
|
59
|
+
* no-op; a file left without content is deleted.
|
|
60
|
+
*/
|
|
61
|
+
export async function stripSectionFromFile(
|
|
62
|
+
filePath: string,
|
|
63
|
+
options: RemoveHeadingSectionOptions,
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
let content: string;
|
|
66
|
+
try {
|
|
67
|
+
content = await fs.readFile(filePath, "utf8");
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const stripped = removeHeadingSection(content, options);
|
|
72
|
+
if (stripped === content) return;
|
|
73
|
+
if (!stripped.trim()) {
|
|
74
|
+
await fs.unlink(filePath);
|
|
75
|
+
} else {
|
|
76
|
+
await fs.writeFile(filePath, stripped, "utf8");
|
|
77
|
+
}
|
|
78
|
+
}
|