@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
@@ -33,29 +33,12 @@ function reportChangedPaths(changed: ProjectionChangedPaths): void {
33
33
 
34
34
  /**
35
35
  * `--companion` is consumed by the dispatch gate before the command runs, so it
36
- * is parsed there too. `--audience` names the audience the checkout's bare
37
- * sessions get beside base.
36
+ * is parsed there too; this only rejects anything else.
38
37
  */
39
- export function parseWrapArgs(
40
- argv: string[],
41
- ): { companion?: string; audience?: string } | { error: string } {
38
+ export function parseWrapArgs(argv: string[]): { companion?: string } | { error: string } {
42
39
  let companion: string | undefined;
43
- let audience: string | undefined;
44
40
  for (let index = 0; index < argv.length; index += 1) {
45
41
  const arg = argv[index]!;
46
- if (arg === "--audience") {
47
- const value = argv[index + 1];
48
- if (!value || value.startsWith("--")) return { error: "--audience requires a name" };
49
- audience = value;
50
- index += 1;
51
- continue;
52
- }
53
- if (arg.startsWith("--audience=")) {
54
- const value = arg.slice("--audience=".length);
55
- if (!value) return { error: "--audience requires a name" };
56
- audience = value;
57
- continue;
58
- }
59
42
  if (arg === "--companion") {
60
43
  const value = argv[index + 1];
61
44
  if (!value || value.startsWith("--")) return { error: "--companion requires a path" };
@@ -71,10 +54,7 @@ export function parseWrapArgs(
71
54
  }
72
55
  return { error: `unknown wrap option: ${arg}` };
73
56
  }
74
- return {
75
- ...(companion === undefined ? {} : { companion }),
76
- ...(audience === undefined ? {} : { audience }),
77
- };
57
+ return companion === undefined ? {} : { companion };
78
58
  }
79
59
 
80
60
  /**
@@ -128,12 +108,8 @@ export async function runWrapCommand(
128
108
  }
129
109
 
130
110
  /** Serialized with Session Prepare and every sync, which rewrite the same manifest. */
131
- /**
132
- * The flag is the only way to give a checkout's bare sessions an audience;
133
- * without it the wrap is base and any recorded audience is withdrawn.
134
- */
135
111
  const locked = await withCompanionLock(companionPath, attendedLockOptions(companionPath), () =>
136
- wrap(companionPath, repository, { audience: parsed.audience ?? null }),
112
+ wrap(companionPath, repository),
137
113
  );
138
114
  const result: WrapPassResult = locked.acquired
139
115
  ? locked.value
@@ -150,7 +126,6 @@ export async function runWrapCommand(
150
126
  : `Already wrapped: ${result.projectionRoot}`,
151
127
  );
152
128
  console.log(` Companion: ${result.companionPath}`);
153
- if (result.audience) console.log(` Audience: ${result.audience}`);
154
129
  for (const document of result.documents) {
155
130
  console.log(` Runtime: ${document}`);
156
131
  }
package/src/cli/usage.ts CHANGED
@@ -21,7 +21,7 @@ export function usage(): string {
21
21
  ` ${n} companion open`,
22
22
  ` ${n} companion tui`,
23
23
  ` ${n} working cleanup`,
24
- ` ${n} wrap [--companion PATH] [--audience NAME]`,
24
+ ` ${n} wrap [--companion PATH]`,
25
25
  ` ${n} unwrap`,
26
26
  ` ${n} session prepare --repo <dir> [--json] [--sync]`,
27
27
  ` ${n} hub init [folder]`,
@@ -54,7 +54,6 @@ export function usage(): string {
54
54
  "",
55
55
  "Wrapped repositories:",
56
56
  ` ${n} wrap configures the repository so sessions you start yourself load the companion`,
57
- ` ${n} wrap --audience NAME gives those sessions one audience beside base; plain wrap returns to base`,
58
57
  ` ${n} claude does not run while a repository is wrapped for Claude`,
59
58
  ` ${n} opencode wraps the repository for OpenCode itself, then starts plain \`opencode\``,
60
59
  ` ${n} unwrap withdraws the wrap and restores managed launches; the link is kept`,
@@ -1,8 +1,5 @@
1
1
  // Deliver the companion guidance to a Claude session Mate did not launch.
2
- import { currentAudienceLayer, readLayerGuidanceSync } from "../runtime/audience-layer";
3
- import { resolveProjection } from "../runtime/projection";
4
2
  import { buildProjectedGuidance } from "../runtime/projected-guidance";
5
- import { readWrapStateSync } from "../runtime/wrap-state";
6
3
  import type { HookEnv } from "./validate-artifact-path";
7
4
 
8
5
  export interface GuidanceOutcome {
@@ -10,19 +7,6 @@ export interface GuidanceOutcome {
10
7
  stdout: string;
11
8
  }
12
9
 
13
- /**
14
- * The guidance of the audience this checkout is wrapped for, from its layer.
15
- * The wrap is the only source: an inherited `MATE_AUDIENCE` selects nothing.
16
- */
17
- function wrappedAudienceGuidance(cwd: string): string {
18
- const projection = resolveProjection(cwd);
19
- if (!projection) return "";
20
- const audience = readWrapStateSync(projection.repoRoot).audience;
21
- if (!audience) return "";
22
- const layer = currentAudienceLayer(projection.companionPath, audience);
23
- return layer ? readLayerGuidanceSync(layer, "CLAUDE.md") : "";
24
- }
25
-
26
10
  /**
27
11
  * `SessionStart.additionalContext` is the file-based form of the managed
28
12
  * launch's `--append-system-prompt`: a settings document can declare a hook,
@@ -34,10 +18,8 @@ function wrappedAudienceGuidance(cwd: string): string {
34
18
  * repository must read exactly as it did before the hook existed.
35
19
  */
36
20
  export function buildSessionGuidance(env: HookEnv, cwd: string = process.cwd()): GuidanceOutcome {
37
- const companionGuidance = buildProjectedGuidance(env, cwd);
38
- if (!companionGuidance) return { exitCode: 0, stdout: "" };
39
- const layer = wrappedAudienceGuidance(cwd);
40
- const guidance = layer ? `${companionGuidance}\n\n${layer}` : companionGuidance;
21
+ const guidance = buildProjectedGuidance(env, cwd);
22
+ if (!guidance) return { exitCode: 0, stdout: "" };
41
23
 
42
24
  return {
43
25
  exitCode: 0,
@@ -9,11 +9,6 @@ import {
9
9
  type ClaudeDeliveryPlan,
10
10
  type SessionKind,
11
11
  } from "../../../runtime/claude-delivery-plan";
12
- import {
13
- audienceLayerDir,
14
- currentAudienceLayer,
15
- readLayerGuidanceSync,
16
- } from "../../../runtime/audience-layer";
17
12
  import { hookGroupKey, type ClaudeHookGroup } from "../../../runtime/claude-hook-group";
18
13
  import { NOT_WRAPPED } from "../../../runtime/wrap-state";
19
14
  import { writeAtomic } from "../../../runtime/write-atomic";
@@ -24,31 +19,15 @@ import {
24
19
  import type { ClaudeSettings } from "../../../tools/setup/providers/claude-format";
25
20
  import { getContextModePackageRoot, validateContextModePackage } from "../../context-mode-package";
26
21
  import { getClaudePluginRoot, validateClaudePluginAssets } from "../../package-paths";
27
- import type { RenderedAudienceLayer } from "../../../tools/setup/audience-layer";
28
22
  import type { PrepareResult } from "../session-prepare";
29
23
  import { LaunchPreflightError } from "../types";
30
24
  import { type AdapterContext, LaunchAdapter } from "./base";
31
25
 
32
- /** The active audience's layer, attached to this launch only. */
33
- interface ClaudeLayerDelivery {
34
- audience: string;
35
- root: string;
36
- /** The layer's MCP configuration, when the layer holds one. */
37
- mcpConfigPath?: string;
38
- }
39
-
40
26
  /** The paths are present only where the plan names that channel and the file exists. */
41
27
  interface ClaudeDelivery {
42
28
  plan: ClaudeDeliveryPlan;
43
29
  settingsPath?: string;
44
30
  mcpConfigPath?: string;
45
- layer?: ClaudeLayerDelivery;
46
- /**
47
- * The guidance of the audience the checkout is wrapped for. The wrap carries
48
- * the rest of that audience; its guidance hook stays silent in a managed
49
- * session, so the launch's own system prompt carries this.
50
- */
51
- wrappedGuidance?: string;
52
31
  }
53
32
 
54
33
  /** A Claude launch's context: the delivery its plan chose, and the files it chose or wrote. */
@@ -56,75 +35,34 @@ interface ClaudeLaunchContext extends AdapterContext {
56
35
  delivery: ClaudeDelivery;
57
36
  }
58
37
 
59
- function safeName(value: string): string {
60
- return value.replace(/[^A-Za-z0-9._-]/g, "_");
61
- }
62
-
63
- /**
64
- * Where a launch writes its derived `--settings`. Per Working Repository and
65
- * audience, so concurrent launches never race; a companion-scoped launch
66
- * derives one only for an audience. Without an audience it is git-ignored
67
- * under the companion. With one it carries that audience's hooks and
68
- * permission entries, so it lives beside the audience's layer, outside the
69
- * companion, where no other audience's session can read it.
70
- */
71
- export function launchSettingsPath(
72
- companionPath: string,
73
- repositoryId: string | null,
74
- audience?: string,
75
- ): string {
76
- const scope = repositoryId === null ? "companion-scope" : `settings.${safeName(repositoryId)}`;
77
- if (audience) {
78
- return path.join(audienceLayerDir(companionPath, audience), `launch-${scope}.json`);
79
- }
80
- return path.join(companionPath, ".claude", "state", `launch-${scope}.json`);
81
- }
82
-
83
- async function readSettings(settingsPath: string): Promise<ClaudeSettings> {
84
- if (!existsSync(settingsPath)) return {};
85
- return JSON.parse(await fs.readFile(settingsPath, "utf8")) as ClaudeSettings;
38
+ /** Where a launch with a Working Repository writes its derived `--settings`; git-ignored. */
39
+ export function launchSettingsPath(companionPath: string, repositoryId: string): string {
40
+ const safeId = repositoryId.replace(/[^A-Za-z0-9._-]/g, "_");
41
+ return path.join(companionPath, ".claude", "state", `launch-settings.${safeId}.json`);
86
42
  }
87
43
 
88
44
  /**
89
- * Companion settings minus `exclude`, so each wrap-carried group runs once,
90
- * plus the layer's hook groups and permission entries. A file, not inline JSON
91
- * (platform arg limits). Claude reads one `--settings` file only — a second
92
- * one replaces the first — so the layer is merged here, never passed beside.
45
+ * Companion settings minus `exclude`, so each wrap-carried group runs once. A
46
+ * file, not inline JSON (platform arg limits); per repository, so concurrent
47
+ * launches never race.
93
48
  */
94
49
  async function writeLaunchSettings(
95
50
  companionSettingsPath: string,
96
51
  target: string,
97
52
  exclude: readonly ClaudeHookGroup[],
98
- layerSettingsPath?: string,
99
53
  ): Promise<void> {
100
- const settings = await readSettings(companionSettingsPath);
101
- const layer = layerSettingsPath ? await readSettings(layerSettingsPath) : {};
54
+ const settings = JSON.parse(await fs.readFile(companionSettingsPath, "utf8")) as ClaudeSettings;
102
55
  const excluded = new Set(exclude.map(hookGroupKey));
103
56
  const hooks: Record<string, ClaudeHookGroup[]> = {};
104
57
  for (const [event, groups] of Object.entries(settings.hooks ?? {})) {
105
58
  const kept = (groups ?? []).filter((group) => !excluded.has(hookGroupKey(group)));
106
59
  if (kept.length > 0) hooks[event] = kept;
107
60
  }
108
- for (const [event, groups] of Object.entries(layer.hooks ?? {})) {
109
- hooks[event] = [...(hooks[event] ?? []), ...(groups ?? [])];
110
- }
111
61
  const derived: ClaudeSettings = { ...settings, hooks };
112
62
  if (Object.keys(hooks).length === 0) delete derived.hooks;
113
- const layerAllow = layer.permissions?.allow ?? [];
114
- if (layerAllow.length > 0) {
115
- const allow = [...new Set([...(settings.permissions?.allow ?? []), ...layerAllow])];
116
- derived.permissions = { ...settings.permissions, allow };
117
- }
118
63
 
119
- const text = `${JSON.stringify(derived, null, 2)}\n`;
120
- if (layerSettingsPath) {
121
- /** Beside the audience's layer: as private as the layer itself. */
122
- await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
123
- await writeAtomic(target, text, 0o600);
124
- } else {
125
- await fs.mkdir(path.dirname(target), { recursive: true });
126
- await writeAtomic(target, text);
127
- }
64
+ await fs.mkdir(path.dirname(target), { recursive: true });
65
+ await writeAtomic(target, `${JSON.stringify(derived, null, 2)}\n`);
128
66
  }
129
67
 
130
68
  /** The `--settings` file the plan's hook owner calls for, writing it when derived. */
@@ -132,38 +70,16 @@ async function settingsPathFor(
132
70
  plan: ClaudeDeliveryPlan,
133
71
  companionPath: string,
134
72
  repository: AdapterContext["repository"],
135
- layer: RenderedAudienceLayer | undefined,
136
73
  ): Promise<string | undefined> {
137
74
  const companionSettingsPath = getCompanionClaudeSettingsPath(companionPath);
138
- if (plan.hooks.owner === "none") return undefined;
139
- if (!layer && !existsSync(companionSettingsPath)) return undefined;
140
- if (plan.hooks.owner === "settings" && !layer) return companionSettingsPath;
75
+ if (plan.hooks.owner === "none" || !existsSync(companionSettingsPath)) return undefined;
76
+ if (plan.hooks.owner === "settings") return companionSettingsPath;
141
77
  /** Owner `wrap` is planned only for `managed-repo`, which has a repository. */
142
- const settingsPath = launchSettingsPath(
143
- companionPath,
144
- plan.hooks.owner === "wrap" ? repository!.id : null,
145
- layer?.audience,
146
- );
147
- await writeLaunchSettings(
148
- companionSettingsPath,
149
- settingsPath,
150
- plan.hooks.launchSettingsExclude,
151
- layer ? getCompanionClaudeSettingsPath(layer.root) : undefined,
152
- );
78
+ const settingsPath = launchSettingsPath(companionPath, repository!.id);
79
+ await writeLaunchSettings(companionSettingsPath, settingsPath, plan.hooks.launchSettingsExclude);
153
80
  return settingsPath;
154
81
  }
155
82
 
156
- /** The layer as this launch attaches it: the directory, and its MCP configuration when present. */
157
- function layerDelivery(layer: RenderedAudienceLayer | undefined): ClaudeLayerDelivery | undefined {
158
- if (!layer) return undefined;
159
- const mcpConfigPath = getCompanionClaudeMcpConfigPath(layer.root);
160
- return {
161
- audience: layer.audience,
162
- root: layer.root,
163
- ...(existsSync(mcpConfigPath) ? { mcpConfigPath } : {}),
164
- };
165
- }
166
-
167
83
  export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
168
84
  readonly toolName = "claude";
169
85
  readonly interactive = true;
@@ -184,22 +100,11 @@ export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
184
100
  }
185
101
  const plan = planClaudeDelivery(kind, wrap);
186
102
  const mcpConfigPath = getCompanionClaudeMcpConfigPath(companionPath);
187
- const settingsPath = await settingsPathFor(
188
- plan,
189
- companionPath,
190
- repository,
191
- prepared.audienceLayer,
192
- );
193
- const layer = layerDelivery(prepared.audienceLayer);
194
- const wrappedLayer =
195
- !layer && wrap.audience ? currentAudienceLayer(companionPath, wrap.audience) : null;
196
- const wrappedGuidance = wrappedLayer ? readLayerGuidanceSync(wrappedLayer, "CLAUDE.md") : "";
103
+ const settingsPath = await settingsPathFor(plan, companionPath, repository);
197
104
  const delivery: ClaudeDelivery = {
198
105
  plan,
199
106
  ...(plan.mcp === "mcp-config" && existsSync(mcpConfigPath) ? { mcpConfigPath } : {}),
200
107
  ...(settingsPath ? { settingsPath } : {}),
201
- ...(layer ? { layer } : {}),
202
- ...(wrappedGuidance ? { wrappedGuidance } : {}),
203
108
  };
204
109
  return { ...context, delivery };
205
110
  }
@@ -244,30 +149,15 @@ export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
244
149
  * and context-mode are unconditional: no settings document can carry them.
245
150
  */
246
151
  buildArgs(context: ClaudeLaunchContext, args: string[]): string[] {
247
- const { plan, settingsPath, mcpConfigPath, layer, wrappedGuidance } = context.delivery;
248
- const companionGuidance = buildCompanionGuidance(context);
249
- const systemPrompt = wrappedGuidance
250
- ? `${companionGuidance}\n\n${wrappedGuidance}`
251
- : companionGuidance;
152
+ const { plan, settingsPath, mcpConfigPath } = context.delivery;
252
153
  /** Layered on the user's own sources so global skills and plugins still load. */
253
154
  const settingsArgs =
254
155
  plan.hooks.owner !== "none" && settingsPath
255
156
  ? ["--setting-sources", "user,project,local", "--settings", settingsPath]
256
157
  : [];
257
- /**
258
- * The layer adds its skills, commands, agents and `CLAUDE.md` as one more
259
- * directory, and its MCP servers as one more configuration beside every
260
- * server the launch delivered before. Both flags are variadic, so each is
261
- * followed by another flag, never by the operator's arguments.
262
- */
263
- const addDirArgs = [
264
- ...(plan.dirAccess === "add-dir" ? ["--add-dir", context.companionPath] : []),
265
- ...(layer ? ["--add-dir", layer.root] : []),
266
- ];
267
- const mcpConfigArgs = [
268
- ...(plan.mcp === "mcp-config" && mcpConfigPath ? ["--mcp-config", mcpConfigPath] : []),
269
- ...(layer?.mcpConfigPath ? ["--mcp-config", layer.mcpConfigPath] : []),
270
- ];
158
+ const addDirArgs = plan.dirAccess === "add-dir" ? ["--add-dir", context.companionPath] : [];
159
+ const mcpConfigArgs =
160
+ plan.mcp === "mcp-config" && mcpConfigPath ? ["--mcp-config", mcpConfigPath] : [];
271
161
  const contextModeArgs = context.capabilities.some(
272
162
  (capability) => capability.name === "context-mode",
273
163
  )
@@ -277,7 +167,7 @@ export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
277
167
  return [
278
168
  ...addDirArgs,
279
169
  "--append-system-prompt",
280
- systemPrompt,
170
+ buildCompanionGuidance(context),
281
171
  ...settingsArgs,
282
172
  ...mcpConfigArgs,
283
173
  /** The bundled plugin from the running install, beside context-mode's. */
@@ -56,12 +56,7 @@ function readInvocation(launchWorkingDirectory: string, args: string[]): OpenCod
56
56
  return { command, rest, hostsPrivateServer: Boolean(hostsServer) && !choosesRemoteServer };
57
57
  }
58
58
 
59
- /** An OpenCode launch's context: the audience layer its prepare rendered, if any. */
60
- interface OpenCodeLaunchContext extends AdapterContext {
61
- audienceLayer?: string;
62
- }
63
-
64
- export class OpenCodeAdapter extends LaunchAdapter<OpenCodeLaunchContext> {
59
+ export class OpenCodeAdapter extends LaunchAdapter {
65
60
  readonly toolName = "opencode";
66
61
  readonly interactive = true;
67
62
  private readonly requiredRuntimeAssets = [path.join(".opencode", "opencode.json")] as const;
@@ -84,15 +79,6 @@ export class OpenCodeAdapter extends LaunchAdapter<OpenCodeLaunchContext> {
84
79
  );
85
80
  }
86
81
 
87
- async planLaunch(
88
- context: AdapterContext,
89
- prepared: PrepareResult,
90
- ): Promise<OpenCodeLaunchContext> {
91
- return prepared.audienceLayer
92
- ? { ...context, audienceLayer: prepared.audienceLayer.root }
93
- : context;
94
- }
95
-
96
82
  /** A prepared repository reports its failure as one summary, as it always has. */
97
83
  launchFailure(prepared: PrepareResult): unknown {
98
84
  return prepared.repositoryId
@@ -116,29 +102,26 @@ export class OpenCodeAdapter extends LaunchAdapter<OpenCodeLaunchContext> {
116
102
  * overlay carried now lives in the wrapped `.opencode/opencode.json`. An
117
103
  * inherited `OPENCODE_CONFIG_DIR` or `OPENCODE_CONFIG_CONTENT` passes through.
118
104
  */
119
- extendEnvironment(context: OpenCodeLaunchContext, args: string[] = []): NodeJS.ProcessEnv {
105
+ extendEnvironment(context: AdapterContext, args: string[] = []): NodeJS.ProcessEnv {
120
106
  const { hostsPrivateServer } = readInvocation(context.launchWorkingDirectory, args);
121
107
  return {
122
108
  // Built fresh per launch from the live capability config; always set so
123
109
  // a stale value inherited from an outer Mate session can never leak in.
124
110
  // Masked unless the launch hosts its own server: a payload that reached
125
- // the shared service would apply this session's guidance — and its
126
- // audience layer — to every other.
111
+ // the shared service would apply this session's guidance to every other.
127
112
  [MATE_ENV.guidanceJson]: hostsPrivateServer
128
113
  ? JSON.stringify(this.buildGuidance(context))
129
114
  : "",
130
115
  };
131
116
  }
132
117
 
133
- /** The audience layer travels as a path; the plugin reads it through the surface's reader. */
134
- private buildGuidance(context: OpenCodeLaunchContext): MateGuidanceFile {
135
- const guidance = buildOpenCodeGuidance(context.capabilities, {
118
+ private buildGuidance(context: AdapterContext): MateGuidanceFile {
119
+ return buildOpenCodeGuidance(context.capabilities, {
136
120
  companionScoped: !context.repository,
137
121
  });
138
- return context.audienceLayer ? { ...guidance, audienceLayer: context.audienceLayer } : guidance;
139
122
  }
140
123
 
141
- async validateLaunch(context: OpenCodeLaunchContext): Promise<void> {
124
+ async validateLaunch(context: AdapterContext): Promise<void> {
142
125
  this.validateOpenCodeVersion();
143
126
 
144
127
  const missingAssets = await Promise.all(
@@ -202,7 +185,7 @@ export class OpenCodeAdapter extends LaunchAdapter<OpenCodeLaunchContext> {
202
185
 
203
186
  // Self-check the guidance payload this launch is about to inject so a
204
187
  // broken playbook template fails preflight instead of inside OpenCode.
205
- private validateGuidance(context: OpenCodeLaunchContext): string[] {
188
+ private validateGuidance(context: AdapterContext): string[] {
206
189
  const built = this.buildGuidance(context);
207
190
  const { guidance, errors } = validateGuidanceData(built);
208
191
 
@@ -1,14 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
- import semver from "semver";
3
+ import { normalizeBaseAudience } from "../../tools/setup/dynamic-plugins/audiences";
4
4
 
5
- import {
6
- BASE_AUDIENCE,
7
- BASE_AUDIENCE_KEYS,
8
- normalizeBaseAudience,
9
- } from "../../tools/setup/dynamic-plugins/audiences";
10
-
11
- import { getActiveDistribution } from "../../distribution";
12
5
  import { FRAMEWORK_NAME } from "../../framework";
13
6
  import {
14
7
  getDefaultSetupSelections,
@@ -300,69 +293,12 @@ function validateAudiences(config: FrameworkConfig): void {
300
293
  }
301
294
  }
302
295
 
303
- /**
304
- * The release that introduces the `audiences.base` shape. An older Mate would
305
- * read a saved file as an empty base plus an audience called `base`, so every
306
- * save in the new shape raises the companion's `engines` floor to it — capped
307
- * at the running release, so a pre-release build never locks itself out.
308
- */
309
- export const BASE_AUDIENCE_MIN_VERSION = "0.19.0";
310
-
311
- /** The floor version a save writes: the introducing release, or the running one when older. */
312
- export function baseAudienceFloorVersion(
313
- runningVersion: string = getActiveDistribution().config.version,
314
- ): string {
315
- const running = semver.coerce(runningVersion)?.version;
316
- return running && semver.lt(running, BASE_AUDIENCE_MIN_VERSION)
317
- ? running
318
- : BASE_AUDIENCE_MIN_VERSION;
319
- }
320
-
321
- /** Only companions carry audiences; a hub or a working repository keeps the root shape. */
322
- function usesAudiences(config: FrameworkConfig): boolean {
323
- return config.type === undefined || config.type === "companion";
324
- }
325
-
326
- /** The floor that admits the base audience shape, keeping a higher one. */
327
- function raisedEngineFloor(range: string | undefined): string {
328
- const version = baseAudienceFloorVersion();
329
- const floor = `>=${version}`;
330
- if (!range || semver.validRange(range) === null) return range ?? floor;
331
- const minimum = semver.minVersion(range);
332
- return minimum && semver.gte(minimum, version) ? range : floor;
333
- }
334
-
335
- /**
336
- * The file shape of a companion config: the base set's audience-scoped keys
337
- * under `audiences.base`, first among the audiences. The in-memory config
338
- * keeps them at the root, so its readers never see the difference.
339
- */
340
- function toFileShape(config: FrameworkConfig): FrameworkConfig {
341
- if (!usesAudiences(config)) return config;
342
- const base: Record<string, unknown> = {};
343
- const rest: Record<string, unknown> = { ...config };
344
- for (const key of BASE_AUDIENCE_KEYS) {
345
- if (rest[key] === undefined) continue;
346
- base[key] = rest[key];
347
- delete rest[key];
348
- }
349
- if (Object.keys(base).length === 0) return config;
350
- return {
351
- ...(rest as unknown as FrameworkConfig),
352
- audiences: { [BASE_AUDIENCE]: base, ...config.audiences } as FrameworkConfig["audiences"],
353
- engines: {
354
- ...config.engines,
355
- [FRAMEWORK_NAME]: raisedEngineFloor(config.engines?.[FRAMEWORK_NAME]),
356
- },
357
- };
358
- }
359
-
360
296
  export class ConfigStore extends YamlFileStore<FrameworkConfig> {
361
297
  constructor(configPath = process.env.MATE_CONFIG ?? defaultConfigPath()) {
362
298
  super(path.resolve(configPath));
363
299
  }
364
300
 
365
- /** Either file shape loads as the base set at the root; the new shape persists on the next save. */
301
+ /** Either file shape loads as the base set at the root; the next save writes the root shape. */
366
302
  override async load(): Promise<FrameworkConfig> {
367
303
  const merged = mergeWithDefaults(
368
304
  migrateProfilesToAllowedAgents(normalizeBaseAudience(await super.load())),
@@ -373,10 +309,6 @@ export class ConfigStore extends YamlFileStore<FrameworkConfig> {
373
309
  return merged;
374
310
  }
375
311
 
376
- override async save(config: FrameworkConfig): Promise<void> {
377
- await super.save(toFileShape(config));
378
- }
379
-
380
312
  protected async onMissing(): Promise<FrameworkConfig> {
381
313
  const config = defaultConfig();
382
314
  await this.save(config);