@uniqbit/mate-core 0.18.1 → 0.18.3-canary.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.18.1",
3
+ "version": "0.18.3-canary.0",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,6 +9,11 @@ import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-sto
9
9
  import { pathIsDirectory } from "../../lib/orchestrator/repo-local-registry";
10
10
  import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
11
11
  import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
12
+ import {
13
+ describeUnknownAudience,
14
+ readAudienceSelection,
15
+ resolveAudience,
16
+ } from "../../tools/setup/dynamic-plugins/audiences";
12
17
  import {
13
18
  inspectDeclaredPlugins,
14
19
  type PluginInspection,
@@ -26,6 +31,8 @@ interface DoctorDeps {
26
31
  /** Returns a member checkout's HEAD commit, or null when unreadable. */
27
32
  gitHead?: (memberPath: string) => string | null;
28
33
  inspectPlugins?: (companionPath: string) => Promise<PluginInspection>;
34
+ /** Environment `MATE_AUDIENCE` is read from; defaults to the process environment. */
35
+ env?: Record<string, string | undefined>;
29
36
  }
30
37
 
31
38
  export type DoctorKind = "core" | "working" | "companion" | "hub";
@@ -59,6 +66,10 @@ export interface DoctorReport {
59
66
  toolInstallations?: DoctorToolInstallation[];
60
67
  requiredPluginDrift?: Array<{ pluginId: string; kind: string; reason: string }>;
61
68
  pluginFailures?: PluginVerificationFailure[];
69
+ /** Present for a companion that declares audiences or has one selected. */
70
+ audiences?: { declared: string[]; active: string | null };
71
+ /** Set when `MATE_AUDIENCE` names an audience the companion does not declare. */
72
+ audienceError?: string;
62
73
  /** IDs of the capabilities the verified plugins provide. */
63
74
  pluginCapabilities?: string[];
64
75
  engineRequirement?: { range: string; ok: boolean; detail: string };
@@ -215,6 +226,19 @@ function collectConfiguredRootSections(
215
226
  report.engineRequirement = collectEngineRequirement(config);
216
227
  }
217
228
 
229
+ function collectAudiences(
230
+ report: DoctorReport,
231
+ config: FrameworkConfig,
232
+ env: Record<string, string | undefined>,
233
+ ): void {
234
+ const declared = Object.keys(config.audiences ?? {});
235
+ const requested = readAudienceSelection(env);
236
+ if (declared.length === 0 && requested === null) return;
237
+ const resolution = resolveAudience(config, requested);
238
+ report.audiences = { declared, active: resolution.ok ? resolution.active : null };
239
+ if (!resolution.ok) report.audienceError = describeUnknownAudience(resolution);
240
+ }
241
+
218
242
  export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<DoctorReport> {
219
243
  const cwd = path.resolve(deps.cwd ?? process.cwd());
220
244
  const pathValue = deps.pathValue ?? process.env.PATH ?? "";
@@ -248,8 +272,15 @@ export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<Doctor
248
272
  report.engineRequirement = collectEngineRequirement(root.config);
249
273
  }
250
274
 
275
+ const env = deps.env ?? process.env;
276
+ if (root.rootPath && root.config && kind !== "hub") {
277
+ collectAudiences(report, root.config, env);
278
+ }
279
+
251
280
  if (root.rootPath && root.config) {
252
- const inspection = await (deps.inspectPlugins ?? inspectDeclaredPlugins)(root.rootPath);
281
+ const inspect =
282
+ deps.inspectPlugins ?? ((companionPath) => inspectDeclaredPlugins(companionPath, { env }));
283
+ const inspection = await inspect(root.rootPath);
253
284
  report.pluginFailures = inspection.failures;
254
285
  report.pluginCapabilities = inspection.capabilities;
255
286
  }
@@ -334,6 +365,15 @@ function renderHumanReport(report: DoctorReport): void {
334
365
  if (report.toolInstallations) {
335
366
  printSection("Tool Installations", renderToolInstallations(report.toolInstallations));
336
367
  }
368
+ if (report.audiences) {
369
+ printSection(
370
+ "Audiences",
371
+ renderKeyValueTable([
372
+ ["Declared", report.audiences.declared.join(", ") || "none"],
373
+ ["Active", report.audiences.active ?? "none"],
374
+ ]),
375
+ );
376
+ }
337
377
  if (report.hub) printSection("Hub Members", renderHubMembers(report.hub.members));
338
378
  // Companions with no drift produce no section at all: required plugins are
339
379
  // silent when healthy.
@@ -376,8 +416,9 @@ function renderHumanReport(report: DoctorReport): void {
376
416
  * working repository), `companion`, `hub`, or `core` (not linked) — with
377
417
  * kind-specific diagnostics: policy and capabilities for working repos,
378
418
  * capabilities and tool checks for companions, member health for hubs.
379
- * Declared plugins are verified strictly (allowed by `MATE_ALLOWED_PLUGINS`,
380
- * installed, loadable); any failure is named on stderr and exits non-zero.
419
+ * Declared plugins of the effective set (base plus the `MATE_AUDIENCE`
420
+ * audience) are verified strictly (installed, loadable); any failure is named
421
+ * on stderr and exits non-zero, as is an undeclared `MATE_AUDIENCE`.
381
422
  * `--json` emits the same report as one JSON document.
382
423
  */
383
424
  export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
@@ -389,5 +430,9 @@ export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {
389
430
  for (const failure of failures) {
390
431
  process.stderr.write(`${FRAMEWORK_NAME}: plugin ${failure.package}: ${failure.reason}\n`);
391
432
  }
433
+ if (report.audienceError) {
434
+ process.stderr.write(`${FRAMEWORK_NAME}: ${report.audienceError}\n`);
435
+ process.exitCode = 1;
436
+ }
392
437
  if (failures.length > 0) process.exitCode = 1;
393
438
  }
@@ -9,6 +9,12 @@ import {
9
9
  resolveInstallContext,
10
10
  } from "../../lib/install";
11
11
  import { renderInstallExecution } from "../install-plan";
12
+ import {
13
+ describeUnknownAudience,
14
+ installPluginDeclarations,
15
+ readAudienceSelection,
16
+ resolveAudience,
17
+ } from "../../tools/setup/dynamic-plugins/audiences";
12
18
  import { hydrateDynamicPlugins } from "../../tools/setup/dynamic-plugins/hydrate";
13
19
  import {
14
20
  installDeclaredPlugins,
@@ -69,19 +75,31 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
69
75
  process.exitCode = 1;
70
76
  return false;
71
77
  }
78
+ if (context.kind === "companion") {
79
+ const audience = resolveAudience(context.config, readAudienceSelection(process.env));
80
+ if (!audience.ok) {
81
+ process.stderr.write(`${FRAMEWORK_NAME}: ${describeUnknownAudience(audience)}\n`);
82
+ process.exitCode = 1;
83
+ return false;
84
+ }
85
+ }
86
+ const declarations =
87
+ context.kind === "companion" || context.kind === "hub"
88
+ ? installPluginDeclarations(context.config)
89
+ : [];
72
90
  // Root-declared plugins (companion or hub) install first and hydrate into
73
91
  // the registry before the plan is built, so one run goes install → load →
74
92
  // plan and their own install requirements are part of the plan.
75
93
  if (
76
94
  (context.kind === "companion" || context.kind === "hub") &&
77
95
  context.companionPath &&
78
- context.config.plugins?.length
96
+ declarations.length
79
97
  ) {
80
98
  let results: PluginInstallResult[];
81
99
  try {
82
100
  results = frozen
83
- ? await installDeclaredPluginsFrozen(context.companionPath, context.config.plugins)
84
- : await installDeclaredPlugins(context.companionPath, context.config.plugins);
101
+ ? await installDeclaredPluginsFrozen(context.companionPath, declarations)
102
+ : await installDeclaredPlugins(context.companionPath, declarations);
85
103
  } catch (error) {
86
104
  if (!(error instanceof FrozenInstallError)) throw error;
87
105
  process.stderr.write(`${FRAMEWORK_NAME}: frozen plugin install refused: ${error.message}\n`);
@@ -3,6 +3,10 @@ import path from "node:path";
3
3
 
4
4
  import { FRAMEWORK_NAME } from "../../../framework";
5
5
  import { ConfigStore, mergeWithDefaults } from "../../../lib/orchestrator/config-store";
6
+ import {
7
+ effectiveStudioAgent,
8
+ readAudienceSelection,
9
+ } from "../../../tools/setup/dynamic-plugins/audiences";
6
10
  import type { StudioInventory, StudioInventoryCompanion } from "./inventory";
7
11
  import { findAction, promptArgs, skillInstalledFor, SUBJECT_PATTERN } from "./actions";
8
12
  import { collectSkillInventory, type StudioSkillInventory } from "./mate-inventory";
@@ -45,18 +49,24 @@ export interface DefaultAgentOutcome {
45
49
  }
46
50
 
47
51
  /**
48
- * The companion's `studio.terminal.agent` for one provider. Never throws and
49
- * never echoes the configured value into the notice.
52
+ * The companion's effective default agent for one provider: the active
53
+ * audience's `studio.terminal.agent` (Studio's own `MATE_AUDIENCE`, never the
54
+ * browser's), else the base one. Never throws and never echoes the configured
55
+ * value into the notice.
50
56
  */
51
57
  export async function defaultAgentFor(
52
58
  companionPath: string,
53
59
  agent: TerminalAgent,
60
+ env: Record<string, string | undefined> = process.env,
54
61
  ): Promise<DefaultAgentOutcome> {
55
62
  const configPath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
56
63
  if (!fs.existsSync(configPath)) return {};
57
64
  let name: unknown;
58
65
  try {
59
- name = (await new ConfigStore(configPath).load()).studio?.terminal?.agent;
66
+ name = effectiveStudioAgent(
67
+ await new ConfigStore(configPath).load(),
68
+ readAudienceSelection(env),
69
+ );
60
70
  } catch {
61
71
  return {};
62
72
  }
@@ -141,6 +141,111 @@ function validatePluginDeclarations(config: FrameworkConfig): void {
141
141
  }
142
142
  }
143
143
 
144
+ export const AUDIENCE_NAME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
145
+
146
+ const AUDIENCE_KEYS = ["plugins", "studio"] as const;
147
+
148
+ function isRecord(value: unknown): value is Record<string, unknown> {
149
+ return typeof value === "object" && value !== null && !Array.isArray(value);
150
+ }
151
+
152
+ function validateAudiencePlugins(audience: string, plugins: unknown): Map<string, string> {
153
+ if (!Array.isArray(plugins)) {
154
+ throw new ConfigError(`audience "${audience}": plugins must be a list.`);
155
+ }
156
+ const versions = new Map<string, string>();
157
+ for (const entry of plugins) {
158
+ if (
159
+ !isRecord(entry) ||
160
+ typeof entry.package !== "string" ||
161
+ typeof entry.version !== "string"
162
+ ) {
163
+ throw new ConfigError(
164
+ `audience "${audience}": plugins entry must have string package and version: ${JSON.stringify(entry)}`,
165
+ );
166
+ }
167
+ if (
168
+ entry.policy !== undefined &&
169
+ !PLUGIN_DECLARATION_POLICIES.includes(entry.policy as never)
170
+ ) {
171
+ throw new ConfigError(
172
+ `audience "${audience}": plugins entry "${entry.package}": policy "${String(entry.policy)}" is not allowed for declared plugins (allowed: ${PLUGIN_DECLARATION_POLICIES.join(", ")}).`,
173
+ );
174
+ }
175
+ if (versions.has(entry.package)) {
176
+ throw new ConfigError(
177
+ `audience "${audience}": package "${entry.package}" is declared more than once.`,
178
+ );
179
+ }
180
+ versions.set(entry.package, entry.version);
181
+ }
182
+ return versions;
183
+ }
184
+
185
+ /**
186
+ * Validates the optional `audiences` mapping. Only companions may declare
187
+ * audiences; names are safe identifiers and keys form a closed set so typos
188
+ * fail instead of silently doing nothing.
189
+ */
190
+ function validateAudiences(config: FrameworkConfig): void {
191
+ const audiences = config.audiences as unknown;
192
+ if (audiences === undefined) return;
193
+ if (config.type === "hub") {
194
+ throw new ConfigError('Only companions may declare audiences; a "hub" framework may not.');
195
+ }
196
+ if (!isRecord(audiences)) {
197
+ throw new ConfigError("audiences must be a mapping of audience name to configuration.");
198
+ }
199
+
200
+ const basePackages = new Set((config.plugins ?? []).map((plugin) => plugin.package));
201
+ const seen = new Map<string, { audience: string; version: string }>();
202
+
203
+ for (const [name, audience] of Object.entries(audiences)) {
204
+ if (!AUDIENCE_NAME_PATTERN.test(name)) {
205
+ throw new ConfigError(
206
+ `Invalid audience name "${name}": must match ${AUDIENCE_NAME_PATTERN.source}.`,
207
+ );
208
+ }
209
+ if (!isRecord(audience)) {
210
+ throw new ConfigError(`audience "${name}" must be a mapping.`);
211
+ }
212
+ for (const key of Object.keys(audience)) {
213
+ if (!AUDIENCE_KEYS.includes(key as never)) {
214
+ throw new ConfigError(
215
+ `audience "${name}": unknown key "${key}" (allowed: ${AUDIENCE_KEYS.join(", ")}).`,
216
+ );
217
+ }
218
+ }
219
+
220
+ if (audience.plugins !== undefined) {
221
+ for (const [pkg, version] of validateAudiencePlugins(name, audience.plugins)) {
222
+ if (basePackages.has(pkg)) {
223
+ throw new ConfigError(
224
+ `audience "${name}": package "${pkg}" is also declared in base plugins.`,
225
+ );
226
+ }
227
+ const other = seen.get(pkg);
228
+ if (other && other.version !== version) {
229
+ throw new ConfigError(
230
+ `package "${pkg}" has conflicting versions in audiences "${other.audience}" (${other.version}) and "${name}" (${version}).`,
231
+ );
232
+ }
233
+ seen.set(pkg, other ?? { audience: name, version });
234
+ }
235
+ }
236
+
237
+ if (audience.studio !== undefined) {
238
+ const agent =
239
+ isRecord(audience.studio) && isRecord(audience.studio.terminal)
240
+ ? audience.studio.terminal.agent
241
+ : undefined;
242
+ if (agent !== undefined && typeof agent !== "string") {
243
+ throw new ConfigError(`audience "${name}": studio.terminal.agent must be a string.`);
244
+ }
245
+ }
246
+ }
247
+ }
248
+
144
249
  export class ConfigStore extends YamlFileStore<FrameworkConfig> {
145
250
  constructor(configPath = process.env.MATE_CONFIG ?? defaultConfigPath()) {
146
251
  super(path.resolve(configPath));
@@ -150,6 +255,7 @@ export class ConfigStore extends YamlFileStore<FrameworkConfig> {
150
255
  const merged = mergeWithDefaults(migrateProfilesToAllowedAgents(await super.load()));
151
256
  validateHubConfig(merged);
152
257
  validatePluginDeclarations(merged);
258
+ validateAudiences(merged);
153
259
  return merged;
154
260
  }
155
261
 
@@ -125,6 +125,15 @@ export interface StudioTerminalConfig {
125
125
  agent?: string;
126
126
  }
127
127
 
128
+ /**
129
+ * A named slice of a companion's configuration, activated by `MATE_AUDIENCE`.
130
+ * Plugins add to the base list; `studio.terminal.agent` overrides the base.
131
+ */
132
+ export interface AudienceConfig {
133
+ plugins?: PluginDeclaration[];
134
+ studio?: { terminal?: StudioTerminalConfig };
135
+ }
136
+
128
137
  export interface FrameworkConfig {
129
138
  type?: FrameworkType;
130
139
  git?: GitModeProfile;
@@ -137,6 +146,7 @@ export interface FrameworkConfig {
137
146
  packageManagers?: string[];
138
147
  engines?: EngineConstraints;
139
148
  studio?: { terminal?: StudioTerminalConfig };
149
+ audiences?: Record<string, AudienceConfig>;
140
150
  }
141
151
 
142
152
  export interface CompanionRegistryConfig {
@@ -0,0 +1,128 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { parse } from "yaml";
5
+
6
+ import { FRAMEWORK_NAME } from "../../../framework";
7
+ import type { PluginDeclaration } from "../../../lib/orchestrator/types";
8
+
9
+ export const AUDIENCE_ENV = "MATE_AUDIENCE";
10
+
11
+ export type AudienceResolution =
12
+ | { ok: true; active: string | null }
13
+ | { ok: false; requested: string; declared: string[] };
14
+
15
+ function isRecord(value: unknown): value is Record<string, unknown> {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+
19
+ function pluginList(value: unknown): unknown[] {
20
+ return isRecord(value) && Array.isArray(value.plugins) ? value.plugins : [];
21
+ }
22
+
23
+ function audienceEntry(raw: unknown, audience: string | null): unknown {
24
+ if (audience === null || !isRecord(raw) || !isRecord(raw.audiences)) return undefined;
25
+ return Object.hasOwn(raw.audiences, audience) ? raw.audiences[audience] : undefined;
26
+ }
27
+
28
+ function studioAgent(value: unknown): unknown {
29
+ if (!isRecord(value) || !isRecord(value.studio) || !isRecord(value.studio.terminal)) {
30
+ return undefined;
31
+ }
32
+ return value.studio.terminal.agent;
33
+ }
34
+
35
+ /** `MATE_AUDIENCE` from the environment; unset, empty or blank means none. */
36
+ export function readAudienceSelection(env: Record<string, string | undefined>): string | null {
37
+ const value = env[AUDIENCE_ENV]?.trim();
38
+ return value ? value : null;
39
+ }
40
+
41
+ /** Audience names declared by raw parsed `framework.yaml`, in declared order. */
42
+ export function declaredAudiences(raw: unknown): string[] {
43
+ return isRecord(raw) && isRecord(raw.audiences) ? Object.keys(raw.audiences) : [];
44
+ }
45
+
46
+ export function resolveAudience(raw: unknown, audience: string | null): AudienceResolution {
47
+ if (audience === null) return { ok: true, active: null };
48
+ const declared = declaredAudiences(raw);
49
+ if (declared.includes(audience)) return { ok: true, active: audience };
50
+ return { ok: false, requested: audience, declared };
51
+ }
52
+
53
+ /**
54
+ * Base plugins followed by the active audience's, each declaration keeping
55
+ * its own `policy` and `config`. An unknown audience contributes nothing.
56
+ */
57
+ export function effectivePluginDeclarations(raw: unknown, audience: string | null): unknown[] {
58
+ return [...pluginList(raw), ...pluginList(audienceEntry(raw, audience))];
59
+ }
60
+
61
+ /**
62
+ * Base plugins followed by every audience's, for installation only: a
63
+ * package shared by audiences appears once since validation guarantees one
64
+ * version. Runtime declarations must come from `effectivePluginDeclarations`.
65
+ */
66
+ export function allPluginDeclarations(raw: unknown): unknown[] {
67
+ const all = [...pluginList(raw)];
68
+ for (const name of declaredAudiences(raw)) {
69
+ all.push(...pluginList(audienceEntry(raw, name)));
70
+ }
71
+ const seen = new Set<string>();
72
+ return all.filter((entry) => {
73
+ if (!isRecord(entry) || typeof entry.package !== "string") return true;
74
+ const key = `${entry.package}@${String(entry.version)}`;
75
+ if (seen.has(key)) return false;
76
+ seen.add(key);
77
+ return true;
78
+ });
79
+ }
80
+
81
+ /** Operator-facing message for a `MATE_AUDIENCE` the companion does not declare. */
82
+ export function describeUnknownAudience(
83
+ resolution: Extract<AudienceResolution, { ok: false }>,
84
+ ): string {
85
+ const declared = resolution.declared.length > 0 ? resolution.declared.join(", ") : "none";
86
+ return `unknown audience "${resolution.requested}" (${AUDIENCE_ENV}); declared audiences: ${declared}`;
87
+ }
88
+
89
+ /** Install-time declarations of an already validated config: base plus every audience. */
90
+ export function installPluginDeclarations(config: unknown): PluginDeclaration[] {
91
+ return allPluginDeclarations(config) as PluginDeclaration[];
92
+ }
93
+
94
+ /**
95
+ * The active audience's Studio default agent, else the base one. Returned
96
+ * as found: the caller owns rejecting a value that is not a safe name.
97
+ */
98
+ export function effectiveStudioAgent(raw: unknown, audience: string | null): unknown {
99
+ return studioAgent(audienceEntry(raw, audience)) ?? studioAgent(raw);
100
+ }
101
+
102
+ /**
103
+ * Raw read of the companion's `framework.yaml`. Deliberately avoids
104
+ * `ConfigStore.load()` — hydration runs on every invocation and must never
105
+ * create or migrate config files as a side effect.
106
+ */
107
+ export async function readFrameworkRaw(companionPath: string): Promise<unknown> {
108
+ try {
109
+ const text = await fs.readFile(
110
+ path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
111
+ "utf8",
112
+ );
113
+ return (parse(text) as unknown) ?? null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ export async function readEffectiveDeclarations(
120
+ companionPath: string,
121
+ audience: string | null,
122
+ ): Promise<unknown[]> {
123
+ return effectivePluginDeclarations(await readFrameworkRaw(companionPath), audience);
124
+ }
125
+
126
+ export async function readAllDeclarations(companionPath: string): Promise<unknown[]> {
127
+ return allPluginDeclarations(await readFrameworkRaw(companionPath));
128
+ }
@@ -7,7 +7,14 @@ import { CompanionResolver } from "../../../lib/orchestrator/companion-resolver"
7
7
  import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
8
8
  import type { Plugin } from "../plugin";
9
9
  import type { PluginRegistry } from "../registry";
10
- import { readDeclarations, validateDeclaration } from "./declarations";
10
+ import {
11
+ describeUnknownAudience,
12
+ effectivePluginDeclarations,
13
+ readAudienceSelection,
14
+ readFrameworkRaw,
15
+ resolveAudience,
16
+ } from "./audiences";
17
+ import { validateDeclaration } from "./declarations";
11
18
  import { loadDynamicPlugin, type DynamicPluginLoadDeps } from "./loader";
12
19
 
13
20
  // Packages already registered in this process; re-hydration (e.g. right after
@@ -62,9 +69,9 @@ function activateDeclaredPlugin(plugin: Plugin): Plugin {
62
69
  }
63
70
 
64
71
  /**
65
- * Registers the companion's declared plugins into the active registry —
66
- * compiled-in plugins first (they are already registered), declared order
67
- * after. Runs before cap-command detection on every invocation. No-op
72
+ * Registers the companion's effective plugins (base plus the active
73
+ * `MATE_AUDIENCE`) into the active registry — compiled-in plugins first
74
+ * (they are already registered), declared order after. Runs before cap-command detection on every invocation. No-op
68
75
  * without a companion or `plugins:` entries; all diagnostics go to stderr;
69
76
  * never throws.
70
77
  */
@@ -77,7 +84,12 @@ export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}
77
84
  deps.companionPath ?? (await resolveCompanionQuietly(deps.cwd ?? process.cwd(), env));
78
85
  if (!companionPath) return;
79
86
 
80
- const entries = await readDeclarations(companionPath);
87
+ const raw = await readFrameworkRaw(companionPath);
88
+ const audience = resolveAudience(raw, readAudienceSelection(env));
89
+ if (!audience.ok) {
90
+ warn(`${describeUnknownAudience(audience)}; applying the base configuration only`);
91
+ }
92
+ const entries = effectivePluginDeclarations(raw, audience.ok ? audience.active : null);
81
93
  if (entries.length === 0) return;
82
94
 
83
95
  const registry = deps.registry ?? getActiveDistribution().registry;
@@ -2,7 +2,8 @@ import fs from "node:fs/promises";
2
2
 
3
3
  import { FRAMEWORK_NAME } from "../../../framework";
4
4
  import type { PluginDeclaration } from "../../../lib/orchestrator/types";
5
- import { readDeclarations, validateDeclaration } from "./declarations";
5
+ import { readAudienceSelection, readEffectiveDeclarations } from "./audiences";
6
+ import { validateDeclaration } from "./declarations";
6
7
  import { loadDynamicPlugin, type DynamicPluginLoadDeps } from "./loader";
7
8
  import { pluginPackageRoot } from "./paths";
8
9
 
@@ -18,7 +19,8 @@ export interface PluginInspection {
18
19
  }
19
20
 
20
21
  /**
21
- * Strict, installation-free check of every declared plugin: installed
22
+ * Strict, installation-free check of every effective plugin (base plus the
23
+ * active `MATE_AUDIENCE`; inactive audiences are not checked): installed
22
24
  * and loadable with the effective environment. Unlike hydration it fails
23
25
  * closed on the first-class problems ordinary commands only warn about.
24
26
  */
@@ -37,7 +39,7 @@ export async function inspectDeclaredPlugins(
37
39
  const failures: PluginVerificationFailure[] = [];
38
40
  const capabilities: string[] = [];
39
41
  const declarations: PluginDeclaration[] = [];
40
- for (const entry of await readDeclarations(companionPath)) {
42
+ for (const entry of await readEffectiveDeclarations(companionPath, readAudienceSelection(env))) {
41
43
  const { declaration, error } = validateDeclaration(entry);
42
44
  if (declaration) declarations.push(declaration);
43
45
  else failures.push({ package: "(malformed entry)", reason: error ?? "invalid plugins entry" });
@@ -28,6 +28,12 @@ import { createUvPlugin } from "./setup/package-managers/uv";
28
28
  import type { PackageManagerSetupDeps } from "./setup/package-managers/uv";
29
29
  import { applyRequiredSelectionsToConfig } from "./setup/policy";
30
30
  import { invalidateInstallState } from "../lib/install";
31
+ import {
32
+ describeUnknownAudience,
33
+ installPluginDeclarations,
34
+ readAudienceSelection,
35
+ resolveAudience,
36
+ } from "./setup/dynamic-plugins/audiences";
31
37
  import { hydrateDynamicPlugins } from "./setup/dynamic-plugins/hydrate";
32
38
  import { installDeclaredPlugins } from "./setup/dynamic-plugins/install";
33
39
  import {
@@ -139,6 +145,8 @@ export async function executeSetup(
139
145
  deps.configStore ??
140
146
  new ConfigStore(path.join(cwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
141
147
  const config = mergeWithDefaults(await configStore.load());
148
+ const audience = resolveAudience(config, readAudienceSelection(process.env));
149
+ if (!audience.ok) throw new ConfigError(describeUnknownAudience(audience));
142
150
  if (input.allowedAgents !== undefined) {
143
151
  config.allowedAgents = [...new Set(input.allowedAgents)];
144
152
  }
@@ -162,8 +170,9 @@ export async function executeSetup(
162
170
  // Declared plugin packages install and hydrate before the setup plan runs,
163
171
  // so a fresh clone reaches a fully applied state from one setup run
164
172
  // (install → load → plan).
165
- if (config.plugins?.length) {
166
- for (const result of await installDeclaredPlugins(companionPath, config.plugins)) {
173
+ const declarations = installPluginDeclarations(config);
174
+ if (declarations.length) {
175
+ for (const result of await installDeclaredPlugins(companionPath, declarations)) {
167
176
  if (result.status === "failed") {
168
177
  process.stderr.write(
169
178
  `${FRAMEWORK_NAME}: plugin "${result.package}" failed to install: ${result.error ?? "unknown error"}\n`,