@uniqbit/mate-core 0.15.4-canary.9 → 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 +1 -1
- 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 +155 -45
- package/src/tools/setup/capabilities/context-mode.ts +9 -0
- package/src/tools/setup/capabilities/openspec.ts +11 -0
- package/src/tools/setup/dynamic-plugins/hydrate.ts +15 -1
- package/src/tools/setup/engine.ts +4 -1
- package/src/tools/setup/mate.ts +10 -2
- package/src/tools/setup/plugin.ts +26 -2
- package/src/tools/setup/plugins/gitignore.ts +7 -4
- package/src/tools/setup/providers/claude.ts +53 -8
- package/src/tools/setup/providers/opencode.ts +51 -8
- package/src/tools/setup.ts +7 -56
|
@@ -38,6 +38,12 @@ function getLegacyOwnershipPath(companionPath: string): string {
|
|
|
38
38
|
return path.join(companionPath, ".mate", "state", "context-mode.json");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Legacy install location from before the relocate-distribution-deps-to-plugins-local
|
|
42
|
+
// change; self-heals on every apply the same way the ownership sidecar above does.
|
|
43
|
+
function getLegacyInstallDir(companionPath: string): string {
|
|
44
|
+
return path.join(companionPath, ".mate", "dependencies", CONTEXT_MODE_PACKAGE_NAME);
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
function hasContextModeMcp(servers: Record<string, unknown>): boolean {
|
|
42
48
|
return Object.entries(servers).some(
|
|
43
49
|
([name, value]) =>
|
|
@@ -117,6 +123,9 @@ export function createContextModePlugin(deps: ContextModePluginDeps = {}): Capab
|
|
|
117
123
|
};
|
|
118
124
|
},
|
|
119
125
|
async apply(ctx) {
|
|
126
|
+
const legacyInstallDir = getLegacyInstallDir(ctx.companionPath);
|
|
127
|
+
await fs.rm(legacyInstallDir, { recursive: true, force: true });
|
|
128
|
+
await pruneEmptyAncestors(path.dirname(legacyInstallDir), ctx.companionPath);
|
|
120
129
|
if (ctx.mode === "setup") {
|
|
121
130
|
await installPackage(ctx.companionPath);
|
|
122
131
|
} else {
|
|
@@ -134,6 +134,16 @@ async function reconcileOpenSpecTools(
|
|
|
134
134
|
): Promise<void> {
|
|
135
135
|
const tools = deriveOpenSpecTools(ctx.activeProviders);
|
|
136
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
|
+
}
|
|
137
147
|
|
|
138
148
|
await runCommand("openspec", ["init", "--tools", tools.join(","), "--force", ctx.companionPath], {
|
|
139
149
|
cwd: ctx.companionPath,
|
|
@@ -324,6 +334,7 @@ export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityP
|
|
|
324
334
|
"Skill(openspec-apply-change)",
|
|
325
335
|
"Skill(openspec-archive-change)",
|
|
326
336
|
"Skill(mate-artifact-finish)",
|
|
337
|
+
"Skill(mate-openspec-backfill)",
|
|
327
338
|
"Bash(openspec:*)",
|
|
328
339
|
`Bash(${FRAMEWORK_NAME} cap graphify:*)`,
|
|
329
340
|
`Bash(${path.join(getWrapperBinPath(), "openspec")}:*)`,
|
|
@@ -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,3 +1,4 @@
|
|
|
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";
|
|
@@ -125,6 +126,8 @@ export async function executeSetupInstallationPlan(
|
|
|
125
126
|
});
|
|
126
127
|
|
|
127
128
|
for (const action of plan.actions) {
|
|
129
|
+
if (ctx.scope === "hub" && action.phase !== "provider") continue;
|
|
130
|
+
|
|
128
131
|
const plugin = pluginById.get(action.pluginId);
|
|
129
132
|
if (!plugin) {
|
|
130
133
|
continue;
|
|
@@ -198,7 +201,7 @@ async function reconcileCapabilityContributions(
|
|
|
198
201
|
const capability = plugin as CapabilityPlugin;
|
|
199
202
|
if (!capability.getRuntimeContributions) continue;
|
|
200
203
|
const enabled = enabledByPluginId.get(capability.id) ?? false;
|
|
201
|
-
const byRuntime = capability.getRuntimeContributions(ctx);
|
|
204
|
+
const byRuntime = await capability.getRuntimeContributions(ctx);
|
|
202
205
|
for (const [runtimeId, contributions] of Object.entries(byRuntime)) {
|
|
203
206
|
if (!contributions) continue;
|
|
204
207
|
// Inactive runtimes still reconcile — with everything disabled — so a
|
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 {
|
|
@@ -2,11 +2,15 @@ import type { FrameworkConfig, LinkedRepository } from "../../lib/orchestrator/t
|
|
|
2
2
|
import type { InstallRequirement, InstallRequirementContext } from "./install-contract";
|
|
3
3
|
import type { ClaudeHookGroup } from "./providers/claude-format";
|
|
4
4
|
|
|
5
|
+
export type SetupScope = "companion" | "hub";
|
|
6
|
+
|
|
5
7
|
export interface SetupContext {
|
|
6
8
|
companionPath: string;
|
|
7
9
|
config: FrameworkConfig;
|
|
8
10
|
mode: "setup" | "sync";
|
|
9
11
|
activeProviders: string[];
|
|
12
|
+
/** Hub setup runs providers but excludes companion-only surfaces. */
|
|
13
|
+
scope?: SetupScope;
|
|
10
14
|
/** Working repository path when syncing from a linked repo (e.g. during launch). */
|
|
11
15
|
repoPath?: string;
|
|
12
16
|
/**
|
|
@@ -153,6 +157,22 @@ export interface PluginReferenceContribution {
|
|
|
153
157
|
configFiles?: string[];
|
|
154
158
|
}
|
|
155
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
|
+
|
|
156
176
|
/**
|
|
157
177
|
* Declarative Agent Runtime contributions of one Capability for one runtime.
|
|
158
178
|
* The runtime's Runtime Surface reconciles these symmetrically: applied while
|
|
@@ -165,6 +185,7 @@ export interface RuntimeContributions {
|
|
|
165
185
|
guidanceSections?: GuidanceSectionContribution[];
|
|
166
186
|
skillTrees?: SkillTreeContribution[];
|
|
167
187
|
pluginReferences?: PluginReferenceContribution[];
|
|
188
|
+
agentDefinitions?: AgentDefinitionContribution[];
|
|
168
189
|
}
|
|
169
190
|
|
|
170
191
|
/**
|
|
@@ -193,9 +214,12 @@ export interface CapabilityPlugin extends Plugin {
|
|
|
193
214
|
* Declare Agent Runtime contributions as data. Called on every setup/sync
|
|
194
215
|
* pass for all registered Capabilities — enabled ones contribute their
|
|
195
216
|
* entries, disabled ones only widen the managed strip set so their previous
|
|
196
|
-
* entries are removed.
|
|
217
|
+
* entries are removed. May be async (e.g. reading a companion-local
|
|
218
|
+
* override file) — the engine always awaits the result.
|
|
197
219
|
*/
|
|
198
|
-
getRuntimeContributions?(
|
|
220
|
+
getRuntimeContributions?(
|
|
221
|
+
ctx: SetupContext,
|
|
222
|
+
): RuntimeContributionsByRuntime | Promise<RuntimeContributionsByRuntime>;
|
|
199
223
|
forProvider?: Record<
|
|
200
224
|
string,
|
|
201
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))
|
|
@@ -346,6 +346,12 @@ export async function reconcileClaudeContributions(
|
|
|
346
346
|
): Promise<void> {
|
|
347
347
|
const { companionPath, config } = ctx;
|
|
348
348
|
|
|
349
|
+
if (ctx.scope === "hub") {
|
|
350
|
+
await reconcileClaudeMcpContributions(ctx, inputs);
|
|
351
|
+
await reconcileClaudeAgentDefinitionContributions(ctx, inputs);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
349
355
|
// The settings sync (re)creates the managed settings document, so it only
|
|
350
356
|
// runs while the Claude runtime is active; a deactivated runtime's pass is
|
|
351
357
|
// teardown-only and must not resurrect files the provider teardown removed.
|
|
@@ -353,15 +359,10 @@ export async function reconcileClaudeContributions(
|
|
|
353
359
|
await syncCompanionClaudeSettings(companionPath, config, inputs);
|
|
354
360
|
}
|
|
355
361
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
await updateClaudeMcpServer(
|
|
359
|
-
getCompanionClaudeMcpConfigPath(companionPath),
|
|
360
|
-
descriptor.name,
|
|
361
|
-
input.enabled ? toClaudeMcpEntry(descriptor) : null,
|
|
362
|
-
);
|
|
363
|
-
}
|
|
362
|
+
await reconcileClaudeMcpContributions(ctx, inputs);
|
|
363
|
+
await reconcileClaudeAgentDefinitionContributions(ctx, inputs);
|
|
364
364
|
|
|
365
|
+
for (const input of inputs) {
|
|
365
366
|
// Guidance sections are managed blocks in CLAUDE.md. Current sections are
|
|
366
367
|
// upserted, then stale keys (content changes, disabled capability) are
|
|
367
368
|
// swept. CLAUDE.md is Claude-exclusive, so no shared-file guard applies.
|
|
@@ -389,6 +390,46 @@ export async function reconcileClaudeContributions(
|
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
392
|
|
|
393
|
+
/** Reconcile only MCP entries without touching Claude settings or guidance. */
|
|
394
|
+
async function reconcileClaudeMcpContributions(
|
|
395
|
+
ctx: SetupContext,
|
|
396
|
+
inputs: CapabilityContributionInput[],
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
for (const input of inputs) {
|
|
399
|
+
for (const descriptor of input.contributions.mcpServers ?? []) {
|
|
400
|
+
await updateClaudeMcpServer(
|
|
401
|
+
getCompanionClaudeMcpConfigPath(ctx.companionPath),
|
|
402
|
+
descriptor.name,
|
|
403
|
+
input.enabled ? toClaudeMcpEntry(descriptor) : null,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Reconcile declared agent definition files under `.claude/agents/`. Runs in
|
|
411
|
+
* both companion and hub scope — an agent definition is fully self-contained
|
|
412
|
+
* (no shared-file merge), so it needs no companion-only surface.
|
|
413
|
+
*/
|
|
414
|
+
async function reconcileClaudeAgentDefinitionContributions(
|
|
415
|
+
ctx: SetupContext,
|
|
416
|
+
inputs: CapabilityContributionInput[],
|
|
417
|
+
): Promise<void> {
|
|
418
|
+
const agentsDir = path.join(ctx.companionPath, ".claude", "agents");
|
|
419
|
+
for (const input of inputs) {
|
|
420
|
+
for (const agent of input.contributions.agentDefinitions ?? []) {
|
|
421
|
+
const agentPath = path.join(agentsDir, `${agent.name}.md`);
|
|
422
|
+
if (input.enabled) {
|
|
423
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
424
|
+
await fs.writeFile(agentPath, agent.content, "utf8");
|
|
425
|
+
} else {
|
|
426
|
+
await fs.rm(agentPath, { force: true });
|
|
427
|
+
await pruneEmptyAncestors(agentsDir, ctx.companionPath);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
392
433
|
// Maintain the companion `.mcp.json` shell. Managed MCP servers are reconciled
|
|
393
434
|
// from declared Capability contributions (and legacy `ctx.mcp` hosting); this
|
|
394
435
|
// only guarantees the file exists with an `mcpServers` map. Loaded at launch
|
|
@@ -688,6 +729,10 @@ export function createClaudePlugin(): ProviderPlugin {
|
|
|
688
729
|
},
|
|
689
730
|
},
|
|
690
731
|
async apply(ctx) {
|
|
732
|
+
if (ctx.scope === "hub") {
|
|
733
|
+
await syncCompanionClaudeSettings(ctx.companionPath, ctx.config);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
691
736
|
await configureClaude(ctx.companionPath);
|
|
692
737
|
await configureClaudeGuidance(ctx.companionPath);
|
|
693
738
|
await teardownLegacyClaudeBin(ctx.companionPath);
|
|
@@ -426,15 +426,16 @@ export async function reconcileOpenCodeContributions(
|
|
|
426
426
|
): Promise<void> {
|
|
427
427
|
const { companionPath } = ctx;
|
|
428
428
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
429
|
+
if (ctx.scope === "hub") {
|
|
430
|
+
await reconcileOpenCodeMcpContributions(ctx, inputs);
|
|
431
|
+
await reconcileOpenCodeAgentDefinitionContributions(ctx, inputs);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await reconcileOpenCodeMcpContributions(ctx, inputs);
|
|
436
|
+
await reconcileOpenCodeAgentDefinitionContributions(ctx, inputs);
|
|
437
437
|
|
|
438
|
+
for (const input of inputs) {
|
|
438
439
|
for (const pluginReference of input.contributions.pluginReferences ?? []) {
|
|
439
440
|
const configFiles = pluginReference.configFiles ?? OPENCODE_CONTRIBUTION_CONFIG_FILES;
|
|
440
441
|
for (const name of configFiles) {
|
|
@@ -479,6 +480,47 @@ export async function reconcileOpenCodeContributions(
|
|
|
479
480
|
}
|
|
480
481
|
}
|
|
481
482
|
|
|
483
|
+
/** Reconcile only MCP entries without touching OpenCode plugins or guidance. */
|
|
484
|
+
async function reconcileOpenCodeMcpContributions(
|
|
485
|
+
ctx: SetupContext,
|
|
486
|
+
inputs: CapabilityContributionInput[],
|
|
487
|
+
): Promise<void> {
|
|
488
|
+
for (const input of inputs) {
|
|
489
|
+
for (const descriptor of input.contributions.mcpServers ?? []) {
|
|
490
|
+
await updateOpenCodeMcpServer(
|
|
491
|
+
getCompanionOpenCodeConfigPath(ctx.companionPath),
|
|
492
|
+
descriptor.name,
|
|
493
|
+
input.enabled ? toOpenCodeMcpEntry(descriptor) : null,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Reconcile declared agent definition files under `.opencode/agents/`. Runs
|
|
501
|
+
* in both companion and hub scope — an agent definition is fully
|
|
502
|
+
* self-contained (no shared-file merge), so it needs no companion-only
|
|
503
|
+
* surface.
|
|
504
|
+
*/
|
|
505
|
+
async function reconcileOpenCodeAgentDefinitionContributions(
|
|
506
|
+
ctx: SetupContext,
|
|
507
|
+
inputs: CapabilityContributionInput[],
|
|
508
|
+
): Promise<void> {
|
|
509
|
+
const agentsDir = path.join(ctx.companionPath, ".opencode", "agents");
|
|
510
|
+
for (const input of inputs) {
|
|
511
|
+
for (const agent of input.contributions.agentDefinitions ?? []) {
|
|
512
|
+
const agentPath = path.join(agentsDir, `${agent.name}.md`);
|
|
513
|
+
if (input.enabled) {
|
|
514
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
515
|
+
await fs.writeFile(agentPath, agent.content, "utf8");
|
|
516
|
+
} else {
|
|
517
|
+
await fs.rm(agentPath, { force: true });
|
|
518
|
+
await pruneEmptyAncestors(agentsDir, ctx.companionPath);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
482
524
|
export function createOpenCodePlugin(): ProviderPlugin {
|
|
483
525
|
return {
|
|
484
526
|
id: "opencode",
|
|
@@ -509,6 +551,7 @@ export function createOpenCodePlugin(): ProviderPlugin {
|
|
|
509
551
|
},
|
|
510
552
|
},
|
|
511
553
|
async apply(ctx: SetupContext) {
|
|
554
|
+
if (ctx.scope === "hub") return;
|
|
512
555
|
await syncOpenCodeRuntimeFiles(
|
|
513
556
|
path.join(getSetupProvidersRoot(), "opencode"),
|
|
514
557
|
ctx.companionPath,
|
package/src/tools/setup.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// oxlint-disable no-await-in-loop
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
|
|
5
4
|
import { FRAMEWORK_NAME } from "../framework";
|
|
@@ -25,7 +24,7 @@ import {
|
|
|
25
24
|
collectManagedGitignoreEntries,
|
|
26
25
|
writeManagedGitignoreBlock,
|
|
27
26
|
} from "./setup/plugins/gitignore";
|
|
28
|
-
import type { PluginRegistration, SetupContext } from "./setup/plugin";
|
|
27
|
+
import type { PluginRegistration, SetupContext, SetupScope } from "./setup/plugin";
|
|
29
28
|
import { getActiveDistribution } from "../distribution";
|
|
30
29
|
import { createUvPlugin } from "./setup/package-managers/uv";
|
|
31
30
|
import type { PackageManagerSetupDeps } from "./setup/package-managers/uv";
|
|
@@ -60,10 +59,11 @@ export async function applySetupCompatibilities(
|
|
|
60
59
|
mode: "setup" | "sync",
|
|
61
60
|
plugins: PluginRegistration[] = getActiveDistribution().registry.getEntries(),
|
|
62
61
|
repoPath?: string,
|
|
62
|
+
scope: SetupScope = config.type === "hub" ? "hub" : "companion",
|
|
63
63
|
): Promise<SetupInstallationOutcome> {
|
|
64
64
|
const plan = buildSetupInstallationPlan(config, plugins);
|
|
65
65
|
const activeProviders = plan.activeProviders;
|
|
66
|
-
const ctx: SetupContext = { companionPath, config, mode, activeProviders, repoPath };
|
|
66
|
+
const ctx: SetupContext = { companionPath, config, mode, activeProviders, repoPath, scope };
|
|
67
67
|
return executeSetupInstallationPlan(ctx, plugins, plan);
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -91,48 +91,6 @@ export async function syncCompanionFiles(
|
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export function mateFolderReadme(): string {
|
|
95
|
-
const n = FRAMEWORK_NAME;
|
|
96
|
-
const packageName =
|
|
97
|
-
getActiveDistribution().config.update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`;
|
|
98
|
-
return [
|
|
99
|
-
`# .${FRAMEWORK_NAME}`,
|
|
100
|
-
``,
|
|
101
|
-
`This directory is managed by the **${FRAMEWORK_NAME}** companion framework (\`${packageName}\`).`,
|
|
102
|
-
``,
|
|
103
|
-
`The ${FRAMEWORK_NAME} framework keeps your AI agent's companion artifacts separate from the code it works on.`,
|
|
104
|
-
`Specs, notes, and agent config live here; code stays in the linked working repository.`,
|
|
105
|
-
``,
|
|
106
|
-
`## Common commands`,
|
|
107
|
-
``,
|
|
108
|
-
`| Command | Description |`,
|
|
109
|
-
`|---|---|`,
|
|
110
|
-
`| \`${n} companion setup\` | Initialize or re-configure this companion |`,
|
|
111
|
-
`| \`${n} companion link\` | Link a working repository to a companion |`,
|
|
112
|
-
`| \`${n} companion list\` | List linked repositories for the active working repo context |`,
|
|
113
|
-
`| \`${n} companion open\` | Inject the resolved companion into the current editor window |`,
|
|
114
|
-
`| \`${n} claude\` / \`${n} opencode\` | Launch an allowed agent from a linked working repository |`,
|
|
115
|
-
`| \`${n} doctor\` | Check current link state, installed tools, and active capabilities |`,
|
|
116
|
-
``,
|
|
117
|
-
`Run \`${n} claude\` or \`${n} opencode\` from any linked working repository directory.`,
|
|
118
|
-
``,
|
|
119
|
-
`## Configuration`,
|
|
120
|
-
``,
|
|
121
|
-
`Edit \`.${FRAMEWORK_NAME}/config/framework.yaml\` to configure:`,
|
|
122
|
-
``,
|
|
123
|
-
`- **allowedAgents** — agents permitted to launch from linked repositories`,
|
|
124
|
-
`- **capabilities** — skill and CLI tool capabilities (e.g. react-doctor, openspec, tokensave, headroom, rtk)`,
|
|
125
|
-
`- **git** — set to \`auto\` to synchronize the companion before agent launches`,
|
|
126
|
-
``,
|
|
127
|
-
].join("\n");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function writeMateReadme(companionPath: string): Promise<void> {
|
|
131
|
-
const readmePath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "README.md");
|
|
132
|
-
await fs.mkdir(path.dirname(readmePath), { recursive: true });
|
|
133
|
-
await fs.writeFile(readmePath, mateFolderReadme(), "utf8");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
94
|
export const setupToolDeps = {
|
|
137
95
|
executeSetup: (input: SetupInput) => executeSetup(input),
|
|
138
96
|
};
|
|
@@ -160,14 +118,6 @@ export async function executeSetup(
|
|
|
160
118
|
deps.configStore ??
|
|
161
119
|
new ConfigStore(path.join(cwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
|
|
162
120
|
const config = mergeWithDefaults(await configStore.load());
|
|
163
|
-
// Hubs are never companions: no setup entry path (CLI, wizard, or the
|
|
164
|
-
// setup framework tool) may write agent guidance into a hub root.
|
|
165
|
-
if (config.type === "hub") {
|
|
166
|
-
throw new ConfigError(
|
|
167
|
-
`A companion hub cannot be set up as a companion: ${cwd}. Use \`${FRAMEWORK_NAME} hub\` commands to manage the hub.`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
121
|
if (input.allowedAgents !== undefined) {
|
|
172
122
|
config.allowedAgents = [...new Set(input.allowedAgents)];
|
|
173
123
|
}
|
|
@@ -202,12 +152,13 @@ export async function executeSetup(
|
|
|
202
152
|
await hydrateDynamicPlugins({ companionPath });
|
|
203
153
|
}
|
|
204
154
|
await applySetupCompatibilities(companionPath, config, "setup");
|
|
205
|
-
await invalidateInstallState({
|
|
155
|
+
await invalidateInstallState({
|
|
156
|
+
kind: config.type === "hub" ? "hub" : "companion",
|
|
157
|
+
companionPath,
|
|
158
|
+
});
|
|
206
159
|
|
|
207
160
|
await globalConfigStore.register(companionPath);
|
|
208
161
|
|
|
209
|
-
await writeMateReadme(companionPath);
|
|
210
|
-
|
|
211
162
|
// Setup no longer fans out to linked repos: capabilities that touch a working repo
|
|
212
163
|
// (companion files on launch, the tokensave/graphify graph via `mate cap index`)
|
|
213
164
|
// are applied in that repo's own context, not eagerly from here.
|