@uniqbit/mate-core 0.18.3-canary.1 → 0.18.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/hooks.json +10 -1
- package/claude-plugin/hooks/session-banner.mjs +4 -3
- package/claude-plugin/hooks/session-prepare.mjs +9 -0
- package/opencode-plugin/src/add-dir.ts +10 -76
- package/opencode-plugin/src/audience-layer.ts +91 -0
- package/opencode-plugin/src/companion.ts +70 -18
- package/opencode-plugin/src/launch-environment.ts +29 -0
- package/opencode-plugin/src/server.ts +60 -8
- package/package.json +3 -3
- package/src/cli/commands/artifact/finish/openspec.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +60 -21
- package/src/cli/commands/cap/index-cmd.ts +2 -11
- package/src/cli/commands/companion/link.ts +16 -11
- package/src/cli/commands/companion/list.ts +4 -5
- package/src/cli/commands/companion/open.ts +4 -5
- package/src/cli/commands/companion/tui.ts +1 -1
- package/src/cli/commands/doctor.ts +87 -5
- package/src/cli/commands/install.ts +8 -0
- package/src/cli/commands/report/renderer.ts +9 -19
- package/src/cli/commands/session/prepare.ts +119 -0
- package/src/cli/commands/shared/companion-selection.ts +2 -2
- package/src/cli/commands/studio/reports.ts +0 -1
- package/src/cli/commands/studio/server.ts +6 -2
- package/src/cli/commands/studio/terminal-launch.ts +16 -7
- package/src/cli/commands/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +79 -60
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +5 -2
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-guidance.ts +20 -2
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +45 -9
- package/src/lib/orchestrator/adapters/claude.ts +241 -30
- package/src/lib/orchestrator/adapters/opencode.ts +72 -34
- package/src/lib/orchestrator/companion-git-sync.ts +28 -1
- package/src/lib/orchestrator/companion-hub.ts +2 -1
- package/src/lib/orchestrator/config-store.ts +83 -1
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/openspec-store.ts +202 -0
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +126 -41
- package/src/lib/orchestrator/projection-entries.ts +124 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
- package/src/lib/orchestrator/projection-openspec-store.ts +164 -0
- package/src/lib/orchestrator/projection-record.ts +7 -1
- package/src/lib/orchestrator/projection-runtime-documents.ts +83 -130
- package/src/lib/orchestrator/projection-types.ts +47 -34
- package/src/lib/orchestrator/repository-link.ts +473 -0
- package/src/lib/orchestrator/root-context.ts +6 -2
- package/src/lib/orchestrator/session-prepare.ts +503 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/working-repo-projection.ts +73 -160
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +222 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +30 -135
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- package/src/opencode/projected-guidance.ts +6 -13
- package/src/opencode/session-notes.ts +76 -0
- package/src/opencode/session-prepare.ts +139 -0
- package/src/opencode/tui.tsx +56 -9
- package/src/runtime/audience-layer.ts +84 -0
- package/src/runtime/claude-delivery-plan.ts +73 -0
- package/src/runtime/claude-hook-group.ts +21 -0
- package/src/runtime/companion-lock.ts +121 -0
- package/src/runtime/companion-sync.ts +66 -9
- package/src/runtime/guidance.ts +17 -0
- package/src/runtime/opencode-audience-layer.ts +176 -0
- package/src/runtime/policy.ts +14 -1
- package/src/runtime/projection.ts +1 -1
- package/src/runtime/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +352 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +94 -0
- package/src/runtime/write-atomic.ts +27 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +5 -0
- package/src/templates/capabilities/react-doctor/mate-react-doctor/SKILL.md +84 -0
- package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md +18 -15
- package/src/templates/mate-skills/agents/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +1 -1
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +1 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +850 -499
- package/src/tools/setup/audience-isolation.fixture.ts +325 -0
- package/src/tools/setup/audience-layer.ts +207 -0
- package/src/tools/setup/audience-registrations.ts +52 -0
- package/src/tools/setup/capabilities/openspec.ts +9 -0
- package/src/tools/setup/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/capabilities/tokensave.ts +11 -0
- package/src/tools/setup/contribution-ledger.ts +207 -0
- package/src/tools/setup/dynamic-plugins/audiences.ts +89 -9
- package/src/tools/setup/dynamic-plugins/declarations.ts +3 -2
- package/src/tools/setup/dynamic-plugins/hydrate.ts +8 -5
- package/src/tools/setup/engine.ts +152 -10
- package/src/tools/setup/isolation-diagnostics.ts +139 -0
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/plugin.ts +19 -4
- package/src/tools/setup/plugins/gitignore.ts +7 -1
- package/src/tools/setup/providers/claude-format.ts +11 -18
- package/src/tools/setup/providers/claude-plugin-hooks.ts +61 -24
- package/src/tools/setup/providers/claude.ts +50 -8
- package/src/tools/setup/providers/opencode.ts +10 -0
- package/src/tools/setup/registry.ts +24 -2
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/surface-target.ts +13 -4
- package/src/tools/setup/upgrade-sweep.ts +153 -0
- package/src/tools/setup/working-repo-cleanup.ts +2 -0
- package/src/tools/setup/working-repo-local-state.ts +91 -14
- package/src/tools/setup.ts +11 -8
- package/wrappers/bin/openspec +42 -1
- package/src/lib/orchestrator/companion-registry-reader.ts +0 -37
- package/src/lib/orchestrator/companion-resolver.ts +0 -179
- package/src/lib/orchestrator/companion-store.ts +0 -94
- package/src/lib/orchestrator/repo-local-registry.ts +0 -143
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +0 -144
- package/src/templates/capabilities/react-doctor/skill/SKILL.md +0 -51
- package/src/templates/report-assets/README.md +0 -32
- package/src/templates/report-assets/mermaid.LICENSE +0 -21
- package/src/templates/report-assets/mermaid.min.js +0 -4376
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
upsertManagedBlock,
|
|
12
12
|
} from "../context-services";
|
|
13
13
|
import type { CapabilityContributionInput, ProviderPlugin, SetupContext } from "../plugin";
|
|
14
|
-
import { surfaceRoot } from "../surface-target";
|
|
14
|
+
import { projectionTarget, surfaceRoot } from "../surface-target";
|
|
15
15
|
import { mergeDir, pruneEmptyAncestors } from "../utils";
|
|
16
16
|
import { stripSectionFromFile, type RemoveHeadingSectionOptions } from "./agent-file-sections";
|
|
17
17
|
import { patchSkillTreeMarkdownFiles } from "./skill-tree";
|
|
@@ -89,6 +89,10 @@ function getBaseManagedPermissionEntries(companionPath: string): string[] {
|
|
|
89
89
|
const LEGACY_MANAGED_PERMISSION_ENTRIES = [
|
|
90
90
|
"Bash($MATE_COMPANION_BIN_PATH/openspec:*)",
|
|
91
91
|
"Bash($MATE_COMPANION_BIN_PATH/graphify:*)",
|
|
92
|
+
// React Doctor's pre-seeds before it moved to the pinned mate-react-doctor skill.
|
|
93
|
+
"Skill(react-doctor)",
|
|
94
|
+
"Bash(npx react-doctor:*)",
|
|
95
|
+
"Bash(npx react-doctor@latest *)",
|
|
92
96
|
];
|
|
93
97
|
|
|
94
98
|
function getAllManagedPermissionEntries(companionPath: string): Set<string> {
|
|
@@ -102,16 +106,12 @@ function getAllManagedPermissionEntries(companionPath: string): Set<string> {
|
|
|
102
106
|
]);
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
// single source of truth for Mate-managed Claude hooks, permissions, and MCP
|
|
107
|
-
// servers; it is loaded at launch via `claude --settings`.
|
|
109
|
+
/** Companion-owned Claude settings: sole source of Mate-managed hooks, permissions and MCP servers. */
|
|
108
110
|
export function getCompanionClaudeSettingsPath(companionPath: string): string {
|
|
109
111
|
return path.join(companionPath, ".claude", "settings.local.json");
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
// MCP servers here and loads it at launch via `claude --mcp-config`, keeping
|
|
114
|
-
// the MCP surface out of the working repo and out of settings.local.json.
|
|
114
|
+
/** Companion-owned `.mcp.json`; loaded via `--mcp-config` only by a companion-scoped launch. */
|
|
115
115
|
export function getCompanionClaudeMcpConfigPath(companionPath: string): string {
|
|
116
116
|
return path.join(companionPath, ".mcp.json");
|
|
117
117
|
}
|
|
@@ -147,6 +147,40 @@ export function renderManagedClaudeMcpServers(
|
|
|
147
147
|
return servers;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Enabled Capabilities' Claude hook groups per event, in declared order, as a
|
|
152
|
+
* value: what a Claude wrap carries into the Working Repository.
|
|
153
|
+
*/
|
|
154
|
+
export function renderManagedClaudeHookGroups(
|
|
155
|
+
contributions: CapabilityContributionInput[] = [],
|
|
156
|
+
): Record<string, ClaudeHookGroup[]> {
|
|
157
|
+
const hooks: Record<string, ClaudeHookGroup[]> = {};
|
|
158
|
+
for (const input of contributions) {
|
|
159
|
+
if (!input.enabled) continue;
|
|
160
|
+
for (const hook of input.contributions.hookGroups ?? []) {
|
|
161
|
+
(hooks[hook.event] ??= []).push(hook.group);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return hooks;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A layer's Claude settings: the enabled inputs' hook groups and permission
|
|
169
|
+
* entries, nothing of Mate's own. The launch merges them over its base settings.
|
|
170
|
+
*/
|
|
171
|
+
export function renderLayerClaudeSettings(
|
|
172
|
+
contributions: CapabilityContributionInput[] = [],
|
|
173
|
+
): ClaudeSettings {
|
|
174
|
+
const hooks = renderManagedClaudeHookGroups(contributions);
|
|
175
|
+
const allow = contributions
|
|
176
|
+
.filter((input) => input.enabled)
|
|
177
|
+
.flatMap((input) => input.contributions.permissionEntries ?? []);
|
|
178
|
+
return {
|
|
179
|
+
...(Object.keys(hooks).length > 0 ? { hooks } : {}),
|
|
180
|
+
...(allow.length > 0 ? { permissions: { allow } } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
150
184
|
// Reconcile the Mate-managed Claude settings shape (hooks, permissions, MCP
|
|
151
185
|
// servers) on top of whatever already exists in the settings document. Managed
|
|
152
186
|
// groups/entries always lead their arrays so the emitted shape stays stable
|
|
@@ -270,7 +304,15 @@ export async function reconcileClaudeContributions(
|
|
|
270
304
|
// The settings sync (re)creates the managed settings document, so it only
|
|
271
305
|
// runs while the Claude runtime is active; a deactivated runtime's pass is
|
|
272
306
|
// teardown-only and must not resurrect files the provider teardown removed.
|
|
273
|
-
|
|
307
|
+
// A layer carries its plugins' hooks and permission entries only, for the
|
|
308
|
+
// launch to merge into its one `--settings` file.
|
|
309
|
+
if (projectionTarget(ctx) === "audience-layer") {
|
|
310
|
+
await writeClaudeSettings(
|
|
311
|
+
getCompanionClaudeSettingsPath(root),
|
|
312
|
+
renderLayerClaudeSettings(inputs),
|
|
313
|
+
);
|
|
314
|
+
await syncCompanionClaudeMcpConfig(root);
|
|
315
|
+
} else if (ctx.activeProviders.includes("claude")) {
|
|
274
316
|
await syncCompanionClaudeSettings(root, config, inputs);
|
|
275
317
|
}
|
|
276
318
|
|
|
@@ -416,6 +416,16 @@ export async function removeOpenCodeForeignPluginReferences(
|
|
|
416
416
|
|
|
417
417
|
const OPENCODE_CONTRIBUTION_CONFIG_FILES = ["opencode.json"];
|
|
418
418
|
|
|
419
|
+
/**
|
|
420
|
+
* The surface's reader for an audience layer it wrote: what the Mate OpenCode
|
|
421
|
+
* plugin applies. Defined under `runtime/` so the import-isolated plugin can
|
|
422
|
+
* reach it; this surface owns the format it reads.
|
|
423
|
+
*/
|
|
424
|
+
export {
|
|
425
|
+
readOpenCodeAudienceLayer,
|
|
426
|
+
type OpenCodeLayerContents,
|
|
427
|
+
} from "../../../runtime/opencode-audience-layer";
|
|
428
|
+
|
|
419
429
|
export async function reconcileOpenCodeContributions(
|
|
420
430
|
ctx: SetupContext,
|
|
421
431
|
inputs: CapabilityContributionInput[],
|
|
@@ -3,13 +3,22 @@ import type { Plugin, PluginPolicy, PluginRegistration } from "./plugin";
|
|
|
3
3
|
export interface NormalizedRegistration {
|
|
4
4
|
plugin: Plugin;
|
|
5
5
|
policy: PluginPolicy;
|
|
6
|
+
/** The audience that declared the plugin; `null` for base. */
|
|
7
|
+
audience: string | null;
|
|
8
|
+
/** The declared package a dynamic plugin was loaded from; absent for compiled-in plugins. */
|
|
9
|
+
packageName?: string;
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
export function normalizeRegistration(entry: PluginRegistration): NormalizedRegistration {
|
|
9
13
|
if ("plugin" in entry && "policy" in entry) {
|
|
10
|
-
return {
|
|
14
|
+
return {
|
|
15
|
+
plugin: entry.plugin,
|
|
16
|
+
policy: entry.policy,
|
|
17
|
+
audience: entry.audience ?? null,
|
|
18
|
+
...(entry.packageName ? { packageName: entry.packageName } : {}),
|
|
19
|
+
};
|
|
11
20
|
}
|
|
12
|
-
return { plugin: entry, policy: entry.defaultSelected ? "default" : "optional" };
|
|
21
|
+
return { plugin: entry, policy: entry.defaultSelected ? "default" : "optional", audience: null };
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
export class PluginRegistry {
|
|
@@ -32,6 +41,19 @@ export class PluginRegistry {
|
|
|
32
41
|
return this.entries.map((entry) => ({ ...entry }));
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Compiled-in plugins and top-level declarations: the only registrations a
|
|
46
|
+
* pass that writes a persisted surface may reconcile.
|
|
47
|
+
*/
|
|
48
|
+
baseRegistrations(): NormalizedRegistration[] {
|
|
49
|
+
return this.getEntries().filter((entry) => entry.audience === null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The registrations one audience declared; only its launch layer renders them. */
|
|
53
|
+
audienceRegistrations(audience: string): NormalizedRegistration[] {
|
|
54
|
+
return this.getEntries().filter((entry) => entry.audience === audience);
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
getPolicy(pluginId: string): PluginPolicy | undefined {
|
|
36
58
|
return this.entries.find((entry) => entry.plugin.id === pluginId)?.policy;
|
|
37
59
|
}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { runtimeOpenCodePluginRoot } from "../../runtime/runtime-directory";
|
|
1
4
|
import {
|
|
2
5
|
CLAUDE_LOCAL_CONFIG_DOCUMENT,
|
|
3
6
|
CLAUDE_SETTINGS_DOCUMENT,
|
|
4
7
|
OPENCODE_CONFIG_DOCUMENT,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../../lib/orchestrator/projection-types";
|
|
10
|
-
import { getOpenCodePluginRoot } from "../../lib/package-paths";
|
|
8
|
+
OPENCODE_MCP_SERVERS_AT,
|
|
9
|
+
type ManagedRegion,
|
|
10
|
+
type RenderedRuntimeDocument,
|
|
11
|
+
} from "../../runtime/runtime-document-manifest";
|
|
11
12
|
import type { CapabilityContributionInput, SetupContext } from "./plugin";
|
|
12
13
|
import {
|
|
13
14
|
mateBannerHookGroups,
|
|
14
15
|
mateGuardHookGroups,
|
|
15
16
|
mateGuidanceHookGroups,
|
|
17
|
+
mateSessionPrepareHookGroups,
|
|
16
18
|
} from "./providers/claude-plugin-hooks";
|
|
17
19
|
import {
|
|
20
|
+
renderManagedClaudeHookGroups,
|
|
18
21
|
renderManagedClaudeMcpServers,
|
|
19
22
|
renderManagedClaudePermissionEntries,
|
|
20
23
|
} from "./providers/claude";
|
|
@@ -22,6 +25,7 @@ import {
|
|
|
22
25
|
renderCompanionExternalDirectoryPermissions,
|
|
23
26
|
renderManagedOpenCodeMcpServers,
|
|
24
27
|
} from "./providers/opencode";
|
|
28
|
+
import { isRecord, readOpenCodeConfig } from "./providers/opencode-format";
|
|
25
29
|
import { projectionRepoRoot } from "./surface-target";
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -47,40 +51,39 @@ function document(documentPath: string, regions: ManagedRegion[]): RenderedRunti
|
|
|
47
51
|
/**
|
|
48
52
|
* The Mate plugin root is what makes an Unmanaged OpenCode session load the
|
|
49
53
|
* companion guidance at all: OpenCode has no config channel for a generated
|
|
50
|
-
* system prompt, so the plugin is the only injection point
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* A managed launch lists the same root in its `OPENCODE_CONFIG_CONTENT`
|
|
55
|
-
* overlay; OpenCode dedupes identical plugin paths across config sources.
|
|
54
|
+
* system prompt, so the plugin is the only injection point. It is named through
|
|
55
|
+
* the runtime directory, so a reinstall does not strand a wrapped repository.
|
|
56
|
+
* The managed launch carries no overlay of its own, so this document is the one
|
|
57
|
+
* place every OpenCode session in the repository loads it from.
|
|
56
58
|
*/
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* tool input, the guidance hook because it yields nothing under a launch
|
|
65
|
-
* environment, and the banner because the copy carried here is flagged
|
|
66
|
-
* `--projected` and defers to the one the launch's own plugin prints.
|
|
67
|
-
*
|
|
68
|
-
* `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD` is what a managed launch sets
|
|
69
|
-
* beside `--add-dir`. Without it the companion directories this document
|
|
70
|
-
* already permits are reachable but their `CLAUDE.md` is not loaded, so the
|
|
71
|
-
* permission alone delivers no guidance.
|
|
61
|
+
* Loaded by a managed session beside the launch's plugin. Mate hooks here run
|
|
62
|
+
* twice there, inertly: guard is input-pure, guidance/prepare yield under a
|
|
63
|
+
* launch env, banner is `--projected`. Capability groups run once: the launch
|
|
64
|
+
* omits them from `--settings`. A launch passes no `--add-dir`; it relies on
|
|
65
|
+
* `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD` for companion `CLAUDE.md`.
|
|
72
66
|
*/
|
|
73
67
|
function claudeSettingsRegions(
|
|
74
68
|
ctx: SetupContext,
|
|
75
69
|
inputs: CapabilityContributionInput[],
|
|
76
70
|
): ManagedRegion[] {
|
|
71
|
+
const capabilityHooks = renderManagedClaudeHookGroups(inputs);
|
|
72
|
+
const mateHooks: Record<string, unknown[]> = {
|
|
73
|
+
PreToolUse: mateGuardHookGroups(),
|
|
74
|
+
SessionStart: [
|
|
75
|
+
...mateBannerHookGroups(),
|
|
76
|
+
...mateGuidanceHookGroups(),
|
|
77
|
+
...mateSessionPrepareHookGroups(),
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
const events = [...new Set([...Object.keys(mateHooks), ...Object.keys(capabilityHooks)])];
|
|
77
81
|
return [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
at: ["hooks", "SessionStart"],
|
|
82
|
+
...events.map((event): ManagedRegion => ({
|
|
83
|
+
at: ["hooks", event],
|
|
81
84
|
kind: "list",
|
|
82
|
-
values: [...
|
|
83
|
-
},
|
|
85
|
+
values: [...(mateHooks[event] ?? []), ...(capabilityHooks[event] ?? [])],
|
|
86
|
+
})),
|
|
84
87
|
/**
|
|
85
88
|
* `stripPrefix` heals a repository an older Mate wrapped. That version wrote
|
|
86
89
|
* the whole launch environment here, and Claude Code applies `env` to the
|
|
@@ -152,10 +155,10 @@ export function renderDocumentsForTarget(
|
|
|
152
155
|
{
|
|
153
156
|
at: ["plugins"],
|
|
154
157
|
kind: "list",
|
|
155
|
-
values: [
|
|
158
|
+
values: [runtimeOpenCodePluginRoot()],
|
|
156
159
|
},
|
|
157
160
|
{
|
|
158
|
-
at: [
|
|
161
|
+
at: [...OPENCODE_MCP_SERVERS_AT],
|
|
159
162
|
kind: "map",
|
|
160
163
|
entries: renderManagedOpenCodeMcpServers(inputs),
|
|
161
164
|
},
|
|
@@ -164,8 +167,125 @@ export function renderDocumentsForTarget(
|
|
|
164
167
|
kind: "list",
|
|
165
168
|
values: renderCompanionExternalDirectoryPermissions(ctx.companionPath),
|
|
166
169
|
},
|
|
170
|
+
{
|
|
171
|
+
at: ["skills"],
|
|
172
|
+
kind: "list",
|
|
173
|
+
values: [path.join(ctx.companionPath, ".agents", "skills")],
|
|
174
|
+
},
|
|
175
|
+
{ at: ["references"], kind: "map", entries: { mate: ctx.companionPath } },
|
|
167
176
|
]),
|
|
168
177
|
);
|
|
169
178
|
}
|
|
170
179
|
return documents;
|
|
171
180
|
}
|
|
181
|
+
|
|
182
|
+
/** Keys Mate renders its own regions for; the companion's values join those regions. */
|
|
183
|
+
const MATE_OPENCODE_KEYS = new Set(["plugins", "permissions", "skills", "mcp", "references"]);
|
|
184
|
+
|
|
185
|
+
function relativePathValues(value: unknown, at: string[]): string[] {
|
|
186
|
+
if (typeof value === "string") {
|
|
187
|
+
return value.startsWith("./") || value.startsWith("../") ? [at.join(".")] : [];
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(value)) {
|
|
190
|
+
return value.flatMap((entry, index) => relativePathValues(entry, [...at, String(index)]));
|
|
191
|
+
}
|
|
192
|
+
if (isRecord(value)) {
|
|
193
|
+
return Object.entries(value).flatMap(([key, entry]) => relativePathValues(entry, [...at, key]));
|
|
194
|
+
}
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Replaces the region at `at` with what `merge` builds from it, or appends one. */
|
|
199
|
+
function upsertRegion(
|
|
200
|
+
regions: ManagedRegion[],
|
|
201
|
+
at: string[],
|
|
202
|
+
merge: (found: ManagedRegion | undefined) => ManagedRegion,
|
|
203
|
+
): void {
|
|
204
|
+
const key = JSON.stringify(at);
|
|
205
|
+
const index = regions.findIndex((region) => JSON.stringify(region.at) === key);
|
|
206
|
+
if (index === -1) regions.push(merge(undefined));
|
|
207
|
+
else regions[index] = merge(regions[index]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Mate's own values stay; companion values it does not already list go first. */
|
|
211
|
+
function mergeListRegion(regions: ManagedRegion[], at: string[], extra: unknown[]): void {
|
|
212
|
+
upsertRegion(regions, at, (found) => {
|
|
213
|
+
const own = found?.kind === "list" ? found.values : [];
|
|
214
|
+
const seen = new Set(own.map((value) => JSON.stringify(value)));
|
|
215
|
+
const values = [...extra.filter((value) => !seen.has(JSON.stringify(value))), ...own];
|
|
216
|
+
return { at, kind: "list", values };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Mate's own entries win over companion entries of the same name. */
|
|
221
|
+
function mergeMapRegion(
|
|
222
|
+
regions: ManagedRegion[],
|
|
223
|
+
at: string[],
|
|
224
|
+
extra: Record<string, unknown>,
|
|
225
|
+
): void {
|
|
226
|
+
upsertRegion(regions, at, (found) => {
|
|
227
|
+
const own = found?.kind === "map" ? found.entries : {};
|
|
228
|
+
return { at, kind: "map", entries: { ...extra, ...own } };
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Relative reference paths resolved against the companion's `.opencode/`. */
|
|
233
|
+
function resolveReferences(
|
|
234
|
+
references: Record<string, unknown>,
|
|
235
|
+
companionDir: string,
|
|
236
|
+
): Record<string, unknown> {
|
|
237
|
+
return Object.fromEntries(
|
|
238
|
+
Object.entries(references).map(([name, reference]) => [
|
|
239
|
+
name,
|
|
240
|
+
typeof reference === "string" && !path.isAbsolute(reference)
|
|
241
|
+
? path.resolve(companionDir, reference)
|
|
242
|
+
: reference,
|
|
243
|
+
]),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Folds the companion's `.opencode/opencode.json` into the wrapped document.
|
|
249
|
+
* Mate's regions beat companion values; other top-level keys yield to the
|
|
250
|
+
* repository's. `references` resolve against the companion's `.opencode/`;
|
|
251
|
+
* other relative paths are reported, not rewritten.
|
|
252
|
+
*/
|
|
253
|
+
export async function mergeCompanionOpenCodeConfig(
|
|
254
|
+
companionPath: string,
|
|
255
|
+
documents: RenderedRuntimeDocument[],
|
|
256
|
+
): Promise<{ documents: RenderedRuntimeDocument[]; warnings: string[] }> {
|
|
257
|
+
const index = documents.findIndex((document) => document.path === OPENCODE_CONFIG_DOCUMENT);
|
|
258
|
+
const companionConfigPath = path.join(companionPath, ".opencode", "opencode.json");
|
|
259
|
+
const companion = index === -1 ? null : await readOpenCodeConfig(companionConfigPath);
|
|
260
|
+
if (!companion?.present) return { documents, warnings: [] };
|
|
261
|
+
|
|
262
|
+
const regions = [...documents[index]!.regions] as ManagedRegion[];
|
|
263
|
+
const warnings: string[] = [];
|
|
264
|
+
|
|
265
|
+
const companionDir = path.dirname(companionConfigPath);
|
|
266
|
+
for (const [key, value] of Object.entries(companion.config)) {
|
|
267
|
+
if (key === "$schema") continue;
|
|
268
|
+
if (key === "references" && isRecord(value)) {
|
|
269
|
+
mergeMapRegion(regions, ["references"], resolveReferences(value, companionDir));
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
for (const at of relativePathValues(value, [key])) {
|
|
273
|
+
warnings.push(
|
|
274
|
+
`${companionConfigPath}: ${at} is a relative path, which resolves against the working repository once merged into ${OPENCODE_CONFIG_DOCUMENT}; use an absolute path.`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (MATE_OPENCODE_KEYS.has(key)) {
|
|
278
|
+
if (key === "mcp" && isRecord(value) && isRecord(value.servers)) {
|
|
279
|
+
mergeMapRegion(regions, [...OPENCODE_MCP_SERVERS_AT], value.servers);
|
|
280
|
+
} else if (Array.isArray(value)) {
|
|
281
|
+
mergeListRegion(regions, [key], value);
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
regions.push({ at: [key], kind: "value", value, yields: true });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const next = [...documents];
|
|
289
|
+
next[index] = { path: OPENCODE_CONFIG_DOCUMENT, regions };
|
|
290
|
+
return { documents: next, warnings };
|
|
291
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import { audienceLayerDir } from "../../runtime/audience-layer";
|
|
3
4
|
import type { ProjectionTarget, SetupContext } from "./plugin";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -14,6 +15,9 @@ export function projectionTarget(ctx: SetupContext): ProjectionTarget {
|
|
|
14
15
|
"a working-target reconcile needs a Working Repository in scope; none was supplied",
|
|
15
16
|
);
|
|
16
17
|
}
|
|
18
|
+
if (target === "audience-layer" && !ctx.audience) {
|
|
19
|
+
throw new Error("an audience-layer reconcile needs an audience; none was supplied");
|
|
20
|
+
}
|
|
17
21
|
return target;
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -38,13 +42,18 @@ export function projectionRepoRoot(ctx: SetupContext): string {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
|
-
* The root a Runtime Surface writes into
|
|
42
|
-
*
|
|
43
|
-
*
|
|
45
|
+
* The root a Runtime Surface writes into: the companion, or one audience's
|
|
46
|
+
* layer outside every repository. Never a Working Repository: a working-target
|
|
47
|
+
* pass renders its documents for the Projection Root to place, so no Runtime
|
|
48
|
+
* Surface ever computes a path inside one.
|
|
44
49
|
*/
|
|
45
50
|
export function surfaceRoot(ctx: SetupContext): string {
|
|
46
|
-
|
|
51
|
+
const target = projectionTarget(ctx);
|
|
52
|
+
if (target === "working") {
|
|
47
53
|
throw new Error("a working-target reconcile renders its documents; it writes no surface root");
|
|
48
54
|
}
|
|
55
|
+
if (target === "audience-layer") {
|
|
56
|
+
return ctx.layerRoot ?? audienceLayerDir(ctx.companionPath, ctx.audience!);
|
|
57
|
+
}
|
|
49
58
|
return ctx.companionPath;
|
|
50
59
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
6
|
+
import { removeManagedBlocksForPlugin } from "./context-services";
|
|
7
|
+
import {
|
|
8
|
+
audiencePluginDeclarations,
|
|
9
|
+
declaredAudiences,
|
|
10
|
+
effectivePluginDeclarations,
|
|
11
|
+
readFrameworkRaw,
|
|
12
|
+
} from "./dynamic-plugins/audiences";
|
|
13
|
+
import { validateDeclaration } from "./dynamic-plugins/declarations";
|
|
14
|
+
import { loadDynamicPlugin, type DynamicPluginLoadDeps } from "./dynamic-plugins/loader";
|
|
15
|
+
import type { CapabilityContributionInput, CapabilityPlugin, Plugin, SetupContext } from "./plugin";
|
|
16
|
+
import type { NormalizedRegistration } from "./registry";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Upgrade Sweep: removes what audience plugins leaked into a companion before
|
|
20
|
+
* the contribution ledger existed. Runs once, when a persisted pass finds no
|
|
21
|
+
* ledger, and again only for the packages it could not load then.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The companion files Mate's managed guidance blocks live in. */
|
|
25
|
+
const GUIDANCE_FILES = ["CLAUDE.md", "AGENTS.md", path.join(".opencode", "AGENTS.md")];
|
|
26
|
+
|
|
27
|
+
const MANAGED_BLOCK_START = new RegExp(
|
|
28
|
+
`<!-- ${FRAMEWORK_NAME}:managed:([^\\n]+):[0-9a-f]+ start -->`,
|
|
29
|
+
"g",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/** Plugin ids owning a managed guidance block in `text`. */
|
|
33
|
+
export function managedBlockOwners(text: string): string[] {
|
|
34
|
+
return [...new Set([...text.matchAll(MANAGED_BLOCK_START)].map((match) => match[1]!))];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `relative path → owners` for every managed guidance block under `root`. */
|
|
38
|
+
export async function managedGuidanceOwners(root: string): Promise<Map<string, string[]>> {
|
|
39
|
+
const owners = new Map<string, string[]>();
|
|
40
|
+
for (const file of GUIDANCE_FILES) {
|
|
41
|
+
const text = await fs.readFile(path.join(root, file), "utf8").catch(() => null);
|
|
42
|
+
if (text === null) continue;
|
|
43
|
+
const found = managedBlockOwners(text);
|
|
44
|
+
if (found.length > 0) owners.set(file, found);
|
|
45
|
+
}
|
|
46
|
+
return owners;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Guidance blocks name their owner, so this needs no plugin loaded: every
|
|
51
|
+
* block whose owner is not a base registration goes.
|
|
52
|
+
*/
|
|
53
|
+
export async function removeForeignGuidanceBlocks(
|
|
54
|
+
root: string,
|
|
55
|
+
baseIds: ReadonlySet<string>,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
for (const [file, owners] of await managedGuidanceOwners(root)) {
|
|
58
|
+
for (const owner of owners) {
|
|
59
|
+
if (baseIds.has(owner)) continue;
|
|
60
|
+
await removeManagedBlocksForPlugin(path.join(root, file), FRAMEWORK_NAME, owner);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AudienceSweep {
|
|
66
|
+
/** Disabled inputs for every swept plugin, per runtime. */
|
|
67
|
+
teardown: Map<string, CapabilityContributionInput[]>;
|
|
68
|
+
/** Packages that could not be loaded; their non-guidance entries stay until a retry. */
|
|
69
|
+
unswept: string[];
|
|
70
|
+
warnings: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AudienceSweepOptions {
|
|
74
|
+
/** Every registration of the process; an audience plugin already loaded is reused, not reloaded. */
|
|
75
|
+
registrations: readonly NormalizedRegistration[];
|
|
76
|
+
/** Restrict the sweep to these packages; every audience declaration when absent. */
|
|
77
|
+
packages?: ReadonlySet<string>;
|
|
78
|
+
deps?: DynamicPluginLoadDeps;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One distinct package per audience declaration, base packages excluded. */
|
|
82
|
+
function audiencePackages(raw: unknown): Map<string, ReturnType<typeof validateDeclaration>> {
|
|
83
|
+
const basePackages = new Set(
|
|
84
|
+
effectivePluginDeclarations(raw, null).flatMap((entry) => {
|
|
85
|
+
const { declaration } = validateDeclaration(entry);
|
|
86
|
+
return declaration ? [declaration.package] : [];
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
const found = new Map<string, ReturnType<typeof validateDeclaration>>();
|
|
90
|
+
for (const audience of declaredAudiences(raw)) {
|
|
91
|
+
for (const entry of audiencePluginDeclarations(raw, audience)) {
|
|
92
|
+
const validated = validateDeclaration(entry);
|
|
93
|
+
const name = validated.declaration?.package;
|
|
94
|
+
if (!name || basePackages.has(name) || found.has(name)) continue;
|
|
95
|
+
found.set(name, validated);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Collects the declarations of every audience plugin as disabled inputs:
|
|
103
|
+
* loaded for its declarations only — never activated, no lifecycle hook run.
|
|
104
|
+
* An unloadable plugin is reported, never fatal.
|
|
105
|
+
*/
|
|
106
|
+
export async function sweepAudienceDeclarations(
|
|
107
|
+
ctx: SetupContext,
|
|
108
|
+
options: AudienceSweepOptions,
|
|
109
|
+
): Promise<AudienceSweep> {
|
|
110
|
+
const sweep: AudienceSweep = { teardown: new Map(), unswept: [], warnings: [] };
|
|
111
|
+
const raw = await readFrameworkRaw(ctx.companionPath);
|
|
112
|
+
|
|
113
|
+
for (const [name, { declaration }] of audiencePackages(raw)) {
|
|
114
|
+
if (options.packages && !options.packages.has(name)) continue;
|
|
115
|
+
let plugin: Plugin | undefined = options.registrations.find(
|
|
116
|
+
(entry) => entry.packageName === name,
|
|
117
|
+
)?.plugin;
|
|
118
|
+
if (!plugin) {
|
|
119
|
+
const loaded = await loadDynamicPlugin(ctx.companionPath, declaration!, options.deps);
|
|
120
|
+
if (!loaded.ok) {
|
|
121
|
+
sweep.unswept.push(name);
|
|
122
|
+
sweep.warnings.push(
|
|
123
|
+
`audience plugin "${name}" was not swept from the companion: ${loaded.warning} Its entries stay until a pass can load it.`,
|
|
124
|
+
);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
plugin = loaded.plugin;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const capability = plugin as CapabilityPlugin;
|
|
131
|
+
if (plugin.kind !== "capability" || !capability.getRuntimeContributions) continue;
|
|
132
|
+
try {
|
|
133
|
+
const byRuntime = await capability.getRuntimeContributions(ctx);
|
|
134
|
+
for (const [runtimeId, contributions] of Object.entries(byRuntime)) {
|
|
135
|
+
if (!contributions) continue;
|
|
136
|
+
const inputs = sweep.teardown.get(runtimeId) ?? [];
|
|
137
|
+
inputs.push({
|
|
138
|
+
pluginId: plugin.id,
|
|
139
|
+
enabled: false,
|
|
140
|
+
capabilityEnabled: false,
|
|
141
|
+
contributions,
|
|
142
|
+
});
|
|
143
|
+
sweep.teardown.set(runtimeId, inputs);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
sweep.unswept.push(name);
|
|
147
|
+
sweep.warnings.push(
|
|
148
|
+
`audience plugin "${name}" was not swept from the companion: ${error instanceof Error ? error.message : String(error)}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return sweep;
|
|
153
|
+
}
|
|
@@ -18,6 +18,7 @@ const REPORTED_AS: Partial<
|
|
|
18
18
|
> = {
|
|
19
19
|
"projection-root": { as: "removed", label: ".mate" },
|
|
20
20
|
"git-excludes": { as: "removed", label: "git-excludes" },
|
|
21
|
+
"openspec-store-pointer": { as: "removed", label: "openspec/config.yaml" },
|
|
21
22
|
"claude-working-settings": { as: "updated", label: "claude-settings" },
|
|
22
23
|
"legacy-tokensave-claude-md": { as: "updated", label: "claude-settings" },
|
|
23
24
|
};
|
|
@@ -36,6 +37,7 @@ export async function cleanupWorkingRepository(
|
|
|
36
37
|
retained.push(entry.id);
|
|
37
38
|
continue;
|
|
38
39
|
}
|
|
40
|
+
if (entry.retains && !retained.includes(entry.retains)) retained.push(entry.retains);
|
|
39
41
|
if (entry.state !== "removed") continue;
|
|
40
42
|
const reported = REPORTED_AS[entry.id];
|
|
41
43
|
if (!reported) continue;
|