@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.
- package/opencode-plugin/src/add-dir.ts +76 -10
- package/opencode-plugin/src/companion.ts +1 -8
- package/opencode-plugin/src/server.ts +2 -15
- package/package.json +1 -1
- package/src/cli/commands/artifact/finish/openspec.ts +40 -8
- package/src/cli/commands/doctor.ts +92 -86
- package/src/cli/commands/install.ts +0 -8
- package/src/cli/commands/studio/terminal-launch.ts +7 -16
- package/src/cli/commands/wrap.ts +4 -29
- package/src/cli/usage.ts +1 -2
- package/src/hooks/session-guidance.ts +2 -20
- package/src/lib/orchestrator/adapters/claude.ts +20 -130
- package/src/lib/orchestrator/adapters/opencode.ts +7 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- package/src/lib/orchestrator/projection-claude-skills.ts +35 -88
- package/src/lib/orchestrator/projection-entries.ts +0 -1
- package/src/lib/orchestrator/projection-types.ts +0 -2
- package/src/lib/orchestrator/session-prepare.ts +4 -82
- package/src/lib/orchestrator/wrap-pass.ts +1 -88
- package/src/runtime/guidance.ts +0 -17
- package/src/runtime/policy.ts +1 -14
- package/src/runtime/runtime-document-manifest.ts +0 -17
- package/src/runtime/wrap-state.ts +0 -4
- package/src/tools/setup/capabilities/openspec.ts +140 -18
- package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
- package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
- package/src/tools/setup/engine.ts +10 -141
- package/src/tools/setup/plugin.ts +4 -19
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- package/src/tools/setup/providers/claude.ts +2 -27
- package/src/tools/setup/providers/opencode.ts +0 -10
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup.ts +6 -9
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/runtime/audience-layer.ts +0 -84
- package/src/runtime/opencode-audience-layer.ts +0 -176
- package/src/tools/setup/audience-isolation.fixture.ts +0 -325
- package/src/tools/setup/audience-layer.ts +0 -207
- package/src/tools/setup/audience-registrations.ts +0 -52
- package/src/tools/setup/contribution-ledger.ts +0 -207
- package/src/tools/setup/isolation-diagnostics.ts +0 -139
- package/src/tools/setup/upgrade-sweep.ts +0 -153
package/src/cli/commands/wrap.ts
CHANGED
|
@@ -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
|
|
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
|
|
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]
|
|
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
|
|
38
|
-
if (!
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
*
|
|
91
|
-
*
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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);
|