@uniqbit/mate-core 0.18.3 → 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.
@@ -6,6 +6,7 @@ import { MATE_ENV } from "../../src/runtime/env";
6
6
  import { COMPANION_POLICY_MARKER, type MateGuidanceFile } from "../../src/runtime/guidance";
7
7
 
8
8
  import type { CompanionContext } from "../../src/opencode/companion-policy";
9
+ import type { LaunchEnvironment } from "./launch-environment";
9
10
  import { resolveOpenCodeGuidance } from "../../src/opencode/projected-guidance";
10
11
  import {
11
12
  runtimeDirectoryPath,
@@ -23,8 +24,8 @@ function prependPathEntry(pathValue: string | undefined, entry: string): string
23
24
  * keeps current. `null` when neither exists: guidance naming an unresolved
24
25
  * placeholder would send the model to a path that is not there.
25
26
  */
26
- function resolveWrapperBinPath(): string | null {
27
- if (process.env.MATE_WRAPPER_BIN_PATH) return process.env.MATE_WRAPPER_BIN_PATH;
27
+ function resolveWrapperBinPath(env: LaunchEnvironment): string | null {
28
+ if (env.MATE_WRAPPER_BIN_PATH) return env.MATE_WRAPPER_BIN_PATH;
28
29
  const runtime = runtimeWrapperBinPath();
29
30
  return fs.existsSync(runtime) ? runtime : null;
30
31
  }
@@ -54,8 +55,8 @@ function buildStartupError(details: string[]): Error {
54
55
  * an Unmanaged Session in a wrapped repository builds the same payload from the
55
56
  * companion the projection names, with the Capability names read live.
56
57
  */
57
- function loadGuidance(directory: string): MateGuidanceFile | null {
58
- const { guidance, errors } = resolveOpenCodeGuidance(process.env, directory);
58
+ function loadGuidance(env: LaunchEnvironment, directory: string): MateGuidanceFile | null {
59
+ const { guidance, errors } = resolveOpenCodeGuidance(env, directory);
59
60
  if (errors.length > 0) throw buildStartupError(errors);
60
61
  return guidance;
61
62
  }
@@ -101,9 +102,10 @@ function applyCompanionShellEnv(
101
102
  context: CompanionContext,
102
103
  env: Record<string, string | undefined>,
103
104
  wrapperBinPath: string,
105
+ launchEnv: LaunchEnvironment,
104
106
  ): void {
105
107
  env.MATE_NAME = context.frameworkName;
106
- env.MATE_VERSION = process.env.MATE_VERSION ?? "unknown";
108
+ env.MATE_VERSION = launchEnv.MATE_VERSION ?? "unknown";
107
109
  env.MATE_ARTIFACT_PATH = context.companionPath;
108
110
  env.MATE_WRAPPER_BIN_PATH = wrapperBinPath;
109
111
  if (context.repositoryPath) {
@@ -171,12 +173,20 @@ function mergeCompanionSystem(system: unknown[], companion: string): string[] {
171
173
  return parts.length > 0 ? [parts.join("\n\n")] : [];
172
174
  }
173
175
 
174
- /** Registers guidance, compaction context, shell env, and `companion_paths`. */
175
- export async function registerCompanion(api: Context, context: CompanionContext): Promise<void> {
176
- const guidance = loadGuidance(api.location.directory);
176
+ /**
177
+ * Registers guidance, compaction context, shell env, and `companion_paths`.
178
+ * `env` is the launch environment the caller decided to trust; `{}` resolves
179
+ * everything from the Projection Root.
180
+ */
181
+ export async function registerCompanion(
182
+ api: Context,
183
+ context: CompanionContext,
184
+ env: LaunchEnvironment = process.env,
185
+ ): Promise<void> {
186
+ const guidance = loadGuidance(env, api.location.directory);
177
187
  if (!guidance) return;
178
188
  /** Reported, not thrown: the guardrails registered before this must stay live. */
179
- const wrapperBinPath = resolveWrapperBinPath();
189
+ const wrapperBinPath = resolveWrapperBinPath(env);
180
190
  if (!wrapperBinPath) {
181
191
  console.error(`mate: ${missingRuntimeDirectoryMessage()}`);
182
192
  return;
@@ -199,7 +209,7 @@ export async function registerCompanion(api: Context, context: CompanionContext)
199
209
  });
200
210
 
201
211
  await api.shell.hook("create.before", (event) => {
202
- applyCompanionShellEnv(context, event.env, wrapperBinPath);
212
+ applyCompanionShellEnv(context, event.env, wrapperBinPath, env);
203
213
  });
204
214
 
205
215
  await api.tool.transform((editor) => {
@@ -0,0 +1,29 @@
1
+ import { MATE_ENV } from "../../src/runtime/env";
2
+
3
+ export type LaunchEnvironment = Record<string, string | undefined>;
4
+
5
+ /**
6
+ * True inside OpenCode's shared background service (`opencode serve --service`),
7
+ * which hosts plugins in-process for every bare `opencode` session. Its
8
+ * environment belongs to whichever process started it, not to any session.
9
+ */
10
+ export function runsInSharedService(argv: readonly string[]): boolean {
11
+ return argv.includes("serve") && argv.includes("--service");
12
+ }
13
+
14
+ /**
15
+ * The environment the plugin may treat as a Mate launch environment: `env`
16
+ * itself for a managed launch, `{}` otherwise so every reader falls back to
17
+ * the Projection Root.
18
+ *
19
+ * Inherited `MATE_*` variables prove nothing — every shell under a Mate session
20
+ * carries them. The guidance payload does: only `mate opencode` sets it, and
21
+ * the plugin masks it in every shell it configures.
22
+ */
23
+ export function pluginLaunchEnvironment(
24
+ env: LaunchEnvironment,
25
+ argv: readonly string[],
26
+ ): LaunchEnvironment {
27
+ const managed = Boolean(env[MATE_ENV.guidanceJson]?.trim()) && !runsInSharedService(argv);
28
+ return managed ? env : {};
29
+ }
@@ -12,14 +12,15 @@ import { readWrapStateSync } from "../../src/runtime/wrap-state";
12
12
 
13
13
  import { registerCompanionAccess } from "./add-dir";
14
14
  import { missingRuntimeDirectoryMessage, registerCompanion, resolveMateBin } from "./companion";
15
+ import { pluginLaunchEnvironment, type LaunchEnvironment } from "./launch-environment";
15
16
 
16
17
  /**
17
18
  * Only a location in a repository wrapped for OpenCode prepares, and only
18
19
  * without a launch environment: `mate opencode` has already prepared the
19
20
  * session it starts.
20
21
  */
21
- async function registerPrepare(api: Context, companionPath: string) {
22
- if (hasLaunchEnvironment(process.env)) return undefined;
22
+ async function registerPrepare(api: Context, companionPath: string, env: LaunchEnvironment) {
23
+ if (hasLaunchEnvironment(env)) return undefined;
23
24
  const repoRoot = repositoryWrappedForOpenCode(api.location.directory);
24
25
  if (!repoRoot) return undefined;
25
26
  const mateBin = resolveMateBin();
@@ -39,19 +40,22 @@ async function registerPrepare(api: Context, companionPath: string) {
39
40
  /**
40
41
  * Mate OpenCode server plugin (OpenCode 2.x). Loaded through the package's
41
42
  * `./server` export; stays inert when no companion resolves from the launch
42
- * environment or a Projection Root above the session directory.
43
+ * environment or a Projection Root above the session directory. The launch
44
+ * environment is decided once here: outside a managed launch every reader
45
+ * gets `{}`, so an environment inherited by the shared service never counts.
43
46
  */
44
47
  export const MateOpenCodePlugin = Plugin.define({
45
48
  id: "mate-opencode-plugin",
46
49
  async setup(api) {
47
- const context = readContext(process.env, api.location.directory);
50
+ const env = pluginLaunchEnvironment(process.env, process.argv);
51
+ const context = readContext(env, api.location.directory);
48
52
  if (!context.companionPath) return;
49
53
 
50
54
  await registerCompanionAccess(api, context.companionPath);
51
- await registerCompanionHooks(api, context);
52
- await registerCompanion(api, context);
55
+ await registerCompanionHooks(api, context, env);
56
+ await registerCompanion(api, context, env);
53
57
  /** Last: the guardrails above are live while the local phase holds `setup`. */
54
- const prepareCleanup = await registerPrepare(api, context.companionPath);
58
+ const prepareCleanup = await registerPrepare(api, context.companionPath, env);
55
59
  return async () => {
56
60
  await prepareCleanup?.();
57
61
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -73,9 +73,12 @@ type ProjectionResult =
73
73
  | { ok: true; repository: string; areas: string[] }
74
74
  | { ok: false; reason: string };
75
75
 
76
+ /** A leading `---` block; group 1 is its YAML, the match ends after the closing line. */
77
+ const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
78
+
76
79
  function parseDeltaScopes(source: string): ScopePair[] {
77
80
  const text = source.startsWith(BOM) ? source.slice(BOM.length) : source;
78
- const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
81
+ const match = FRONTMATTER_PATTERN.exec(text);
79
82
  if (!match) return [];
80
83
 
81
84
  try {
@@ -95,6 +98,26 @@ function parseDeltaScopes(source: string): ScopePair[] {
95
98
  }
96
99
  }
97
100
 
101
+ const DELTA_TAGS = new Set(["openspec/change", "openspec/delta"]);
102
+
103
+ /**
104
+ * A header an agent copied from a delta: it declares `type: delta-spec`, carries a
105
+ * `change` key, or a delta tag. A nested `scopes` key alone does not count, since
106
+ * legacy canonical specs carry one.
107
+ */
108
+ function isDeltaShapedFrontmatter(yamlSource: string): boolean {
109
+ try {
110
+ const parsed = parse(yamlSource) as unknown;
111
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
112
+ const header = parsed as Record<string, unknown>;
113
+ if (header.type === "delta-spec" || "change" in header) return true;
114
+ const tags = Array.isArray(header.tags) ? header.tags : [header.tags];
115
+ return tags.some((tag) => typeof tag === "string" && DELTA_TAGS.has(tag));
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
98
121
  function projectDeltaScopes(scopes: ScopePair[]): ProjectionResult {
99
122
  if (scopes.length === 0) return { ok: false, reason: "delta declares no scopes entries" };
100
123
  const repositories = [...new Set(scopes.map((entry) => entry.repository))];
@@ -133,7 +156,10 @@ async function archivedChangeUsesMateSchema(
133
156
  }
134
157
  }
135
158
 
136
- /** Scope keys are omitted when the delta had none — a partial block beats no frontmatter. */
159
+ /**
160
+ * The block through its closing line. Scope keys are omitted when the delta had
161
+ * none — a partial block beats no frontmatter.
162
+ */
137
163
  function renderCanonicalFrontmatter(
138
164
  capability: string,
139
165
  scope: { repository: string; areas: string[] } | null,
@@ -146,14 +172,14 @@ function renderCanonicalFrontmatter(
146
172
  "tags: [openspec/spec]",
147
173
  "---",
148
174
  "",
149
- "",
150
175
  ].join("\n");
151
176
  }
152
177
 
153
178
  /**
154
179
  * Repairs `openspec archive` output: prepends canonical frontmatter to main specs the
155
- * archive run created bare. Publish does not archive, so this runs the first time publish
156
- * resolves that archive, however the archive was made.
180
+ * archive run created bare, and replaces a delta-shaped header an agent copied onto one.
181
+ * Publish does not archive, so this runs the first time publish resolves that archive,
182
+ * however the archive was made.
157
183
  *
158
184
  * `openspec archive` rebuilds a brand-new main spec from a skeleton with no frontmatter slot,
159
185
  * so scope metadata dies exactly once, at spec birth; existing specs keep theirs because only
@@ -197,8 +223,14 @@ async function reconcileMainSpecFrontmatter(
197
223
  const canonicalPath = path.join(companionPath, canonicalRelative);
198
224
 
199
225
  try {
200
- const canonical = await fs.readFile(canonicalPath, "utf8");
201
- if (canonical.replace(BOM, "").startsWith("---")) continue;
226
+ const canonical = (await fs.readFile(canonicalPath, "utf8")).replace(BOM, "");
227
+ /** The body that follows the written block: the whole file, or what trails a delta-shaped header. */
228
+ let body = `\n${canonical}`;
229
+ if (canonical.startsWith("---")) {
230
+ const header = FRONTMATTER_PATTERN.exec(canonical);
231
+ if (!header || !isDeltaShapedFrontmatter(header[1])) continue;
232
+ body = canonical.slice(header[0].length);
233
+ }
202
234
 
203
235
  const deltaSource = await fs.readFile(path.join(directory, entry.name), "utf8");
204
236
  const scopes = parseDeltaScopes(deltaSource);
@@ -215,7 +247,7 @@ async function reconcileMainSpecFrontmatter(
215
247
  capability,
216
248
  projected.ok ? { repository: projected.repository, areas: projected.areas } : null,
217
249
  );
218
- await fs.writeFile(canonicalPath, head + canonical.replace(BOM, ""), "utf8");
250
+ await fs.writeFile(canonicalPath, head + body, "utf8");
219
251
  reconciled.push(canonicalRelative);
220
252
  } catch (error) {
221
253
  process.stderr.write(
@@ -1,6 +1,3 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
1
  import {
5
2
  hasGraphifyCapability,
6
3
  hasTokensaveCapability,
@@ -10,7 +7,7 @@ import type { CapabilityConfig } from "../../../lib/orchestrator/types";
10
7
  import {
11
8
  ensureTokensaveBranchingPosture,
12
9
  ensureTokensaveStoreExcluded,
13
- TOKENSAVE_STORE_DIR,
10
+ isTokensaveIndexed,
14
11
  } from "../../../tools/setup/capabilities/tokensave";
15
12
  import { ensureUnambiguousCompanion } from "../shared/companion-selection";
16
13
  import { runGraphifyCapCommand } from "./graphify";
@@ -43,13 +40,7 @@ async function defaultIsTokensaveInitialized(): Promise<boolean> {
43
40
  const repo =
44
41
  repository ?? (await workingRepoStore.load()).repos.find((entry) => entry.id === repositoryId);
45
42
  if (!repo) return false;
46
-
47
- try {
48
- const stat = await fs.stat(path.join(repo.path, TOKENSAVE_STORE_DIR));
49
- return stat.isDirectory();
50
- } catch {
51
- return false;
52
- }
43
+ return isTokensaveIndexed(repo.path);
53
44
  }
54
45
 
55
46
  // The reused cap commands (runGraphifyCapCommand / runTokensaveCapCommand) signal
@@ -1,9 +1,13 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
 
5
+ import { parse as parseYaml } from "yaml";
6
+
4
7
  import { FRAMEWORK_NAME } from "../../framework";
5
8
  import { gitEnvironment } from "../../runtime/companion-git";
6
9
  import { getActiveDistribution } from "../../distribution";
10
+ import { hasOpenspecCapability } from "../../lib/orchestrator/capabilities";
7
11
  import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
8
12
  import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
9
13
  import { pathIsDirectory } from "../../lib/fs-utils";
@@ -73,6 +77,8 @@ export interface DoctorReport {
73
77
  /** IDs of the capabilities the verified plugins provide. */
74
78
  pluginCapabilities?: string[];
75
79
  engineRequirement?: { range: string; ok: boolean; detail: string };
80
+ /** Present when the companion's `openspec/config.yaml` has `rules` keys OpenSpec ignores. */
81
+ openspecRuleWarnings?: Array<{ key: string; message: string }>;
76
82
  hub?: { members: DoctorHubMember[] };
77
83
  resolutionFailures: Array<{ companionPath: string; message: string }>;
78
84
  }
@@ -239,6 +245,70 @@ function collectAudiences(
239
245
  if (!resolution.ok) report.audienceError = describeUnknownAudience(resolution);
240
246
  }
241
247
 
248
+ /** Artifact IDs of OpenSpec's built-in `spec-driven` schema. */
249
+ const BUILT_IN_OPENSPEC_ARTIFACTS = ["proposal", "specs", "design", "tasks"];
250
+
251
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
252
+ return value && typeof value === "object" && !Array.isArray(value)
253
+ ? (value as Record<string, unknown>)
254
+ : undefined;
255
+ }
256
+
257
+ /** Built-in IDs plus every `artifacts[].id` from a `schema.yaml` under the companion's `openspec/schemas/`. */
258
+ async function knownOpenSpecArtifactIds(companionPath: string): Promise<Set<string>> {
259
+ const schemasDir = path.join(companionPath, "openspec", "schemas");
260
+ let schemaNames: string[] = [];
261
+ try {
262
+ schemaNames = await fs.readdir(schemasDir);
263
+ } catch {
264
+ /* no companion schemas */
265
+ }
266
+ const schemaIds = await Promise.all(
267
+ schemaNames.map(async (name) => {
268
+ try {
269
+ const schema = asRecord(
270
+ parseYaml(await fs.readFile(path.join(schemasDir, name, "schema.yaml"), "utf8")),
271
+ );
272
+ const artifacts: unknown[] = Array.isArray(schema?.artifacts) ? schema.artifacts : [];
273
+ return artifacts.flatMap((artifact) => {
274
+ const id = asRecord(artifact)?.id;
275
+ return typeof id === "string" ? [id] : [];
276
+ });
277
+ } catch {
278
+ return []; // not a readable schema
279
+ }
280
+ }),
281
+ );
282
+ return new Set([...BUILT_IN_OPENSPEC_ARTIFACTS, ...schemaIds.flat()]);
283
+ }
284
+
285
+ /**
286
+ * OpenSpec ignores a `rules` key that names no artifact, warning only on stderr
287
+ * of the command that reads it. Read-only; a missing or unparseable config is silent.
288
+ */
289
+ async function collectOpenSpecRuleWarnings(
290
+ companionPath: string,
291
+ ): Promise<NonNullable<DoctorReport["openspecRuleWarnings"]>> {
292
+ let rules: Record<string, unknown> | undefined;
293
+ try {
294
+ const config = parseYaml(
295
+ await fs.readFile(path.join(companionPath, "openspec", "config.yaml"), "utf8"),
296
+ );
297
+ rules = asRecord(asRecord(config)?.rules);
298
+ } catch {
299
+ return [];
300
+ }
301
+ if (!rules) return [];
302
+
303
+ const known = await knownOpenSpecArtifactIds(companionPath);
304
+ return Object.keys(rules)
305
+ .filter((key) => !known.has(key))
306
+ .map((key) => ({
307
+ key,
308
+ message: `rules.${key} matches no OpenSpec artifact and is ignored; put workflow guidance in operations.apply.guidance or operations.archive.guidance`,
309
+ }));
310
+ }
311
+
242
312
  export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<DoctorReport> {
243
313
  const cwd = path.resolve(deps.cwd ?? process.cwd());
244
314
  const pathValue = deps.pathValue ?? process.env.PATH ?? "";
@@ -277,6 +347,15 @@ export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<Doctor
277
347
  collectAudiences(report, root.config, env);
278
348
  }
279
349
 
350
+ if (
351
+ root.rootPath &&
352
+ (kind === "working" || kind === "companion") &&
353
+ hasOpenspecCapability(root.config?.capabilities)
354
+ ) {
355
+ const warnings = await collectOpenSpecRuleWarnings(root.rootPath);
356
+ if (warnings.length > 0) report.openspecRuleWarnings = warnings;
357
+ }
358
+
280
359
  if (root.rootPath && root.config) {
281
360
  const inspect =
282
361
  deps.inspectPlugins ?? ((companionPath) => inspectDeclaredPlugins(companionPath, { env }));
@@ -398,6 +477,15 @@ function renderHumanReport(report: DoctorReport): void {
398
477
  if (report.engineRequirement) {
399
478
  printSection("Version Requirement", report.engineRequirement.detail);
400
479
  }
480
+ if (report.openspecRuleWarnings) {
481
+ printSection(
482
+ "OpenSpec Config",
483
+ renderList(
484
+ report.openspecRuleWarnings.map((warning) => warning.message),
485
+ "",
486
+ ),
487
+ );
488
+ }
401
489
 
402
490
  printSection(
403
491
  "Resolution Failures",
@@ -88,7 +88,8 @@ export abstract class LaunchAdapter<C extends AdapterContext = AdapterContext> {
88
88
 
89
89
  async validateLaunch(_context: C): Promise<void> {}
90
90
 
91
- extendEnvironment(_context: C): NodeJS.ProcessEnv {
91
+ /** `args` are the operator's arguments, before `buildArgs` rewrites them. */
92
+ extendEnvironment(_context: C, _args: string[] = []): NodeJS.ProcessEnv {
92
93
  return {};
93
94
  }
94
95
 
@@ -130,7 +131,7 @@ export abstract class LaunchAdapter<C extends AdapterContext = AdapterContext> {
130
131
  async prepareLaunch(context: C, args: string[]): Promise<PreparedLaunch> {
131
132
  const builtArgs = this.buildArgs(context, args);
132
133
  const baseEnv = this.environment(context);
133
- const extendedEnv = this.extendEnvironment(context);
134
+ const extendedEnv = this.extendEnvironment(context, args);
134
135
  const env = { ...baseEnv, ...extendedEnv };
135
136
 
136
137
  const processPath = process.env.PATH ?? "";
@@ -36,6 +36,26 @@ function parseOpenCodeMajor(output: string | undefined): number | undefined {
36
36
  return match ? Number(match[1]) : undefined;
37
37
  }
38
38
 
39
+ interface OpenCodeInvocation {
40
+ command: string | undefined;
41
+ rest: string[];
42
+ /**
43
+ * The launch runs its own private server, where the plugin loads with this
44
+ * launch's environment: the TUI or `run`, not pointed at a remote server.
45
+ * Every other invocation may start or reach the shared service, which must
46
+ * never receive a guidance payload.
47
+ */
48
+ hostsPrivateServer: boolean;
49
+ }
50
+
51
+ function readInvocation(launchWorkingDirectory: string, args: string[]): OpenCodeInvocation {
52
+ const isTui = args.length === 0 || args[0]?.startsWith("-");
53
+ const [command, ...rest] = isTui ? [launchWorkingDirectory, ...args] : args;
54
+ const hostsServer = isTui || command === "run";
55
+ const choosesRemoteServer = rest.some((arg) => arg === "--server" || arg.startsWith("--server="));
56
+ return { command, rest, hostsPrivateServer: Boolean(hostsServer) && !choosesRemoteServer };
57
+ }
58
+
39
59
  export class OpenCodeAdapter extends LaunchAdapter {
40
60
  readonly toolName = "opencode";
41
61
  readonly interactive = true;
@@ -67,15 +87,13 @@ export class OpenCodeAdapter extends LaunchAdapter {
67
87
  }
68
88
 
69
89
  buildArgs(context: AdapterContext, args: string[]): string[] {
70
- const codeDir = context.launchWorkingDirectory;
71
- const isTui = args.length === 0 || args[0]?.startsWith("-");
72
- const [command, ...rest] = isTui ? [codeDir, ...args] : args;
73
- if (!command) return args;
74
- const hostsServer = isTui || command === "run";
75
- const choosesServer = rest.some(
76
- (arg) => arg === "--standalone" || arg === "--server" || arg.startsWith("--server="),
90
+ const { command, rest, hostsPrivateServer } = readInvocation(
91
+ context.launchWorkingDirectory,
92
+ args,
77
93
  );
78
- return hostsServer && !choosesServer ? [command, STANDALONE_FLAG, ...rest] : [command, ...rest];
94
+ if (!command) return args;
95
+ const addsStandalone = hostsPrivateServer && !rest.includes(STANDALONE_FLAG);
96
+ return addsStandalone ? [command, STANDALONE_FLAG, ...rest] : [command, ...rest];
79
97
  }
80
98
 
81
99
  /**
@@ -84,11 +102,16 @@ export class OpenCodeAdapter extends LaunchAdapter {
84
102
  * overlay carried now lives in the wrapped `.opencode/opencode.json`. An
85
103
  * inherited `OPENCODE_CONFIG_DIR` or `OPENCODE_CONFIG_CONTENT` passes through.
86
104
  */
87
- extendEnvironment(context: AdapterContext): NodeJS.ProcessEnv {
105
+ extendEnvironment(context: AdapterContext, args: string[] = []): NodeJS.ProcessEnv {
106
+ const { hostsPrivateServer } = readInvocation(context.launchWorkingDirectory, args);
88
107
  return {
89
108
  // Built fresh per launch from the live capability config; always set so
90
109
  // a stale value inherited from an outer Mate session can never leak in.
91
- [MATE_ENV.guidanceJson]: JSON.stringify(this.buildGuidance(context)),
110
+ // Masked unless the launch hosts its own server: a payload that reached
111
+ // the shared service would apply this session's guidance to every other.
112
+ [MATE_ENV.guidanceJson]: hostsPrivateServer
113
+ ? JSON.stringify(this.buildGuidance(context))
114
+ : "",
92
115
  };
93
116
  }
94
117
 
@@ -1,5 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
+ import { normalizeBaseAudience } from "../../tools/setup/dynamic-plugins/audiences";
4
+
3
5
  import { FRAMEWORK_NAME } from "../../framework";
4
6
  import {
5
7
  getDefaultSetupSelections,
@@ -296,8 +298,11 @@ export class ConfigStore extends YamlFileStore<FrameworkConfig> {
296
298
  super(path.resolve(configPath));
297
299
  }
298
300
 
301
+ /** Either file shape loads as the base set at the root; the next save writes the root shape. */
299
302
  override async load(): Promise<FrameworkConfig> {
300
- const merged = mergeWithDefaults(migrateProfilesToAllowedAgents(await super.load()));
303
+ const merged = mergeWithDefaults(
304
+ migrateProfilesToAllowedAgents(normalizeBaseAudience(await super.load())),
305
+ );
301
306
  validateHubConfig(merged);
302
307
  validatePluginDeclarations(merged);
303
308
  validateAudiences(merged);