@uniqbit/mate-core 0.18.4 → 0.18.5

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.
Files changed (44) hide show
  1. package/opencode-plugin/src/add-dir.ts +76 -10
  2. package/opencode-plugin/src/companion.ts +1 -8
  3. package/opencode-plugin/src/server.ts +2 -15
  4. package/package.json +1 -1
  5. package/src/cli/commands/artifact/finish/openspec.ts +40 -8
  6. package/src/cli/commands/doctor.ts +92 -86
  7. package/src/cli/commands/install.ts +0 -8
  8. package/src/cli/commands/studio/terminal-launch.ts +7 -16
  9. package/src/cli/commands/wrap.ts +4 -29
  10. package/src/cli/usage.ts +1 -2
  11. package/src/hooks/session-guidance.ts +2 -20
  12. package/src/lib/orchestrator/adapters/claude.ts +20 -130
  13. package/src/lib/orchestrator/adapters/opencode.ts +7 -24
  14. package/src/lib/orchestrator/config-store.ts +2 -70
  15. package/src/lib/orchestrator/projection-claude-skills.ts +35 -88
  16. package/src/lib/orchestrator/projection-entries.ts +0 -1
  17. package/src/lib/orchestrator/projection-types.ts +0 -2
  18. package/src/lib/orchestrator/session-prepare.ts +4 -82
  19. package/src/lib/orchestrator/wrap-pass.ts +1 -88
  20. package/src/runtime/guidance.ts +0 -17
  21. package/src/runtime/policy.ts +1 -14
  22. package/src/runtime/runtime-document-manifest.ts +0 -17
  23. package/src/runtime/wrap-state.ts +0 -4
  24. package/src/tools/setup/capabilities/openspec.ts +140 -18
  25. package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
  26. package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
  27. package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
  28. package/src/tools/setup/engine.ts +10 -141
  29. package/src/tools/setup/plugin.ts +4 -19
  30. package/src/tools/setup/plugins/gitignore.ts +1 -7
  31. package/src/tools/setup/providers/claude.ts +2 -27
  32. package/src/tools/setup/providers/opencode.ts +0 -10
  33. package/src/tools/setup/registry.ts +2 -24
  34. package/src/tools/setup/surface-target.ts +4 -13
  35. package/src/tools/setup.ts +6 -9
  36. package/opencode-plugin/src/audience-layer.ts +0 -91
  37. package/src/runtime/audience-layer.ts +0 -84
  38. package/src/runtime/opencode-audience-layer.ts +0 -176
  39. package/src/tools/setup/audience-isolation.fixture.ts +0 -325
  40. package/src/tools/setup/audience-layer.ts +0 -207
  41. package/src/tools/setup/audience-registrations.ts +0 -52
  42. package/src/tools/setup/contribution-ledger.ts +0 -207
  43. package/src/tools/setup/isolation-diagnostics.ts +0 -139
  44. package/src/tools/setup/upgrade-sweep.ts +0 -153
@@ -3,22 +3,13 @@ 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;
10
6
  }
11
7
 
12
8
  export function normalizeRegistration(entry: PluginRegistration): NormalizedRegistration {
13
9
  if ("plugin" in entry && "policy" in entry) {
14
- return {
15
- plugin: entry.plugin,
16
- policy: entry.policy,
17
- audience: entry.audience ?? null,
18
- ...(entry.packageName ? { packageName: entry.packageName } : {}),
19
- };
10
+ return { plugin: entry.plugin, policy: entry.policy };
20
11
  }
21
- return { plugin: entry, policy: entry.defaultSelected ? "default" : "optional", audience: null };
12
+ return { plugin: entry, policy: entry.defaultSelected ? "default" : "optional" };
22
13
  }
23
14
 
24
15
  export class PluginRegistry {
@@ -41,19 +32,6 @@ export class PluginRegistry {
41
32
  return this.entries.map((entry) => ({ ...entry }));
42
33
  }
43
34
 
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
-
57
35
  getPolicy(pluginId: string): PluginPolicy | undefined {
58
36
  return this.entries.find((entry) => entry.plugin.id === pluginId)?.policy;
59
37
  }
@@ -1,6 +1,5 @@
1
1
  import path from "node:path";
2
2
 
3
- import { audienceLayerDir } from "../../runtime/audience-layer";
4
3
  import type { ProjectionTarget, SetupContext } from "./plugin";
5
4
 
6
5
  /**
@@ -15,9 +14,6 @@ export function projectionTarget(ctx: SetupContext): ProjectionTarget {
15
14
  "a working-target reconcile needs a Working Repository in scope; none was supplied",
16
15
  );
17
16
  }
18
- if (target === "audience-layer" && !ctx.audience) {
19
- throw new Error("an audience-layer reconcile needs an audience; none was supplied");
20
- }
21
17
  return target;
22
18
  }
23
19
 
@@ -42,18 +38,13 @@ export function projectionRepoRoot(ctx: SetupContext): string {
42
38
  }
43
39
 
44
40
  /**
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.
41
+ * The root a Runtime Surface writes into. Defined for the companion target
42
+ * only: a working-target pass renders its documents for the Projection Root to
43
+ * place, so no Runtime Surface ever computes a path inside a Working Repository.
49
44
  */
50
45
  export function surfaceRoot(ctx: SetupContext): string {
51
- const target = projectionTarget(ctx);
52
- if (target === "working") {
46
+ if (projectionTarget(ctx) === "working") {
53
47
  throw new Error("a working-target reconcile renders its documents; it writes no surface root");
54
48
  }
55
- if (target === "audience-layer") {
56
- return ctx.layerRoot ?? audienceLayerDir(ctx.companionPath, ctx.audience!);
57
- }
58
49
  return ctx.companionPath;
59
50
  }
@@ -61,7 +61,7 @@ export async function applySetupCompatibilities(
61
61
  companionPath: string,
62
62
  config: FrameworkConfig,
63
63
  mode: "setup" | "sync",
64
- plugins: PluginRegistration[] = getActiveDistribution().registry.baseRegistrations(),
64
+ plugins: PluginRegistration[] = getActiveDistribution().registry.getEntries(),
65
65
  repoPath?: string,
66
66
  scope: SetupScope = config.type === "hub" ? "hub" : "companion",
67
67
  ): Promise<SetupInstallationOutcome> {
@@ -73,15 +73,14 @@ export async function applySetupCompatibilities(
73
73
 
74
74
  /**
75
75
  * The active Agent Runtimes' documents for a Working Repository, rendered
76
- * without a write: base, plus the audience the checkout is wrapped for. Runs no plugin phase: the working target reconciles
76
+ * without a write. Runs no plugin phase: the working target reconciles
77
77
  * declarations only, so nothing is written to the companion on its behalf.
78
78
  */
79
79
  export async function renderWorkingRuntimeDocuments(
80
80
  companionPath: string,
81
81
  config: FrameworkConfig,
82
82
  repoPath: string,
83
- plugins: PluginRegistration[] = getActiveDistribution().registry.baseRegistrations(),
84
- wrapped?: { audience: string; registrations: PluginRegistration[] },
83
+ plugins: PluginRegistration[] = getActiveDistribution().registry.getAll(),
85
84
  ): Promise<RenderedRuntimeDocuments> {
86
85
  const plan = buildSetupInstallationPlan(config, plugins);
87
86
  const ctx: SetupContext = {
@@ -92,7 +91,7 @@ export async function renderWorkingRuntimeDocuments(
92
91
  repoPath,
93
92
  target: "working",
94
93
  };
95
- return renderRuntimeDocuments(ctx, plugins, plan, wrapped);
94
+ return renderRuntimeDocuments(ctx, plugins, plan);
96
95
  }
97
96
 
98
97
  export async function updateProjectGitignore(
@@ -100,9 +99,7 @@ export async function updateProjectGitignore(
100
99
  config: FrameworkConfig,
101
100
  ): Promise<void> {
102
101
  const ctx: SetupContext = { companionPath, config, mode: "sync", activeProviders: [] };
103
- const plugins = getActiveDistribution()
104
- .registry.baseRegistrations()
105
- .map((entry) => entry.plugin);
102
+ const plugins = getActiveDistribution().registry.getAll();
106
103
  const entries = collectManagedGitignoreEntries(ctx, plugins);
107
104
  await writeManagedGitignoreBlock(path.join(companionPath, ".gitignore"), FRAMEWORK_NAME, entries);
108
105
  }
@@ -116,7 +113,7 @@ export async function syncCompanionFiles(
116
113
  companionPath,
117
114
  config,
118
115
  "sync",
119
- getActiveDistribution().registry.baseRegistrations(),
116
+ getActiveDistribution().registry.getAll(),
120
117
  repoPath,
121
118
  );
122
119
  }
@@ -1,91 +0,0 @@
1
- import type { Context } from "@opencode/plugin/promise/plugin";
2
-
3
- import { repositoryWrappedForOpenCode } from "../../src/opencode/companion-hooks";
4
- import { currentAudienceLayer } from "../../src/runtime/audience-layer";
5
- import { MATE_ENV } from "../../src/runtime/env";
6
- import { parseGuidanceContent } from "../../src/runtime/guidance";
7
- import {
8
- readOpenCodeAudienceLayer,
9
- type OpenCodeLayerContents,
10
- } from "../../src/runtime/opencode-audience-layer";
11
- import { readWrapStateSync } from "../../src/runtime/wrap-state";
12
- import type { LaunchEnvironment } from "./launch-environment";
13
-
14
- export interface TrustedAudienceLayer {
15
- root: string;
16
- contents: OpenCodeLayerContents;
17
- }
18
-
19
- /**
20
- * The audience layer named by a payload the plugin trusts. `env` is the
21
- * launch environment `pluginLaunchEnvironment` decided on, so a masked
22
- * payload, a bare session and the shared service all read as `{}` here and
23
- * never apply a layer.
24
- */
25
- export async function readTrustedAudienceLayer(
26
- env: LaunchEnvironment,
27
- ): Promise<TrustedAudienceLayer | null> {
28
- const payload = env[MATE_ENV.guidanceJson];
29
- if (!payload?.trim()) return null;
30
- const root = parseGuidanceContent(payload).guidance.audienceLayer;
31
- if (!root) return null;
32
- return { root, contents: await readOpenCodeAudienceLayer(root) };
33
- }
34
-
35
- /**
36
- * The layer of the audience the session's checkout is wrapped for. The wrap is
37
- * resolved from the session's own directory, so the shared service serves each
38
- * checkout its own audience; the environment never selects one.
39
- */
40
- export async function readWrappedAudienceLayer(
41
- directory: string,
42
- companionPath: string,
43
- ): Promise<TrustedAudienceLayer | null> {
44
- const repoRoot = repositoryWrappedForOpenCode(directory);
45
- if (!repoRoot) return null;
46
- const audience = readWrapStateSync(repoRoot).audience;
47
- if (!audience) return null;
48
- const root = currentAudienceLayer(companionPath, audience);
49
- return root ? { root, contents: await readOpenCodeAudienceLayer(root) } : null;
50
- }
51
-
52
- /**
53
- * Applies a layer's MCP servers, skills and agents through the plugin API. A
54
- * new agent id is created by updating it; its prompt goes in `system`.
55
- */
56
- export async function registerAudienceLayer(
57
- api: Context,
58
- layer: OpenCodeLayerContents,
59
- ): Promise<void> {
60
- if (layer.mcpServers.length > 0) {
61
- await api.mcp.transform((editor) => {
62
- for (const server of layer.mcpServers) editor.set(server.name, server.config as never);
63
- });
64
- }
65
-
66
- if (layer.skills.length > 0) {
67
- await api.skill.transform((editor) => {
68
- for (const skill of layer.skills) {
69
- if (editor.get(skill.id)) {
70
- editor.update(skill.id, (current) => Object.assign(current, skill));
71
- } else {
72
- editor.add(skill as never);
73
- }
74
- }
75
- });
76
- }
77
-
78
- if (layer.agents.length > 0) {
79
- await api.agent.transform((editor) => {
80
- for (const agent of layer.agents) {
81
- editor.update(agent.id, (current) => {
82
- const target = current as Record<string, unknown>;
83
- target.name ??= agent.id;
84
- if (agent.description) target.description = agent.description;
85
- target.mode = agent.mode;
86
- target.system = agent.system;
87
- });
88
- }
89
- });
90
- }
91
- }
@@ -1,84 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- import { FRAMEWORK_NAME } from "./framework";
6
- import { mateHome } from "./runtime-directory";
7
-
8
- /**
9
- * Audience Layer locations. A layer holds one audience's plugin contributions
10
- * for one companion, rendered for managed launches only. It lives under Mate's
11
- * machine-local state, never inside a repository: a layer inside the companion
12
- * would be readable by every session with companion access, and could carry
13
- * values resolved from plugin configuration.
14
- *
15
- * <state>/audience-layers/<companion-key>/<audience>/
16
- * <fingerprint>/ one complete render; never changed once renamed into place
17
- * current symlink to the newest complete render
18
- *
19
- * A launch attaches the fingerprint directory itself, so a later re-render
20
- * never changes what a running session reads.
21
- */
22
-
23
- /** Mirrors `AUDIENCE_NAME_PATTERN`; repeated here because `runtime/` imports nothing from core. */
24
- const SAFE_AUDIENCE = /^[a-z][a-z0-9-]{0,63}$/;
25
-
26
- export const CURRENT_LAYER_LINK = "current";
27
-
28
- export function audienceLayersRoot(homeDir?: string): string {
29
- return path.join(mateHome(homeDir), "state", "audience-layers");
30
- }
31
-
32
- /** A short, stable key for one companion checkout: a hash of its resolved path. */
33
- export function companionLayerKey(companionPath: string): string {
34
- return crypto.createHash("sha256").update(path.resolve(companionPath)).digest("hex").slice(0, 16);
35
- }
36
-
37
- /** The directory holding one audience's layers for one companion. */
38
- export function audienceLayerDir(
39
- companionPath: string,
40
- audience: string,
41
- homeDir?: string,
42
- ): string {
43
- if (!SAFE_AUDIENCE.test(audience)) {
44
- throw new Error(`"${audience}" is not a valid audience name for a layer directory`);
45
- }
46
- return path.join(audienceLayersRoot(homeDir), companionLayerKey(companionPath), audience);
47
- }
48
-
49
- /** The newest complete layer of `audience`, or `null` when none was rendered on this machine. */
50
- export function currentAudienceLayer(
51
- companionPath: string,
52
- audience: string,
53
- homeDir?: string,
54
- ): string | null {
55
- try {
56
- const dir = audienceLayerDir(companionPath, audience, homeDir);
57
- const layer = path.join(dir, fs.readlinkSync(path.join(dir, CURRENT_LAYER_LINK)));
58
- return fs.existsSync(layer) ? layer : null;
59
- } catch {
60
- return null;
61
- }
62
- }
63
-
64
- const MANAGED_MARKER = new RegExp(
65
- `^<!-- ${FRAMEWORK_NAME}:managed:[^\\n]* (?:start|end) -->$`,
66
- "gm",
67
- );
68
-
69
- /** A layer guidance document's text, managed-block markers removed; empty when absent. */
70
- export function layerGuidanceText(text: string): string {
71
- return text
72
- .replace(MANAGED_MARKER, "")
73
- .replace(/\n{3,}/g, "\n\n")
74
- .trim();
75
- }
76
-
77
- /** `file` (`CLAUDE.md`, `AGENTS.md`) of the layer at `root`, markers removed; empty when absent. */
78
- export function readLayerGuidanceSync(root: string, file: string): string {
79
- try {
80
- return layerGuidanceText(fs.readFileSync(path.join(root, file), "utf8"));
81
- } catch {
82
- return "";
83
- }
84
- }
@@ -1,176 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- import { layerGuidanceText } from "./audience-layer";
5
- import { OPENCODE_MCP_SERVERS_AT } from "./runtime-document-manifest";
6
-
7
- /**
8
- * Reads an audience layer the OpenCode Runtime Surface wrote, as what the Mate
9
- * OpenCode plugin applies through the plugin API. The surface re-exports it as
10
- * its own reader; it lives under `runtime/` only so the import-isolated plugin
11
- * can reach it. The plugin parses no OpenCode document itself.
12
- */
13
-
14
- export interface LayerMcpServer {
15
- name: string;
16
- /** The entry exactly as the surface wrote it under `mcp.servers`. */
17
- config: Record<string, unknown>;
18
- }
19
-
20
- export interface LayerSkill {
21
- id: string;
22
- name: string;
23
- description?: string;
24
- /** The skill's `SKILL.md`. */
25
- path: string;
26
- content: string;
27
- }
28
-
29
- export type LayerAgentMode = "subagent" | "primary" | "all";
30
-
31
- export interface LayerAgent {
32
- id: string;
33
- description?: string;
34
- mode: LayerAgentMode;
35
- /** The agent's prompt: the definition's body. */
36
- system: string;
37
- }
38
-
39
- export interface OpenCodeLayerContents {
40
- mcpServers: LayerMcpServer[];
41
- skills: LayerSkill[];
42
- agents: LayerAgent[];
43
- /** The layer's guidance sections, managed-block markers removed. */
44
- guidance: string;
45
- }
46
-
47
- function isRecord(value: unknown): value is Record<string, unknown> {
48
- return typeof value === "object" && value !== null && !Array.isArray(value);
49
- }
50
-
51
- function splitFrontmatter(content: string): { fields: string; body: string } {
52
- const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
53
- return match
54
- ? { fields: match[1]!, body: content.slice(match[0].length) }
55
- : { fields: "", body: content };
56
- }
57
-
58
- function frontmatterScalarFromFields(fields: string, key: string): string | undefined {
59
- const value = fields.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim();
60
- return value?.replace(
61
- /^(?:"([\s\S]*)"|'([\s\S]*)')$/,
62
- (_all, doubleQuoted?: string, singleQuoted?: string) => doubleQuoted ?? singleQuoted ?? "",
63
- );
64
- }
65
-
66
- /** One scalar of a simple YAML frontmatter, quotes removed. */
67
- export function frontmatterScalar(content: string, key: string): string | undefined {
68
- return frontmatterScalarFromFields(splitFrontmatter(content).fields, key);
69
- }
70
-
71
- /** An id OpenCode accepts for a skill or agent name. */
72
- export function contributionId(name: string, fallback: string): string {
73
- return (
74
- name
75
- .toLowerCase()
76
- .replace(/[^a-z0-9_-]+/g, "-")
77
- .replace(/^-+|-+$/g, "") || fallback
78
- );
79
- }
80
-
81
- async function findSkillFiles(directory: string): Promise<string[]> {
82
- try {
83
- const entries = await fs.readdir(directory, { withFileTypes: true });
84
- const nested = await Promise.all(
85
- entries.map((entry) => {
86
- const entryPath = path.join(directory, entry.name);
87
- if (entry.isFile() && entry.name === "SKILL.md") return Promise.resolve([entryPath]);
88
- return entry.isDirectory() ? findSkillFiles(entryPath) : Promise.resolve([]);
89
- }),
90
- );
91
- return nested.flat().toSorted();
92
- } catch {
93
- return [];
94
- }
95
- }
96
-
97
- /** Every `SKILL.md` under `roots`; an unreadable skill is skipped, never fatal. */
98
- export async function loadSkillInfos(roots: readonly string[]): Promise<LayerSkill[]> {
99
- const files = (await Promise.all(roots.map(findSkillFiles))).flat();
100
- const skills = await Promise.all(
101
- files.map(async (filePath): Promise<LayerSkill | null> => {
102
- try {
103
- const content = await fs.readFile(filePath, "utf8");
104
- const { fields } = splitFrontmatter(content);
105
- const name =
106
- frontmatterScalarFromFields(fields, "name") || path.basename(path.dirname(filePath));
107
- const description = frontmatterScalarFromFields(fields, "description");
108
- return {
109
- id: contributionId(name, "mate-skill"),
110
- name,
111
- ...(description ? { description } : {}),
112
- path: filePath,
113
- content,
114
- };
115
- } catch {
116
- return null;
117
- }
118
- }),
119
- );
120
- return skills.filter((skill) => skill !== null);
121
- }
122
-
123
- async function readMcpServers(root: string): Promise<LayerMcpServer[]> {
124
- const parsed = await fs
125
- .readFile(path.join(root, ".opencode", "opencode.json"), "utf8")
126
- .then((text) => JSON.parse(text) as unknown)
127
- .catch(() => null);
128
- let servers: unknown = parsed;
129
- for (const key of OPENCODE_MCP_SERVERS_AT) servers = isRecord(servers) ? servers[key] : undefined;
130
- if (!isRecord(servers)) return [];
131
- return Object.entries(servers)
132
- .filter((entry): entry is [string, Record<string, unknown>] => isRecord(entry[1]))
133
- .map(([name, config]) => ({ name, config }))
134
- .toSorted((left, right) => left.name.localeCompare(right.name));
135
- }
136
-
137
- function agentMode(value: string | undefined): LayerAgentMode {
138
- return value === "subagent" || value === "primary" ? value : "all";
139
- }
140
-
141
- async function readAgents(root: string): Promise<LayerAgent[]> {
142
- const dir = path.join(root, ".opencode", "agents");
143
- const names = await fs.readdir(dir).catch(() => [] as string[]);
144
- const agents = await Promise.all(
145
- names
146
- .filter((name) => name.endsWith(".md"))
147
- .toSorted()
148
- .map(async (name): Promise<LayerAgent | null> => {
149
- const content = await fs.readFile(path.join(dir, name), "utf8").catch(() => null);
150
- if (content === null) return null;
151
- const { fields, body } = splitFrontmatter(content);
152
- const description = frontmatterScalarFromFields(fields, "description");
153
- return {
154
- id: contributionId(path.basename(name, ".md"), "mate-agent"),
155
- ...(description ? { description } : {}),
156
- mode: agentMode(frontmatterScalarFromFields(fields, "mode")),
157
- system: body.trim(),
158
- };
159
- }),
160
- );
161
- return agents.filter((agent) => agent !== null);
162
- }
163
-
164
- async function readGuidance(root: string): Promise<string> {
165
- return layerGuidanceText(await fs.readFile(path.join(root, "AGENTS.md"), "utf8").catch(() => ""));
166
- }
167
-
168
- export async function readOpenCodeAudienceLayer(root: string): Promise<OpenCodeLayerContents> {
169
- const [mcpServers, skills, agents, guidance] = await Promise.all([
170
- readMcpServers(root),
171
- loadSkillInfos([path.join(root, ".agents", "skills")]),
172
- readAgents(root),
173
- readGuidance(root),
174
- ]);
175
- return { mcpServers, skills, agents, guidance };
176
- }