@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
|
@@ -3,6 +3,8 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
|
|
6
|
+
import { Document, isMap, isScalar, isSeq, parseDocument, type YAMLMap, type YAMLSeq } from "yaml";
|
|
7
|
+
|
|
6
8
|
import { getOpenSpecSchemaSelection } from "../../../lib/orchestrator/setup-compatibilities";
|
|
7
9
|
import { getWrapperBinPath } from "../../../lib/package-paths";
|
|
8
10
|
import { fetchPublicPackageVersion } from "../../../lib/public-npm";
|
|
@@ -55,6 +57,25 @@ const MATE_SCHEMA_SOURCE_ROOT = path.join(
|
|
|
55
57
|
);
|
|
56
58
|
const MATE_CONVENTIONS_SOURCE = path.join(MATE_SCHEMA_SOURCE_ROOT, "openspec-conventions.yaml");
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Identifies Mate's own `rules.specs` entry, so a later release can replace its
|
|
62
|
+
* text instead of adding a second entry.
|
|
63
|
+
*/
|
|
64
|
+
export const MATE_SPECS_RULE_MARKER = "[mate] ";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Delivered through `rules.specs`, the one config hook OpenSpec's archive and
|
|
68
|
+
* sync workflows read when writing main specs. Propose reads it for delta specs
|
|
69
|
+
* too, so the text binds main specs only.
|
|
70
|
+
*/
|
|
71
|
+
export const MATE_SPECS_RULE = `${MATE_SPECS_RULE_MARKER}A main spec at openspec/specs/<capability>/spec.md MUST begin with flat frontmatter: type: spec, capability: <capability>, repository: <org/repository> (one scalar), areas: [<area>, ...] (a flat list in the delta's scope order), and tags: [openspec/spec]. Project repository and areas from the delta's paired scopes; never copy scopes, change, or delta tags onto a main spec. Delta specs under openspec/changes/<name>/specs/ keep their delta frontmatter. Full rules: openspec/mate-conventions.yaml.`;
|
|
72
|
+
|
|
73
|
+
/** What Mate writes besides its entry; anything beyond the two is user content. */
|
|
74
|
+
const MANAGED_CONFIG_SCHEMA_ONLY = "schema: mate-v1";
|
|
75
|
+
|
|
76
|
+
/** Unfolded output keeps the Mate entry on one line and user scalars unwrapped. */
|
|
77
|
+
const CONFIG_TO_STRING = { lineWidth: 0 } as const;
|
|
78
|
+
|
|
58
79
|
/**
|
|
59
80
|
* Runtime → OpenSpec CLI tools. OpenCode reads workflow skills from the shared
|
|
60
81
|
* `agents` root; its own tool only contributes the `/opsx-*` commands.
|
|
@@ -293,20 +314,131 @@ async function ensureOpenSpecInstalled(
|
|
|
293
314
|
return true;
|
|
294
315
|
}
|
|
295
316
|
|
|
296
|
-
|
|
317
|
+
function isMateSpecsRule(item: unknown): boolean {
|
|
318
|
+
return (
|
|
319
|
+
isScalar(item) &&
|
|
320
|
+
typeof item.value === "string" &&
|
|
321
|
+
item.value.startsWith(MATE_SPECS_RULE_MARKER)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The config's `rules.specs` sequence, `null` when absent, or `undefined` when a shape is foreign. */
|
|
326
|
+
function readSpecsRules(doc: Document): YAMLSeq | null | undefined {
|
|
327
|
+
if (doc.contents !== null && !isMap(doc.contents)) return undefined;
|
|
328
|
+
const rules = doc.get("rules", true);
|
|
329
|
+
if (rules === undefined || rules === null) return null;
|
|
330
|
+
if (!isMap(rules)) return undefined;
|
|
331
|
+
const specs = rules.get("specs", true);
|
|
332
|
+
if (specs === undefined || specs === null) return null;
|
|
333
|
+
return isSeq(specs) ? specs : undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Removes Mate's `rules.specs` entry and any `rules.specs` list or `rules` map
|
|
338
|
+
* that leaves empty. Returns whether the document changed.
|
|
339
|
+
*/
|
|
340
|
+
function removeMateSpecsRule(doc: Document): boolean {
|
|
341
|
+
const specs = readSpecsRules(doc);
|
|
342
|
+
if (!specs) return false;
|
|
343
|
+
const before = specs.items.length;
|
|
344
|
+
specs.items = specs.items.filter((item) => !isMateSpecsRule(item));
|
|
345
|
+
if (specs.items.length === before) return false;
|
|
346
|
+
if (specs.items.length === 0) doc.deleteIn(["rules", "specs"]);
|
|
347
|
+
const rules = doc.get("rules", true);
|
|
348
|
+
if (isMap(rules) && rules.items.length === 0) doc.delete("rules");
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Ensures exactly one current Mate entry in `rules.specs`: appended when
|
|
354
|
+
* missing, replaced in place when outdated. Returns whether the document changed.
|
|
355
|
+
*/
|
|
356
|
+
function upsertMateSpecsRule(doc: Document): boolean {
|
|
357
|
+
if (doc.contents === null) doc.contents = doc.createNode({}) as YAMLMap;
|
|
358
|
+
const specs = readSpecsRules(doc);
|
|
359
|
+
if (specs === null) {
|
|
360
|
+
doc.setIn(["rules", "specs"], doc.createNode([MATE_SPECS_RULE]));
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
if (!specs) return false;
|
|
364
|
+
|
|
365
|
+
const index = specs.items.findIndex(isMateSpecsRule);
|
|
366
|
+
if (index === -1) {
|
|
367
|
+
specs.add(doc.createNode(MATE_SPECS_RULE));
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
let changed = false;
|
|
371
|
+
const current = specs.items[index];
|
|
372
|
+
if (isScalar(current) && current.value !== MATE_SPECS_RULE) {
|
|
373
|
+
current.value = MATE_SPECS_RULE;
|
|
374
|
+
changed = true;
|
|
375
|
+
}
|
|
376
|
+
const deduplicated = specs.items.filter((item, i) => i === index || !isMateSpecsRule(item));
|
|
377
|
+
if (deduplicated.length !== specs.items.length) {
|
|
378
|
+
specs.items = deduplicated;
|
|
379
|
+
changed = true;
|
|
380
|
+
}
|
|
381
|
+
return changed;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Removes Mate's own config content: its `rules.specs` entry always, and the
|
|
386
|
+
* file only when what remains is exactly what Mate writes. User content stays.
|
|
387
|
+
*/
|
|
297
388
|
async function removeManagedOpenSpecConfig(companionPath: string): Promise<void> {
|
|
298
389
|
const configPath = getOpenSpecConfigPath(companionPath);
|
|
390
|
+
let content: string;
|
|
299
391
|
try {
|
|
300
|
-
|
|
301
|
-
if (content.trim() === "schema: mate-v1") {
|
|
302
|
-
await fs.rm(configPath, { force: true });
|
|
303
|
-
await pruneEmptyAncestors(path.dirname(configPath), companionPath);
|
|
304
|
-
}
|
|
392
|
+
content = await fs.readFile(configPath, "utf8");
|
|
305
393
|
} catch {
|
|
306
|
-
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let remaining = content;
|
|
398
|
+
if (content.trim() !== MANAGED_CONFIG_SCHEMA_ONLY) {
|
|
399
|
+
const doc = parseDocument(content);
|
|
400
|
+
if (doc.errors.length > 0 || !removeMateSpecsRule(doc)) return;
|
|
401
|
+
remaining = doc.toString(CONFIG_TO_STRING);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (remaining.trim() === MANAGED_CONFIG_SCHEMA_ONLY) {
|
|
405
|
+
await fs.rm(configPath, { force: true });
|
|
406
|
+
await pruneEmptyAncestors(path.dirname(configPath), companionPath);
|
|
407
|
+
} else {
|
|
408
|
+
await fs.writeFile(configPath, remaining, "utf8");
|
|
307
409
|
}
|
|
308
410
|
}
|
|
309
411
|
|
|
412
|
+
/** Writes a fresh Mate config, or merges Mate's entry into an existing one; writes only on change. */
|
|
413
|
+
async function ensureManagedOpenSpecConfig(companionPath: string): Promise<void> {
|
|
414
|
+
const configPath = getOpenSpecConfigPath(companionPath);
|
|
415
|
+
let content: string | undefined;
|
|
416
|
+
try {
|
|
417
|
+
content = await fs.readFile(configPath, "utf8");
|
|
418
|
+
} catch {
|
|
419
|
+
/* no config yet */
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (content === undefined) {
|
|
423
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
424
|
+
const fresh = new Document({ schema: "mate-v1", rules: { specs: [MATE_SPECS_RULE] } });
|
|
425
|
+
await fs.writeFile(configPath, fresh.toString(CONFIG_TO_STRING), "utf8");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// config.yaml is user-editable once created; only Mate's own entry is touched.
|
|
430
|
+
const doc = parseDocument(content);
|
|
431
|
+
if (doc.errors.length > 0) {
|
|
432
|
+
process.stderr.write(
|
|
433
|
+
`openspec: left openspec/config.yaml unchanged, it is not valid YAML: ${doc.errors[0].message.split("\n")[0]}\n`,
|
|
434
|
+
);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (!upsertMateSpecsRule(doc)) return;
|
|
438
|
+
const merged = doc.toString(CONFIG_TO_STRING);
|
|
439
|
+
if (merged !== content) await fs.writeFile(configPath, merged, "utf8");
|
|
440
|
+
}
|
|
441
|
+
|
|
310
442
|
async function teardownOpenSpecSchema(companionPath: string): Promise<void> {
|
|
311
443
|
for (const schemaName of MATE_SCHEMA_NAMES) {
|
|
312
444
|
const schemaDir = getMateSchemaDir(companionPath, schemaName);
|
|
@@ -340,17 +472,7 @@ async function reconcileOpenSpecSchema(ctx: SetupContext): Promise<void> {
|
|
|
340
472
|
return;
|
|
341
473
|
}
|
|
342
474
|
|
|
343
|
-
|
|
344
|
-
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
345
|
-
|
|
346
|
-
// config.yaml is user-editable once created; never overwrite it on re-sync.
|
|
347
|
-
const configExists = await fs
|
|
348
|
-
.access(configPath)
|
|
349
|
-
.then(() => true)
|
|
350
|
-
.catch(() => false);
|
|
351
|
-
if (configExists) return;
|
|
352
|
-
|
|
353
|
-
await fs.writeFile(configPath, "schema: mate-v1\n", "utf8");
|
|
475
|
+
await ensureManagedOpenSpecConfig(ctx.companionPath);
|
|
354
476
|
}
|
|
355
477
|
|
|
356
478
|
export function createOpenspecPlugin(deps: OpenSpecPluginDeps = {}): CapabilityPlugin {
|
|
@@ -8,10 +8,13 @@ import { ConfigError, type PluginDeclaration } from "../../../lib/orchestrator/t
|
|
|
8
8
|
|
|
9
9
|
export const AUDIENCE_ENV = "MATE_AUDIENCE";
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Mate 0.18.4 saved the base set under `audiences.base`. That shape is read
|
|
13
|
+
* back for compatibility, never written: the next save uses the root shape.
|
|
14
|
+
*/
|
|
12
15
|
export const BASE_AUDIENCE = "base";
|
|
13
16
|
|
|
14
|
-
/** The
|
|
17
|
+
/** The keys 0.18.4 moved under `audiences.base`; companion-wide keys stayed at the root. */
|
|
15
18
|
export const BASE_AUDIENCE_KEYS = ["capabilities", "plugins", "studio"] as const;
|
|
16
19
|
|
|
17
20
|
export type AudienceResolution =
|
|
@@ -23,13 +26,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function pluginList(value: unknown): unknown[] {
|
|
26
|
-
if (
|
|
27
|
-
isRecord(value) &&
|
|
28
|
-
isRecord(value.audiences) &&
|
|
29
|
-
Object.hasOwn(value.audiences, BASE_AUDIENCE)
|
|
30
|
-
) {
|
|
31
|
-
return pluginList(normalizeBaseAudience(value));
|
|
32
|
-
}
|
|
33
29
|
return isRecord(value) && Array.isArray(value.plugins) ? value.plugins : [];
|
|
34
30
|
}
|
|
35
31
|
|
|
@@ -91,7 +87,6 @@ export function declaredAudiences(raw: unknown): string[] {
|
|
|
91
87
|
: [];
|
|
92
88
|
}
|
|
93
89
|
|
|
94
|
-
/** `MATE_AUDIENCE=base` selects the base set alone, the same as no audience. */
|
|
95
90
|
export function resolveAudience(raw: unknown, audience: string | null): AudienceResolution {
|
|
96
91
|
if (audience === null || audience === BASE_AUDIENCE) return { ok: true, active: null };
|
|
97
92
|
const declared = declaredAudiences(raw);
|
|
@@ -104,34 +99,7 @@ export function resolveAudience(raw: unknown, audience: string | null): Audience
|
|
|
104
99
|
* its own `policy` and `config`. An unknown audience contributes nothing.
|
|
105
100
|
*/
|
|
106
101
|
export function effectivePluginDeclarations(raw: unknown, audience: string | null): unknown[] {
|
|
107
|
-
return [...pluginList(raw), ...pluginList(audienceEntry(raw, audience))];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** One effective declaration and its source: `null` for base, else the audience name. */
|
|
111
|
-
export interface SourcedDeclaration {
|
|
112
|
-
entry: unknown;
|
|
113
|
-
audience: string | null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** `effectivePluginDeclarations`, each entry marked with the source that declared it. */
|
|
117
|
-
export function effectivePluginDeclarationSources(
|
|
118
|
-
raw: unknown,
|
|
119
|
-
audience: string | null,
|
|
120
|
-
): SourcedDeclaration[] {
|
|
121
|
-
return [
|
|
122
|
-
...pluginList(raw).map((entry) => ({ entry, audience: null })),
|
|
123
|
-
...pluginList(audienceEntry(raw, audience)).map((entry) => ({ entry, audience })),
|
|
124
|
-
];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** The declarations of one audience only, without base. */
|
|
128
|
-
export function audiencePluginDeclarations(raw: unknown, audience: string): unknown[] {
|
|
129
|
-
return pluginList(audienceEntry(raw, audience));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The raw `audiences.<name>` entry, for fingerprinting what a layer depends on. */
|
|
133
|
-
export function audienceDeclarationEntry(raw: unknown, audience: string): unknown {
|
|
134
|
-
return audienceEntry(raw, audience);
|
|
102
|
+
return [...pluginList(normalizeBaseAudience(raw)), ...pluginList(audienceEntry(raw, audience))];
|
|
135
103
|
}
|
|
136
104
|
|
|
137
105
|
/**
|
|
@@ -140,7 +108,7 @@ export function audienceDeclarationEntry(raw: unknown, audience: string): unknow
|
|
|
140
108
|
* version. Runtime declarations must come from `effectivePluginDeclarations`.
|
|
141
109
|
*/
|
|
142
110
|
export function allPluginDeclarations(raw: unknown): unknown[] {
|
|
143
|
-
const all = [...pluginList(raw)];
|
|
111
|
+
const all = [...pluginList(normalizeBaseAudience(raw))];
|
|
144
112
|
for (const name of declaredAudiences(raw)) {
|
|
145
113
|
all.push(...pluginList(audienceEntry(raw, name)));
|
|
146
114
|
}
|
|
@@ -13,7 +13,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Raw read of the companion's
|
|
16
|
+
* Raw read of the companion's `plugins:` list. Deliberately avoids
|
|
17
17
|
* `ConfigStore.load()` — hydration runs on every invocation and must never
|
|
18
18
|
* create or migrate config files as a side effect.
|
|
19
19
|
*/
|
|
@@ -9,7 +9,7 @@ import type { Plugin } from "../plugin";
|
|
|
9
9
|
import type { PluginRegistry } from "../registry";
|
|
10
10
|
import {
|
|
11
11
|
describeUnknownAudience,
|
|
12
|
-
|
|
12
|
+
effectivePluginDeclarations,
|
|
13
13
|
readAudienceSelection,
|
|
14
14
|
readFrameworkRaw,
|
|
15
15
|
resolveAudience,
|
|
@@ -71,8 +71,7 @@ function activateDeclaredPlugin(plugin: Plugin): Plugin {
|
|
|
71
71
|
/**
|
|
72
72
|
* Registers the companion's effective plugins (base plus the active
|
|
73
73
|
* `MATE_AUDIENCE`) into the active registry — compiled-in plugins first
|
|
74
|
-
* (they are already registered), declared order after.
|
|
75
|
-
* marked with its source, so persisted passes can keep to base. Runs before cap-command detection on every invocation. No-op
|
|
74
|
+
* (they are already registered), declared order after. Runs before cap-command detection on every invocation. No-op
|
|
76
75
|
* without a companion or `plugins:` entries; all diagnostics go to stderr;
|
|
77
76
|
* never throws.
|
|
78
77
|
*/
|
|
@@ -90,12 +89,12 @@ export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}
|
|
|
90
89
|
if (!audience.ok) {
|
|
91
90
|
warn(`${describeUnknownAudience(audience)}; applying the base configuration only`);
|
|
92
91
|
}
|
|
93
|
-
const entries =
|
|
92
|
+
const entries = effectivePluginDeclarations(raw, audience.ok ? audience.active : null);
|
|
94
93
|
if (entries.length === 0) return;
|
|
95
94
|
|
|
96
95
|
const registry = deps.registry ?? getActiveDistribution().registry;
|
|
97
96
|
|
|
98
|
-
for (const
|
|
97
|
+
for (const entry of entries) {
|
|
99
98
|
const { declaration, error } = validateDeclaration(entry);
|
|
100
99
|
if (!declaration) {
|
|
101
100
|
if (error) warn(error);
|
|
@@ -123,8 +122,6 @@ export async function hydrateDynamicPlugins(deps: HydrateDynamicPluginsDeps = {}
|
|
|
123
122
|
registry.register({
|
|
124
123
|
plugin: activateDeclaredPlugin(result.plugin),
|
|
125
124
|
policy: declaration.policy ?? "optional",
|
|
126
|
-
audience: source,
|
|
127
|
-
packageName: declaration.package,
|
|
128
125
|
});
|
|
129
126
|
hydratedPackages.add(declaration.package);
|
|
130
127
|
}
|
|
@@ -4,13 +4,6 @@ import { FRAMEWORK_NAME } from "../../framework";
|
|
|
4
4
|
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
5
5
|
import type { RenderedRuntimeDocument } from "../../runtime/runtime-document-manifest";
|
|
6
6
|
import { collectHostingProviders, ContextServiceMediator } from "./context-services";
|
|
7
|
-
import {
|
|
8
|
-
ledgerFromInputs,
|
|
9
|
-
ledgerTeardownInput,
|
|
10
|
-
readContributionLedger,
|
|
11
|
-
writeContributionLedger,
|
|
12
|
-
type ContributionLedger,
|
|
13
|
-
} from "./contribution-ledger";
|
|
14
7
|
import type {
|
|
15
8
|
CapabilityContributionInput,
|
|
16
9
|
CapabilityPlugin,
|
|
@@ -22,8 +15,6 @@ import { reconcileClaudeContributions } from "./providers/claude";
|
|
|
22
15
|
import { reconcileOpenCodeContributions } from "./providers/opencode";
|
|
23
16
|
import { mergeCompanionOpenCodeConfig, renderDocumentsForTarget } from "./runtime-documents";
|
|
24
17
|
import { normalizeRegistration, type NormalizedRegistration } from "./registry";
|
|
25
|
-
import { surfaceRoot } from "./surface-target";
|
|
26
|
-
import { removeForeignGuidanceBlocks, sweepAudienceDeclarations } from "./upgrade-sweep";
|
|
27
18
|
|
|
28
19
|
export interface SetupInstallationPlanAction {
|
|
29
20
|
phase: Plugin["kind"];
|
|
@@ -44,23 +35,11 @@ export interface SetupInstallationOutcome {
|
|
|
44
35
|
warnings: string[];
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
/**
|
|
48
|
-
* The registrations a pass that writes a persisted surface may reconcile:
|
|
49
|
-
* base only. An audience registration takes part in no such pass — neither
|
|
50
|
-
* its lifecycle hooks nor its declared contributions — whatever audience is
|
|
51
|
-
* active; its contributions reach sessions only through the launch layer.
|
|
52
|
-
*/
|
|
53
|
-
export function persistedRegistrations(
|
|
54
|
-
registrations: PluginRegistration[],
|
|
55
|
-
): NormalizedRegistration[] {
|
|
56
|
-
return registrations.map(normalizeRegistration).filter((entry) => entry.audience === null);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
38
|
export function buildSetupInstallationPlan(
|
|
60
39
|
config: FrameworkConfig,
|
|
61
40
|
registrations: PluginRegistration[],
|
|
62
41
|
): SetupInstallationPlan {
|
|
63
|
-
const entries =
|
|
42
|
+
const entries = registrations.map(normalizeRegistration);
|
|
64
43
|
// Required plugins are always treated as enabled: saved selections cannot
|
|
65
44
|
// deselect them and deselection never produces a teardown action.
|
|
66
45
|
const isEntryEnabled = (entry: NormalizedRegistration) =>
|
|
@@ -130,7 +109,7 @@ export async function executeSetupInstallationPlan(
|
|
|
130
109
|
registrations: PluginRegistration[],
|
|
131
110
|
plan: SetupInstallationPlan,
|
|
132
111
|
): Promise<SetupInstallationOutcome> {
|
|
133
|
-
const plugins =
|
|
112
|
+
const plugins = registrations.map((entry) => normalizeRegistration(entry).plugin);
|
|
134
113
|
const pluginById = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
|
135
114
|
const executedActions: SetupInstallationPlanAction[] = [];
|
|
136
115
|
const skippedActions: SetupInstallationPlanAction[] = [];
|
|
@@ -197,22 +176,13 @@ export async function executeSetupInstallationPlan(
|
|
|
197
176
|
|
|
198
177
|
await mediator.finalize();
|
|
199
178
|
|
|
200
|
-
|
|
201
|
-
ctx,
|
|
202
|
-
plugins,
|
|
203
|
-
plan,
|
|
204
|
-
registrations.map(normalizeRegistration),
|
|
205
|
-
);
|
|
206
|
-
for (const warning of reconcileWarnings) {
|
|
207
|
-
warnings.push(warning);
|
|
208
|
-
process.stderr.write(`${warning}\n`);
|
|
209
|
-
}
|
|
179
|
+
await reconcileCapabilityContributions(ctx, plugins, plan);
|
|
210
180
|
|
|
211
181
|
return { executedActions, skippedActions, warnings };
|
|
212
182
|
}
|
|
213
183
|
|
|
214
184
|
// The Runtime Surface of each active Agent Runtime, by runtime id.
|
|
215
|
-
|
|
185
|
+
const RUNTIME_SURFACE_RECONCILERS: Record<
|
|
216
186
|
string,
|
|
217
187
|
(ctx: SetupContext, inputs: CapabilityContributionInput[]) => Promise<void>
|
|
218
188
|
> = {
|
|
@@ -273,104 +243,13 @@ async function reconcileCapabilityContributions(
|
|
|
273
243
|
ctx: SetupContext,
|
|
274
244
|
plugins: Plugin[],
|
|
275
245
|
plan: SetupInstallationPlan,
|
|
276
|
-
|
|
277
|
-
): Promise<string[]> {
|
|
246
|
+
): Promise<void> {
|
|
278
247
|
const inputsByRuntime = await collectContributionInputs(ctx, plugins, plan);
|
|
279
|
-
const ledgerRoot = surfaceRoot(ctx);
|
|
280
|
-
const ledger = await readContributionLedger(ledgerRoot);
|
|
281
|
-
const warnings: string[] = [];
|
|
282
|
-
let unswept = ledger?.unswept ?? [];
|
|
283
|
-
if (!ledger || unswept.length > 0) {
|
|
284
|
-
if (!ledger) {
|
|
285
|
-
await removeForeignGuidanceBlocks(ledgerRoot, new Set(plugins.map((plugin) => plugin.id)));
|
|
286
|
-
}
|
|
287
|
-
const sweep = await sweepAudienceDeclarations(ctx, {
|
|
288
|
-
registrations,
|
|
289
|
-
...(ledger ? { packages: new Set(unswept) } : {}),
|
|
290
|
-
});
|
|
291
|
-
prependTeardown(inputsByRuntime, sweep.teardown);
|
|
292
|
-
unswept = sweep.unswept;
|
|
293
|
-
warnings.push(...sweep.warnings);
|
|
294
|
-
}
|
|
295
|
-
prependLedgerTeardown(inputsByRuntime, ledger);
|
|
296
248
|
for (const [runtimeId, inputs] of inputsByRuntime) {
|
|
297
249
|
const reconcile = RUNTIME_SURFACE_RECONCILERS[runtimeId];
|
|
298
250
|
if (!reconcile || inputs.length === 0) continue;
|
|
299
251
|
await reconcile(ctx, inputs);
|
|
300
252
|
}
|
|
301
|
-
await writeContributionLedger(ledgerRoot, ledgerFromInputs(inputsByRuntime, unswept));
|
|
302
|
-
return warnings;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Disabled inputs placed first, skipping any owner that is an enabled input
|
|
307
|
-
* of the pass on that runtime: removal runs before enabled inputs apply.
|
|
308
|
-
*/
|
|
309
|
-
function prependTeardown(
|
|
310
|
-
inputsByRuntime: Map<string, CapabilityContributionInput[]>,
|
|
311
|
-
teardownByRuntime: ReadonlyMap<string, readonly CapabilityContributionInput[]>,
|
|
312
|
-
): void {
|
|
313
|
-
for (const [runtimeId, teardown] of teardownByRuntime) {
|
|
314
|
-
const inputs = inputsByRuntime.get(runtimeId) ?? [];
|
|
315
|
-
const enabled = new Set(inputs.filter((input) => input.enabled).map((input) => input.pluginId));
|
|
316
|
-
const kept = teardown.filter((input) => !enabled.has(input.pluginId));
|
|
317
|
-
if (kept.length > 0) inputsByRuntime.set(runtimeId, [...kept, ...inputs]);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Every recorded owner that is not an enabled input of this pass, as a
|
|
323
|
-
* disabled input placed first: its entries are removed before enabled inputs
|
|
324
|
-
* apply, so an identity another plugin still declares is re-applied after.
|
|
325
|
-
* Entries the ledger does not record are never named, so they survive.
|
|
326
|
-
*/
|
|
327
|
-
function prependLedgerTeardown(
|
|
328
|
-
inputsByRuntime: Map<string, CapabilityContributionInput[]>,
|
|
329
|
-
ledger: ContributionLedger | null,
|
|
330
|
-
): void {
|
|
331
|
-
prependTeardown(
|
|
332
|
-
inputsByRuntime,
|
|
333
|
-
new Map(
|
|
334
|
-
Object.entries(ledger?.runtimes ?? {}).map(([runtimeId, owners]) => [
|
|
335
|
-
runtimeId,
|
|
336
|
-
Object.entries(owners).map(([owner, identities]) => ledgerTeardownInput(owner, identities)),
|
|
337
|
-
]),
|
|
338
|
-
),
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* The declared contributions of one audience's plugins, as enabled inputs per
|
|
344
|
-
* active runtime. No plugin phase and no lifecycle hook runs: an audience
|
|
345
|
-
* plugin contributes declaratively only. OpenCode plugin references are
|
|
346
|
-
* dropped — the Mate plugin has no way to load another plugin.
|
|
347
|
-
*/
|
|
348
|
-
export async function collectAudienceInputs(
|
|
349
|
-
ctx: SetupContext,
|
|
350
|
-
registrations: PluginRegistration[],
|
|
351
|
-
audience: string,
|
|
352
|
-
): Promise<Map<string, CapabilityContributionInput[]>> {
|
|
353
|
-
const inputsByRuntime = new Map<string, CapabilityContributionInput[]>();
|
|
354
|
-
for (const entry of registrations.map(normalizeRegistration)) {
|
|
355
|
-
if (entry.audience !== audience || entry.plugin.kind !== "capability") continue;
|
|
356
|
-
const capability = entry.plugin as CapabilityPlugin;
|
|
357
|
-
if (!capability.getRuntimeContributions) continue;
|
|
358
|
-
if (entry.policy !== "required" && !capability.isEnabled(ctx.config)) continue;
|
|
359
|
-
const byRuntime = await capability.getRuntimeContributions(ctx);
|
|
360
|
-
for (const [runtimeId, contributions] of Object.entries(byRuntime)) {
|
|
361
|
-
if (!contributions || !ctx.activeProviders.includes(runtimeId)) continue;
|
|
362
|
-
const { pluginReferences: _dropped, ...declarative } = contributions;
|
|
363
|
-
const inputs = inputsByRuntime.get(runtimeId) ?? [];
|
|
364
|
-
inputs.push({
|
|
365
|
-
pluginId: capability.id,
|
|
366
|
-
enabled: true,
|
|
367
|
-
capabilityEnabled: true,
|
|
368
|
-
contributions: declarative,
|
|
369
|
-
});
|
|
370
|
-
inputsByRuntime.set(runtimeId, inputs);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return inputsByRuntime;
|
|
374
253
|
}
|
|
375
254
|
|
|
376
255
|
/** What a render returns: its warnings are data for the caller, never printed here. */
|
|
@@ -388,22 +267,12 @@ export async function renderRuntimeDocuments(
|
|
|
388
267
|
ctx: SetupContext,
|
|
389
268
|
registrations: PluginRegistration[],
|
|
390
269
|
plan: SetupInstallationPlan,
|
|
391
|
-
wrapped?: { audience: string; registrations: PluginRegistration[] },
|
|
392
270
|
): Promise<RenderedRuntimeDocuments> {
|
|
393
|
-
const plugins =
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
ctx,
|
|
399
|
-
wrapped.registrations,
|
|
400
|
-
wrapped.audience,
|
|
401
|
-
);
|
|
402
|
-
for (const [runtimeId, inputs] of audienceInputs) {
|
|
403
|
-
inputsByRuntime.set(runtimeId, [...(inputsByRuntime.get(runtimeId) ?? []), ...inputs]);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
const documents = renderDocumentsForTarget(ctx, inputsByRuntime);
|
|
271
|
+
const plugins = registrations.map((entry) => normalizeRegistration(entry).plugin);
|
|
272
|
+
const documents = renderDocumentsForTarget(
|
|
273
|
+
ctx,
|
|
274
|
+
await collectContributionInputs(ctx, plugins, plan),
|
|
275
|
+
);
|
|
407
276
|
if (ctx.target !== "working") return { documents, warnings: [] };
|
|
408
277
|
return mergeCompanionOpenCodeConfig(ctx.companionPath, documents);
|
|
409
278
|
}
|
|
@@ -7,10 +7,9 @@ export type SetupScope = "companion" | "hub";
|
|
|
7
7
|
/**
|
|
8
8
|
* Where a reconcile pass lands its documents. `companion` writes the material's
|
|
9
9
|
* own repository; `working` renders the documents an Agent Runtime discovers by
|
|
10
|
-
* its own upward walk and leaves the placing to the Projection Root
|
|
11
|
-
* `audience-layer` writes one audience's launch layer, outside every repository.
|
|
10
|
+
* its own upward walk and leaves the placing to the Projection Root.
|
|
12
11
|
*/
|
|
13
|
-
export type ProjectionTarget = "companion" | "working"
|
|
12
|
+
export type ProjectionTarget = "companion" | "working";
|
|
14
13
|
|
|
15
14
|
export interface SetupContext {
|
|
16
15
|
companionPath: string;
|
|
@@ -23,14 +22,6 @@ export interface SetupContext {
|
|
|
23
22
|
repoPath?: string;
|
|
24
23
|
/** Defaults to `companion`, so a caller that names no target is unchanged. */
|
|
25
24
|
target?: ProjectionTarget;
|
|
26
|
-
/** The audience an `audience-layer` pass renders; required by that target only. */
|
|
27
|
-
audience?: string;
|
|
28
|
-
/**
|
|
29
|
-
* Where an `audience-layer` pass writes: the staging directory a render is
|
|
30
|
-
* built in before it is renamed into place. The audience's layer directory
|
|
31
|
-
* when absent.
|
|
32
|
-
*/
|
|
33
|
-
layerRoot?: string;
|
|
34
25
|
/**
|
|
35
26
|
* Provider-mediated context services. Always populated when the engine
|
|
36
27
|
* executes a plugin; optional so hand-built contexts (tests, direct calls)
|
|
@@ -131,15 +122,9 @@ export type PluginPolicy = "required" | "default" | "optional";
|
|
|
131
122
|
|
|
132
123
|
/**
|
|
133
124
|
* A plugin registration entry. Bare plugins derive their policy from
|
|
134
|
-
* `defaultSelected` (`true` → "default", `false` → "optional").
|
|
135
|
-
* the source of a declared plugin: `null` (or absent) for compiled-in plugins
|
|
136
|
-
* and top-level `plugins` declarations, the audience name for a declaration
|
|
137
|
-
* under `audiences.<name>.plugins`. `packageName` names the declared package a
|
|
138
|
-
* dynamic plugin was loaded from.
|
|
125
|
+
* `defaultSelected` (`true` → "default", `false` → "optional").
|
|
139
126
|
*/
|
|
140
|
-
export type PluginRegistration =
|
|
141
|
-
| Plugin
|
|
142
|
-
| { plugin: Plugin; policy: PluginPolicy; audience?: string | null; packageName?: string };
|
|
127
|
+
export type PluginRegistration = Plugin | { plugin: Plugin; policy: PluginPolicy };
|
|
143
128
|
|
|
144
129
|
/**
|
|
145
130
|
* A hook group a Capability contributes to a runtime's settings. `marker` is
|
|
@@ -97,13 +97,7 @@ export function createGitignorePlugin(
|
|
|
97
97
|
): Plugin {
|
|
98
98
|
// Resolve lazily so plugin sets can be built before a distribution is active.
|
|
99
99
|
const resolveName = typeof frameworkName === "function" ? frameworkName : () => frameworkName;
|
|
100
|
-
|
|
101
|
-
const resolvePlugins =
|
|
102
|
-
allPlugins ??
|
|
103
|
-
(() =>
|
|
104
|
-
getActiveDistribution()
|
|
105
|
-
.registry.baseRegistrations()
|
|
106
|
-
.map((entry) => entry.plugin));
|
|
100
|
+
const resolvePlugins = allPlugins ?? (() => getActiveDistribution().registry.getAll());
|
|
107
101
|
return {
|
|
108
102
|
id: "gitignore",
|
|
109
103
|
kind: "root",
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
upsertManagedBlock,
|
|
12
12
|
} from "../context-services";
|
|
13
13
|
import type { CapabilityContributionInput, ProviderPlugin, SetupContext } from "../plugin";
|
|
14
|
-
import {
|
|
14
|
+
import { surfaceRoot } from "../surface-target";
|
|
15
15
|
import { mergeDir, pruneEmptyAncestors } from "../utils";
|
|
16
16
|
import { stripSectionFromFile, type RemoveHeadingSectionOptions } from "./agent-file-sections";
|
|
17
17
|
import { patchSkillTreeMarkdownFiles } from "./skill-tree";
|
|
@@ -164,23 +164,6 @@ export function renderManagedClaudeHookGroups(
|
|
|
164
164
|
return hooks;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/**
|
|
168
|
-
* A layer's Claude settings: the enabled inputs' hook groups and permission
|
|
169
|
-
* entries, nothing of Mate's own. The launch merges them over its base settings.
|
|
170
|
-
*/
|
|
171
|
-
export function renderLayerClaudeSettings(
|
|
172
|
-
contributions: CapabilityContributionInput[] = [],
|
|
173
|
-
): ClaudeSettings {
|
|
174
|
-
const hooks = renderManagedClaudeHookGroups(contributions);
|
|
175
|
-
const allow = contributions
|
|
176
|
-
.filter((input) => input.enabled)
|
|
177
|
-
.flatMap((input) => input.contributions.permissionEntries ?? []);
|
|
178
|
-
return {
|
|
179
|
-
...(Object.keys(hooks).length > 0 ? { hooks } : {}),
|
|
180
|
-
...(allow.length > 0 ? { permissions: { allow } } : {}),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
167
|
// Reconcile the Mate-managed Claude settings shape (hooks, permissions, MCP
|
|
185
168
|
// servers) on top of whatever already exists in the settings document. Managed
|
|
186
169
|
// groups/entries always lead their arrays so the emitted shape stays stable
|
|
@@ -304,15 +287,7 @@ export async function reconcileClaudeContributions(
|
|
|
304
287
|
// The settings sync (re)creates the managed settings document, so it only
|
|
305
288
|
// runs while the Claude runtime is active; a deactivated runtime's pass is
|
|
306
289
|
// teardown-only and must not resurrect files the provider teardown removed.
|
|
307
|
-
|
|
308
|
-
// launch to merge into its one `--settings` file.
|
|
309
|
-
if (projectionTarget(ctx) === "audience-layer") {
|
|
310
|
-
await writeClaudeSettings(
|
|
311
|
-
getCompanionClaudeSettingsPath(root),
|
|
312
|
-
renderLayerClaudeSettings(inputs),
|
|
313
|
-
);
|
|
314
|
-
await syncCompanionClaudeMcpConfig(root);
|
|
315
|
-
} else if (ctx.activeProviders.includes("claude")) {
|
|
290
|
+
if (ctx.activeProviders.includes("claude")) {
|
|
316
291
|
await syncCompanionClaudeSettings(root, config, inputs);
|
|
317
292
|
}
|
|
318
293
|
|
|
@@ -416,16 +416,6 @@ export async function removeOpenCodeForeignPluginReferences(
|
|
|
416
416
|
|
|
417
417
|
const OPENCODE_CONTRIBUTION_CONFIG_FILES = ["opencode.json"];
|
|
418
418
|
|
|
419
|
-
/**
|
|
420
|
-
* The surface's reader for an audience layer it wrote: what the Mate OpenCode
|
|
421
|
-
* plugin applies. Defined under `runtime/` so the import-isolated plugin can
|
|
422
|
-
* reach it; this surface owns the format it reads.
|
|
423
|
-
*/
|
|
424
|
-
export {
|
|
425
|
-
readOpenCodeAudienceLayer,
|
|
426
|
-
type OpenCodeLayerContents,
|
|
427
|
-
} from "../../../runtime/opencode-audience-layer";
|
|
428
|
-
|
|
429
419
|
export async function reconcileOpenCodeContributions(
|
|
430
420
|
ctx: SetupContext,
|
|
431
421
|
inputs: CapabilityContributionInput[],
|