@uniqbit/mate-core 0.18.3-canary.1 → 0.18.4
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/claude-plugin/hooks/hooks.json +10 -1
- package/claude-plugin/hooks/session-banner.mjs +4 -3
- package/claude-plugin/hooks/session-prepare.mjs +9 -0
- package/opencode-plugin/src/add-dir.ts +10 -76
- package/opencode-plugin/src/audience-layer.ts +91 -0
- package/opencode-plugin/src/companion.ts +70 -18
- package/opencode-plugin/src/launch-environment.ts +29 -0
- package/opencode-plugin/src/server.ts +60 -8
- package/package.json +3 -3
- package/src/cli/commands/artifact/finish/openspec.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +60 -21
- package/src/cli/commands/cap/index-cmd.ts +2 -11
- package/src/cli/commands/companion/link.ts +16 -11
- package/src/cli/commands/companion/list.ts +4 -5
- package/src/cli/commands/companion/open.ts +4 -5
- package/src/cli/commands/companion/tui.ts +1 -1
- package/src/cli/commands/doctor.ts +87 -5
- package/src/cli/commands/install.ts +8 -0
- package/src/cli/commands/report/renderer.ts +9 -19
- package/src/cli/commands/session/prepare.ts +119 -0
- package/src/cli/commands/shared/companion-selection.ts +2 -2
- package/src/cli/commands/studio/reports.ts +0 -1
- package/src/cli/commands/studio/server.ts +6 -2
- package/src/cli/commands/studio/terminal-launch.ts +16 -7
- package/src/cli/commands/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +79 -60
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +5 -2
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-guidance.ts +20 -2
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +45 -9
- package/src/lib/orchestrator/adapters/claude.ts +241 -30
- package/src/lib/orchestrator/adapters/opencode.ts +72 -34
- package/src/lib/orchestrator/companion-git-sync.ts +28 -1
- package/src/lib/orchestrator/companion-hub.ts +2 -1
- package/src/lib/orchestrator/config-store.ts +83 -1
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/openspec-store.ts +202 -0
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +126 -41
- package/src/lib/orchestrator/projection-entries.ts +124 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
- package/src/lib/orchestrator/projection-openspec-store.ts +164 -0
- package/src/lib/orchestrator/projection-record.ts +7 -1
- package/src/lib/orchestrator/projection-runtime-documents.ts +83 -130
- package/src/lib/orchestrator/projection-types.ts +47 -34
- package/src/lib/orchestrator/repository-link.ts +473 -0
- package/src/lib/orchestrator/root-context.ts +6 -2
- package/src/lib/orchestrator/session-prepare.ts +503 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/working-repo-projection.ts +73 -160
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +222 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +30 -135
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- package/src/opencode/projected-guidance.ts +6 -13
- package/src/opencode/session-notes.ts +76 -0
- package/src/opencode/session-prepare.ts +139 -0
- package/src/opencode/tui.tsx +56 -9
- package/src/runtime/audience-layer.ts +84 -0
- package/src/runtime/claude-delivery-plan.ts +73 -0
- package/src/runtime/claude-hook-group.ts +21 -0
- package/src/runtime/companion-lock.ts +121 -0
- package/src/runtime/companion-sync.ts +66 -9
- package/src/runtime/guidance.ts +17 -0
- package/src/runtime/opencode-audience-layer.ts +176 -0
- package/src/runtime/policy.ts +14 -1
- package/src/runtime/projection.ts +1 -1
- package/src/runtime/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +352 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +94 -0
- package/src/runtime/write-atomic.ts +27 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +5 -0
- package/src/templates/capabilities/react-doctor/mate-react-doctor/SKILL.md +84 -0
- package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md +18 -15
- package/src/templates/mate-skills/agents/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +1 -1
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +1 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +850 -499
- package/src/tools/setup/audience-isolation.fixture.ts +325 -0
- package/src/tools/setup/audience-layer.ts +207 -0
- package/src/tools/setup/audience-registrations.ts +52 -0
- package/src/tools/setup/capabilities/openspec.ts +9 -0
- package/src/tools/setup/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/capabilities/tokensave.ts +11 -0
- package/src/tools/setup/contribution-ledger.ts +207 -0
- package/src/tools/setup/dynamic-plugins/audiences.ts +89 -9
- package/src/tools/setup/dynamic-plugins/declarations.ts +3 -2
- package/src/tools/setup/dynamic-plugins/hydrate.ts +8 -5
- package/src/tools/setup/engine.ts +152 -10
- package/src/tools/setup/isolation-diagnostics.ts +139 -0
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/plugin.ts +19 -4
- package/src/tools/setup/plugins/gitignore.ts +7 -1
- package/src/tools/setup/providers/claude-format.ts +11 -18
- package/src/tools/setup/providers/claude-plugin-hooks.ts +61 -24
- package/src/tools/setup/providers/claude.ts +50 -8
- package/src/tools/setup/providers/opencode.ts +10 -0
- package/src/tools/setup/registry.ts +24 -2
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/surface-target.ts +13 -4
- package/src/tools/setup/upgrade-sweep.ts +153 -0
- package/src/tools/setup/working-repo-cleanup.ts +2 -0
- package/src/tools/setup/working-repo-local-state.ts +91 -14
- package/src/tools/setup.ts +11 -8
- package/wrappers/bin/openspec +42 -1
- package/src/lib/orchestrator/companion-registry-reader.ts +0 -37
- package/src/lib/orchestrator/companion-resolver.ts +0 -179
- package/src/lib/orchestrator/companion-store.ts +0 -94
- package/src/lib/orchestrator/repo-local-registry.ts +0 -143
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +0 -144
- package/src/templates/capabilities/react-doctor/skill/SKILL.md +0 -51
- package/src/templates/report-assets/README.md +0 -32
- package/src/templates/report-assets/mermaid.LICENSE +0 -21
- package/src/templates/report-assets/mermaid.min.js +0 -4376
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// oxlint-disable no-await-in-loop
|
|
2
2
|
import { getActiveDistribution } from "../../distribution";
|
|
3
3
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
|
-
import type { RenderedRuntimeDocument } from "../../lib/orchestrator/projection-types";
|
|
5
4
|
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
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";
|
|
7
14
|
import type {
|
|
8
15
|
CapabilityContributionInput,
|
|
9
16
|
CapabilityPlugin,
|
|
@@ -13,8 +20,10 @@ import type {
|
|
|
13
20
|
} from "./plugin";
|
|
14
21
|
import { reconcileClaudeContributions } from "./providers/claude";
|
|
15
22
|
import { reconcileOpenCodeContributions } from "./providers/opencode";
|
|
16
|
-
import { renderDocumentsForTarget } from "./runtime-documents";
|
|
23
|
+
import { mergeCompanionOpenCodeConfig, renderDocumentsForTarget } from "./runtime-documents";
|
|
17
24
|
import { normalizeRegistration, type NormalizedRegistration } from "./registry";
|
|
25
|
+
import { surfaceRoot } from "./surface-target";
|
|
26
|
+
import { removeForeignGuidanceBlocks, sweepAudienceDeclarations } from "./upgrade-sweep";
|
|
18
27
|
|
|
19
28
|
export interface SetupInstallationPlanAction {
|
|
20
29
|
phase: Plugin["kind"];
|
|
@@ -35,11 +44,23 @@ export interface SetupInstallationOutcome {
|
|
|
35
44
|
warnings: string[];
|
|
36
45
|
}
|
|
37
46
|
|
|
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
|
+
|
|
38
59
|
export function buildSetupInstallationPlan(
|
|
39
60
|
config: FrameworkConfig,
|
|
40
61
|
registrations: PluginRegistration[],
|
|
41
62
|
): SetupInstallationPlan {
|
|
42
|
-
const entries = registrations
|
|
63
|
+
const entries = persistedRegistrations(registrations);
|
|
43
64
|
// Required plugins are always treated as enabled: saved selections cannot
|
|
44
65
|
// deselect them and deselection never produces a teardown action.
|
|
45
66
|
const isEntryEnabled = (entry: NormalizedRegistration) =>
|
|
@@ -109,7 +130,7 @@ export async function executeSetupInstallationPlan(
|
|
|
109
130
|
registrations: PluginRegistration[],
|
|
110
131
|
plan: SetupInstallationPlan,
|
|
111
132
|
): Promise<SetupInstallationOutcome> {
|
|
112
|
-
const plugins = registrations.map((entry) =>
|
|
133
|
+
const plugins = persistedRegistrations(registrations).map((entry) => entry.plugin);
|
|
113
134
|
const pluginById = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
|
114
135
|
const executedActions: SetupInstallationPlanAction[] = [];
|
|
115
136
|
const skippedActions: SetupInstallationPlanAction[] = [];
|
|
@@ -176,13 +197,22 @@ export async function executeSetupInstallationPlan(
|
|
|
176
197
|
|
|
177
198
|
await mediator.finalize();
|
|
178
199
|
|
|
179
|
-
await reconcileCapabilityContributions(
|
|
200
|
+
const reconcileWarnings = await reconcileCapabilityContributions(
|
|
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
|
+
}
|
|
180
210
|
|
|
181
211
|
return { executedActions, skippedActions, warnings };
|
|
182
212
|
}
|
|
183
213
|
|
|
184
214
|
// The Runtime Surface of each active Agent Runtime, by runtime id.
|
|
185
|
-
const RUNTIME_SURFACE_RECONCILERS: Record<
|
|
215
|
+
export const RUNTIME_SURFACE_RECONCILERS: Record<
|
|
186
216
|
string,
|
|
187
217
|
(ctx: SetupContext, inputs: CapabilityContributionInput[]) => Promise<void>
|
|
188
218
|
> = {
|
|
@@ -243,13 +273,110 @@ async function reconcileCapabilityContributions(
|
|
|
243
273
|
ctx: SetupContext,
|
|
244
274
|
plugins: Plugin[],
|
|
245
275
|
plan: SetupInstallationPlan,
|
|
246
|
-
|
|
276
|
+
registrations: NormalizedRegistration[],
|
|
277
|
+
): Promise<string[]> {
|
|
247
278
|
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);
|
|
248
296
|
for (const [runtimeId, inputs] of inputsByRuntime) {
|
|
249
297
|
const reconcile = RUNTIME_SURFACE_RECONCILERS[runtimeId];
|
|
250
298
|
if (!reconcile || inputs.length === 0) continue;
|
|
251
299
|
await reconcile(ctx, inputs);
|
|
252
300
|
}
|
|
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
|
+
}
|
|
375
|
+
|
|
376
|
+
/** What a render returns: its warnings are data for the caller, never printed here. */
|
|
377
|
+
export interface RenderedRuntimeDocuments {
|
|
378
|
+
documents: RenderedRuntimeDocument[];
|
|
379
|
+
warnings: string[];
|
|
253
380
|
}
|
|
254
381
|
|
|
255
382
|
/**
|
|
@@ -261,7 +388,22 @@ export async function renderRuntimeDocuments(
|
|
|
261
388
|
ctx: SetupContext,
|
|
262
389
|
registrations: PluginRegistration[],
|
|
263
390
|
plan: SetupInstallationPlan,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
391
|
+
wrapped?: { audience: string; registrations: PluginRegistration[] },
|
|
392
|
+
): Promise<RenderedRuntimeDocuments> {
|
|
393
|
+
const plugins = persistedRegistrations(registrations).map((entry) => entry.plugin);
|
|
394
|
+
const inputsByRuntime = await collectContributionInputs(ctx, plugins, plan);
|
|
395
|
+
/** A checkout wrapped for an audience carries it beside base; no other audience ever. */
|
|
396
|
+
if (wrapped) {
|
|
397
|
+
const audienceInputs = await collectAudienceInputs(
|
|
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);
|
|
407
|
+
if (ctx.target !== "working") return { documents, warnings: [] };
|
|
408
|
+
return mergeCompanionOpenCodeConfig(ctx.companionPath, documents);
|
|
267
409
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
5
|
+
import {
|
|
6
|
+
readContributionLedger,
|
|
7
|
+
type ContributionLedger,
|
|
8
|
+
type LedgerIdentities,
|
|
9
|
+
} from "./contribution-ledger";
|
|
10
|
+
import type { CapabilityPlugin, SetupContext } from "./plugin";
|
|
11
|
+
import type { PluginRegistry } from "./registry";
|
|
12
|
+
import { managedGuidanceOwners } from "./upgrade-sweep";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What `mate doctor` reports about audience isolation: contributions on the
|
|
16
|
+
* companion's persisted surfaces that no base plugin owns, audience plugins
|
|
17
|
+
* the upgrade sweep could not load, and the active audience's plugins whose
|
|
18
|
+
* imperative parts a launch layer cannot deliver.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface LeakedContribution {
|
|
22
|
+
/** Companion-relative file or directory. */
|
|
23
|
+
surface: string;
|
|
24
|
+
kind: string;
|
|
25
|
+
/** The entry's name; absent where the surface entry is the owner's block. */
|
|
26
|
+
identity?: string;
|
|
27
|
+
owner: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UndeliveredAudiencePlugin {
|
|
31
|
+
pluginId: string;
|
|
32
|
+
/** `apply`, `teardown`, `forProvider` or `pluginReferences`. */
|
|
33
|
+
parts: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AudienceIsolationReport {
|
|
37
|
+
leaked: LeakedContribution[];
|
|
38
|
+
unswept: string[];
|
|
39
|
+
undelivered: UndeliveredAudiencePlugin[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Ledger kinds by where each runtime keeps them; guidance is read from the files themselves. */
|
|
43
|
+
const LEDGER_SURFACES: Record<string, Partial<Record<keyof LedgerIdentities, string>>> = {
|
|
44
|
+
claude: {
|
|
45
|
+
mcpServers: ".mcp.json",
|
|
46
|
+
skillTrees: path.join(".claude", "skills"),
|
|
47
|
+
agentDefinitions: path.join(".claude", "agents"),
|
|
48
|
+
hookMarkers: path.join(".claude", "settings.local.json"),
|
|
49
|
+
permissionEntries: path.join(".claude", "settings.local.json"),
|
|
50
|
+
},
|
|
51
|
+
opencode: {
|
|
52
|
+
mcpServers: path.join(".opencode", "opencode.json"),
|
|
53
|
+
pluginReferences: path.join(".opencode", "opencode.json"),
|
|
54
|
+
skillTrees: path.join(".agents", "skills"),
|
|
55
|
+
agentDefinitions: path.join(".opencode", "agents"),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function leakedLedgerContributions(
|
|
60
|
+
ledger: ContributionLedger | null,
|
|
61
|
+
baseIds: ReadonlySet<string>,
|
|
62
|
+
): LeakedContribution[] {
|
|
63
|
+
const leaked: LeakedContribution[] = [];
|
|
64
|
+
for (const [runtimeId, owners] of Object.entries(ledger?.runtimes ?? {})) {
|
|
65
|
+
for (const [owner, identities] of Object.entries(owners)) {
|
|
66
|
+
if (baseIds.has(owner)) continue;
|
|
67
|
+
for (const [kind, surface] of Object.entries(LEDGER_SURFACES[runtimeId] ?? {})) {
|
|
68
|
+
for (const identity of identities[kind as keyof LedgerIdentities] ?? []) {
|
|
69
|
+
leaked.push({ surface: surface!, kind, identity, owner });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return leaked;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether a lifecycle hook does anything. `apply` and `teardown` are required
|
|
79
|
+
* members, so a declarative plugin carries empty ones; only a body counts.
|
|
80
|
+
*/
|
|
81
|
+
function hasBody(hook: unknown): boolean {
|
|
82
|
+
if (typeof hook !== "function") return false;
|
|
83
|
+
const source = Function.prototype.toString.call(hook);
|
|
84
|
+
const open = source.indexOf("{");
|
|
85
|
+
const close = source.lastIndexOf("}");
|
|
86
|
+
if (open === -1 || close <= open) return true;
|
|
87
|
+
const body = source
|
|
88
|
+
.slice(open + 1, close)
|
|
89
|
+
.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "")
|
|
90
|
+
.trim();
|
|
91
|
+
return body.length > 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function undeliveredParts(plugin: CapabilityPlugin, ctx: SetupContext): Promise<string[]> {
|
|
95
|
+
const parts: string[] = [];
|
|
96
|
+
if (hasBody(plugin.apply)) parts.push("apply");
|
|
97
|
+
if (hasBody(plugin.teardown)) parts.push("teardown");
|
|
98
|
+
if (plugin.forProvider && Object.keys(plugin.forProvider).length > 0) parts.push("forProvider");
|
|
99
|
+
try {
|
|
100
|
+
const byRuntime = (await plugin.getRuntimeContributions?.(ctx)) ?? {};
|
|
101
|
+
if (Object.values(byRuntime).some((runtime) => (runtime?.pluginReferences ?? []).length > 0)) {
|
|
102
|
+
parts.push("pluginReferences");
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
/** A plugin whose declarations fail is the plugin checks' finding, not this one. */
|
|
106
|
+
}
|
|
107
|
+
return parts;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function inspectAudienceIsolation(
|
|
111
|
+
companionPath: string,
|
|
112
|
+
config: FrameworkConfig,
|
|
113
|
+
registry: PluginRegistry,
|
|
114
|
+
activeAudience: string | null,
|
|
115
|
+
): Promise<AudienceIsolationReport> {
|
|
116
|
+
const baseIds = new Set(registry.baseRegistrations().map((entry) => entry.plugin.id));
|
|
117
|
+
const leaked: LeakedContribution[] = [];
|
|
118
|
+
|
|
119
|
+
for (const [surface, owners] of await managedGuidanceOwners(companionPath)) {
|
|
120
|
+
for (const owner of owners) {
|
|
121
|
+
if (!baseIds.has(owner)) leaked.push({ surface, kind: "guidance", owner });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const ledger = await readContributionLedger(companionPath);
|
|
126
|
+
leaked.push(...leakedLedgerContributions(ledger, baseIds));
|
|
127
|
+
|
|
128
|
+
const undelivered: UndeliveredAudiencePlugin[] = [];
|
|
129
|
+
if (activeAudience) {
|
|
130
|
+
const ctx: SetupContext = { companionPath, config, mode: "sync", activeProviders: [] };
|
|
131
|
+
for (const { plugin } of registry.audienceRegistrations(activeAudience)) {
|
|
132
|
+
if (plugin.kind !== "capability") continue;
|
|
133
|
+
const parts = await undeliveredParts(plugin as CapabilityPlugin, ctx);
|
|
134
|
+
if (parts.length > 0) undelivered.push({ pluginId: plugin.id, parts });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { leaked, unswept: ledger?.unswept ?? [], undelivered };
|
|
139
|
+
}
|
package/src/tools/setup/mate.ts
CHANGED
|
@@ -81,7 +81,7 @@ A \`diff\` section carries a unified diff patch as produced by \`git diff\`. The
|
|
|
81
81
|
{ "id": "changes", "title": "Changes", "type": "diff", "patch": "diff --git a/src/acme.ts b/src/acme.ts\\n..." }
|
|
82
82
|
\`\`\`
|
|
83
83
|
|
|
84
|
-
A report carrying a mermaid payload
|
|
84
|
+
A report carrying a mermaid payload loads the pinned, integrity-checked diagram runtime from a CDN; a report without one does not. Offline, each diagram stays readable as its source. Otherwise the page stays a single self-contained file.
|
|
85
85
|
|
|
86
86
|
## Invocation
|
|
87
87
|
|
|
@@ -7,9 +7,10 @@ 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
|
|
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.
|
|
11
12
|
*/
|
|
12
|
-
export type ProjectionTarget = "companion" | "working";
|
|
13
|
+
export type ProjectionTarget = "companion" | "working" | "audience-layer";
|
|
13
14
|
|
|
14
15
|
export interface SetupContext {
|
|
15
16
|
companionPath: string;
|
|
@@ -22,6 +23,14 @@ export interface SetupContext {
|
|
|
22
23
|
repoPath?: string;
|
|
23
24
|
/** Defaults to `companion`, so a caller that names no target is unchanged. */
|
|
24
25
|
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;
|
|
25
34
|
/**
|
|
26
35
|
* Provider-mediated context services. Always populated when the engine
|
|
27
36
|
* executes a plugin; optional so hand-built contexts (tests, direct calls)
|
|
@@ -122,9 +131,15 @@ export type PluginPolicy = "required" | "default" | "optional";
|
|
|
122
131
|
|
|
123
132
|
/**
|
|
124
133
|
* A plugin registration entry. Bare plugins derive their policy from
|
|
125
|
-
* `defaultSelected` (`true` → "default", `false` → "optional").
|
|
134
|
+
* `defaultSelected` (`true` → "default", `false` → "optional"). `audience` is
|
|
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.
|
|
126
139
|
*/
|
|
127
|
-
export type PluginRegistration =
|
|
140
|
+
export type PluginRegistration =
|
|
141
|
+
| Plugin
|
|
142
|
+
| { plugin: Plugin; policy: PluginPolicy; audience?: string | null; packageName?: string };
|
|
128
143
|
|
|
129
144
|
/**
|
|
130
145
|
* A hook group a Capability contributes to a runtime's settings. `marker` is
|
|
@@ -97,7 +97,13 @@ 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
|
-
|
|
100
|
+
/** Base only: an audience plugin's entries would reach the committed `.gitignore`. */
|
|
101
|
+
const resolvePlugins =
|
|
102
|
+
allPlugins ??
|
|
103
|
+
(() =>
|
|
104
|
+
getActiveDistribution()
|
|
105
|
+
.registry.baseRegistrations()
|
|
106
|
+
.map((entry) => entry.plugin));
|
|
101
107
|
return {
|
|
102
108
|
id: "gitignore",
|
|
103
109
|
kind: "root",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { hookGroupKey, type ClaudeHookGroup } from "../../../runtime/claude-hook-group";
|
|
5
|
+
|
|
4
6
|
// Structurally matches `McpServerDescriptor` from ../plugin; declared here so
|
|
5
7
|
// the format module stays dependency-free of the plugin contract (which
|
|
6
8
|
// imports hook types from this module).
|
|
@@ -17,17 +19,11 @@ export interface McpEntryDescriptor {
|
|
|
17
19
|
// groups are Mate-managed lives here too, because the companion-side reconcile
|
|
18
20
|
// and the working-repo projection entry must strip the same set.
|
|
19
21
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ClaudeHookGroup {
|
|
28
|
-
matcher?: string;
|
|
29
|
-
hooks?: ClaudeHookCommand[];
|
|
30
|
-
}
|
|
22
|
+
export {
|
|
23
|
+
hookGroupKey,
|
|
24
|
+
type ClaudeHookCommand,
|
|
25
|
+
type ClaudeHookGroup,
|
|
26
|
+
} from "../../../runtime/claude-hook-group";
|
|
31
27
|
|
|
32
28
|
export type ClaudeHookMap = Record<string, ClaudeHookGroup[]>;
|
|
33
29
|
|
|
@@ -43,10 +39,6 @@ export interface ClaudeMcpConfig {
|
|
|
43
39
|
[key: string]: unknown;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
function hookGroupKey(group: ClaudeHookGroup): string {
|
|
47
|
-
return JSON.stringify(group);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
42
|
/** Append incoming hook groups per event, skipping structurally identical ones. */
|
|
51
43
|
export function mergeClaudeHookGroups(
|
|
52
44
|
existingHooks: ClaudeHookMap,
|
|
@@ -84,9 +76,10 @@ export function filterClaudeHookGroups(
|
|
|
84
76
|
|
|
85
77
|
// Command substrings that mark a hook group as Mate-managed. The mate plugin
|
|
86
78
|
// hooks (validate-artifact-path, mate-session-banner) now ship in the bundled
|
|
87
|
-
// Claude plugin, and the retired mate-artifact-finish.sh archive hook
|
|
88
|
-
// entirely; their markers are retained
|
|
89
|
-
// written by earlier releases keep being
|
|
79
|
+
// Claude plugin, and the retired mate-artifact-finish.sh archive hook and
|
|
80
|
+
// react-doctor.sh post-edit scan are gone entirely; their markers are retained
|
|
81
|
+
// migration-only so stale managed groups written by earlier releases keep being
|
|
82
|
+
// stripped, and are never re-added.
|
|
90
83
|
const MANAGED_HOOK_MARKERS = [
|
|
91
84
|
"validate-artifact-path",
|
|
92
85
|
"mate-session-banner",
|
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { PROJECTED_BANNER_FLAG } from "../../../hooks/session-banner";
|
|
5
5
|
import { getClaudePluginRoot } from "../../../lib/package-paths";
|
|
6
|
-
import
|
|
6
|
+
import { runtimeClaudePluginRoot } from "../../../runtime/runtime-directory";
|
|
7
|
+
import { hookGroupKey, type ClaudeHookGroup } from "./claude-format";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The bundled Claude plugin's hook wiring, read as a value. A managed launch
|
|
@@ -17,11 +18,16 @@ import type { ClaudeHookGroup } from "./claude-format";
|
|
|
17
18
|
* environment is present and so cannot double the prompt the launch already
|
|
18
19
|
* appended; and the banner, whose carried copy is marked so it defers to the
|
|
19
20
|
* launch's.
|
|
21
|
+
*
|
|
22
|
+
* The wiring is read from the installed plugin, but every command names
|
|
23
|
+
* `<mate home>/runtime/claude-plugin`, so a reinstall or Node switch does not
|
|
24
|
+
* strand a wrapped repository.
|
|
20
25
|
*/
|
|
21
26
|
|
|
22
27
|
const GUARD_HOOK_SHIM = "validate-artifact-path.mjs";
|
|
23
28
|
const GUIDANCE_HOOK_SHIM = "session-guidance.mjs";
|
|
24
29
|
const BANNER_HOOK_SHIM = "session-banner.mjs";
|
|
30
|
+
const PREPARE_HOOK_SHIM = "session-prepare.mjs";
|
|
25
31
|
const PLUGIN_ROOT_TOKEN = "${CLAUDE_PLUGIN_ROOT}";
|
|
26
32
|
|
|
27
33
|
interface PluginHookWiring {
|
|
@@ -50,26 +56,24 @@ function resolveCommands(group: ClaudeHookGroup, pluginRoot: string): ClaudeHook
|
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
function runsShim(shim: string):
|
|
54
|
-
return (group
|
|
59
|
+
function runsShim(group: ClaudeHookGroup, shim: string): boolean {
|
|
60
|
+
return (group.hooks ?? []).some((hook) => (hook.command ?? "").includes(shim));
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
function groupsFor(
|
|
58
|
-
event
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
): ClaudeHookGroup[] {
|
|
62
|
-
return (readPluginHookWiring(pluginRoot).hooks?.[event] ?? [])
|
|
63
|
-
.filter(runsShim(shim))
|
|
64
|
-
.map((group) => resolveCommands(group, pluginRoot));
|
|
63
|
+
function groupsFor(event: string, shim: string, commandRoot: string): ClaudeHookGroup[] {
|
|
64
|
+
return (readPluginHookWiring(getClaudePluginRoot()).hooks?.[event] ?? []).flatMap((group) =>
|
|
65
|
+
runsShim(group, shim) ? [resolveCommands(group, commandRoot)] : [],
|
|
66
|
+
);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* The guard's `PreToolUse` groups with `${CLAUDE_PLUGIN_ROOT}` resolved against
|
|
69
|
-
* the
|
|
71
|
+
* the runtime link, so the command works with no launch environment.
|
|
70
72
|
*/
|
|
71
|
-
export function mateGuardHookGroups(
|
|
72
|
-
|
|
73
|
+
export function mateGuardHookGroups(
|
|
74
|
+
commandRoot: string = runtimeClaudePluginRoot(),
|
|
75
|
+
): ClaudeHookGroup[] {
|
|
76
|
+
return groupsFor("PreToolUse", GUARD_HOOK_SHIM, commandRoot);
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/**
|
|
@@ -79,9 +83,9 @@ export function mateGuardHookGroups(pluginRoot: string = getClaudePluginRoot()):
|
|
|
79
83
|
* flag.
|
|
80
84
|
*/
|
|
81
85
|
export function mateGuidanceHookGroups(
|
|
82
|
-
|
|
86
|
+
commandRoot: string = runtimeClaudePluginRoot(),
|
|
83
87
|
): ClaudeHookGroup[] {
|
|
84
|
-
return groupsFor("SessionStart", GUIDANCE_HOOK_SHIM,
|
|
88
|
+
return groupsFor("SessionStart", GUIDANCE_HOOK_SHIM, commandRoot);
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
@@ -91,9 +95,9 @@ export function mateGuidanceHookGroups(
|
|
|
91
95
|
* would print the banner twice.
|
|
92
96
|
*/
|
|
93
97
|
export function mateBannerHookGroups(
|
|
94
|
-
|
|
98
|
+
commandRoot: string = runtimeClaudePluginRoot(),
|
|
95
99
|
): ClaudeHookGroup[] {
|
|
96
|
-
return groupsFor("SessionStart", BANNER_HOOK_SHIM,
|
|
100
|
+
return groupsFor("SessionStart", BANNER_HOOK_SHIM, commandRoot).map((group) => ({
|
|
97
101
|
...group,
|
|
98
102
|
hooks: (group.hooks ?? []).map((hook) => ({
|
|
99
103
|
...hook,
|
|
@@ -102,16 +106,49 @@ export function mateBannerHookGroups(
|
|
|
102
106
|
}));
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The session-prepare `SessionStart` group, resolved the same way. Its declared
|
|
111
|
+
* `timeout` sits above the shim's own bound, so Claude Code never kills a
|
|
112
|
+
* prepare the shim has not given up on. Inert under a launch environment.
|
|
113
|
+
*/
|
|
114
|
+
export function mateSessionPrepareHookGroups(
|
|
115
|
+
commandRoot: string = runtimeClaudePluginRoot(),
|
|
116
|
+
): ClaudeHookGroup[] {
|
|
117
|
+
return groupsFor("SessionStart", PREPARE_HOOK_SHIM, commandRoot);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function mateOwnHookGroups(commandRoot: string): ClaudeHookGroup[] {
|
|
121
|
+
return [
|
|
122
|
+
...mateGuardHookGroups(commandRoot),
|
|
123
|
+
...mateGuidanceHookGroups(commandRoot),
|
|
124
|
+
...mateBannerHookGroups(commandRoot),
|
|
125
|
+
...mateSessionPrepareHookGroups(commandRoot),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ownGroupKeys = new Map<string, ReadonlySet<string>>();
|
|
130
|
+
|
|
131
|
+
/** Memoized per plugin-root pair; hooks are short-lived, so a refresh mid-process is not tracked. */
|
|
132
|
+
function mateOwnHookGroupKeys(runtimeRoot: string, installRoot: string): ReadonlySet<string> {
|
|
133
|
+
const memoKey = `${runtimeRoot}\0${installRoot}`;
|
|
134
|
+
let keys = ownGroupKeys.get(memoKey);
|
|
135
|
+
if (!keys) {
|
|
136
|
+
keys = new Set(
|
|
137
|
+
[...mateOwnHookGroups(runtimeRoot), ...mateOwnHookGroups(installRoot)].map(hookGroupKey),
|
|
138
|
+
);
|
|
139
|
+
ownGroupKeys.set(memoKey, keys);
|
|
140
|
+
}
|
|
141
|
+
return keys;
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
/**
|
|
106
145
|
* Whether a hook group is one the working target placed. It matches a managed
|
|
107
146
|
* marker, so the entry that strips managed groups from a Working Repository has
|
|
108
|
-
* to be told to leave these alone.
|
|
147
|
+
* to be told to leave these alone. Both the runtime-link form and the
|
|
148
|
+
* install-path form an older release wrote are recognized.
|
|
109
149
|
*/
|
|
110
150
|
export function isMateGuardHookGroup(group: ClaudeHookGroup): boolean {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(candidate) => JSON.stringify(candidate),
|
|
114
|
-
),
|
|
151
|
+
return mateOwnHookGroupKeys(runtimeClaudePluginRoot(), getClaudePluginRoot()).has(
|
|
152
|
+
hookGroupKey(group),
|
|
115
153
|
);
|
|
116
|
-
return own.has(JSON.stringify(group));
|
|
117
154
|
}
|