@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
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
// oxlint-disable no-await-in-loop
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
|
|
5
|
-
import { CURRENT_LAYER_LINK } from "../../runtime/audience-layer";
|
|
6
4
|
import { readProjectionFile } from "../../runtime/projection";
|
|
7
5
|
import { realLinkTarget, realOrResolved } from "../fs-utils";
|
|
8
6
|
import {
|
|
@@ -66,114 +64,63 @@ async function createSkillLink(target: string, link: string): Promise<void> {
|
|
|
66
64
|
await fs.symlink(target, link, process.platform === "win32" ? "junction" : "dir");
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
const CLAUDE_AGENTS_PATH = path.join(".claude", "agents");
|
|
70
|
-
|
|
71
|
-
/** The manifest key of an agent link: repo-relative, POSIX separators. */
|
|
72
|
-
function agentKey(file: string): string {
|
|
73
|
-
return `.claude/agents/${file}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function agentFiles(root: string): Promise<string[]> {
|
|
77
|
-
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
78
|
-
return entries
|
|
79
|
-
.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md"))
|
|
80
|
-
.map((entry) => entry.name);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Every link the wrap wants, by manifest key: the companion's skills, then the
|
|
85
|
-
* wrapped audience's skills and agents. An audience link targets the layer
|
|
86
|
-
* through its `current` link, so a re-rendered layer never strands it. A
|
|
87
|
-
* companion skill wins a name both provide.
|
|
88
|
-
*/
|
|
89
|
-
async function wantedLinks(
|
|
90
|
-
currentRoot: string | null,
|
|
91
|
-
layerRoot: string | null,
|
|
92
|
-
): Promise<Map<string, string>> {
|
|
93
|
-
const wanted = new Map<string, string>();
|
|
94
|
-
if (currentRoot) {
|
|
95
|
-
for (const name of await skillNames(currentRoot)) {
|
|
96
|
-
wanted.set(recordKey(name), path.join(currentRoot, name));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (layerRoot) {
|
|
100
|
-
const layerSkills = path.join(layerRoot, "skills");
|
|
101
|
-
for (const name of await skillNames(layerSkills)) {
|
|
102
|
-
if (!wanted.has(recordKey(name))) wanted.set(recordKey(name), path.join(layerSkills, name));
|
|
103
|
-
}
|
|
104
|
-
const layerAgents = path.join(layerRoot, "agents");
|
|
105
|
-
for (const file of await agentFiles(layerAgents)) {
|
|
106
|
-
wanted.set(agentKey(file), path.join(layerAgents, file));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return wanted;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
67
|
/**
|
|
113
68
|
* Recorded link: Mate's while it resolves to its recorded target. Unrecorded:
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* so a partial failure re-adopts.
|
|
69
|
+
* adopted only when it resolves into a registered, current or projected
|
|
70
|
+
* companion. Records follow the filesystem, so a partial failure re-adopts.
|
|
117
71
|
*/
|
|
118
72
|
export async function reconcileWorkingRepoClaudeSkillLinks(
|
|
119
73
|
repoPath: string,
|
|
120
74
|
companionPath: string | null,
|
|
121
75
|
enabled: boolean,
|
|
122
76
|
registeredCompanionPaths: string[] = [],
|
|
123
|
-
audienceLayerDir: string | null = null,
|
|
124
77
|
): Promise<"written" | "current"> {
|
|
125
|
-
const repoRoot = path.resolve(repoPath);
|
|
126
78
|
const currentRoot = companionPath ? path.resolve(companionPath, CLAUDE_SKILLS_PATH) : null;
|
|
127
|
-
const layerRoot =
|
|
128
|
-
audienceLayerDir && enabled ? path.join(audienceLayerDir, CURRENT_LAYER_LINK, ".claude") : null;
|
|
129
79
|
const roots = await registeredSkillRoots(repoPath, companionPath, registeredCompanionPaths);
|
|
80
|
+
const targetRoot = path.join(path.resolve(repoPath), CLAUDE_SKILLS_PATH);
|
|
130
81
|
const recorded = links(await readManifest(repoPath), "claude");
|
|
131
|
-
const
|
|
82
|
+
const existing = await fs.readdir(targetRoot, { withFileTypes: true }).catch(() => []);
|
|
83
|
+
const sourceNames: Set<string> =
|
|
84
|
+
currentRoot && enabled ? new Set(await skillNames(currentRoot)) : new Set<string>();
|
|
132
85
|
const records: Record<string, LinkRecord> = {};
|
|
133
86
|
let changed = false;
|
|
134
87
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (!owned) continue;
|
|
88
|
+
for (const entry of existing) {
|
|
89
|
+
if (!entry.isSymbolicLink()) continue;
|
|
90
|
+
const link = path.join(targetRoot, entry.name);
|
|
91
|
+
const target = await linkTarget(link);
|
|
92
|
+
if (target === null) continue;
|
|
93
|
+
const record = recorded[recordKey(entry.name)];
|
|
94
|
+
const owned = record
|
|
95
|
+
? (await realOrResolved(record.target)) === target
|
|
96
|
+
: roots.some((root) => path.dirname(target) === root);
|
|
97
|
+
if (!owned) continue;
|
|
98
|
+
|
|
99
|
+
const wanted = currentRoot ? path.join(currentRoot, entry.name) : null;
|
|
100
|
+
if (wanted && sourceNames.has(entry.name) && (await realOrResolved(wanted)) === target) {
|
|
101
|
+
records[recordKey(entry.name)] = { runtime: "claude", target: wanted };
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
152
104
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
105
|
+
await fs.unlink(link);
|
|
106
|
+
changed = true;
|
|
107
|
+
}
|
|
158
108
|
|
|
159
|
-
|
|
109
|
+
if (currentRoot && sourceNames.size > 0) {
|
|
110
|
+
await fs.mkdir(targetRoot, { recursive: true });
|
|
111
|
+
for (const name of sourceNames) {
|
|
112
|
+
const link = path.join(targetRoot, name);
|
|
113
|
+
const existingLink = await fs.lstat(link).catch(() => null);
|
|
114
|
+
if (existingLink) continue;
|
|
115
|
+
const target = path.join(currentRoot, name);
|
|
116
|
+
await createSkillLink(target, link);
|
|
117
|
+
records[recordKey(name)] = { runtime: "claude", target };
|
|
160
118
|
changed = true;
|
|
161
119
|
}
|
|
162
120
|
}
|
|
163
121
|
|
|
164
|
-
for (const [key, target] of wanted) {
|
|
165
|
-
const link = path.join(repoRoot, ...key.split("/"));
|
|
166
|
-
const existingLink = await fs.lstat(link).catch(() => null);
|
|
167
|
-
if (existingLink) continue;
|
|
168
|
-
await fs.mkdir(path.dirname(link), { recursive: true });
|
|
169
|
-
if (key.startsWith(".claude/agents/")) await fs.symlink(target, link, "file");
|
|
170
|
-
else await createSkillLink(target, link);
|
|
171
|
-
records[key] = { runtime: "claude", target };
|
|
172
|
-
changed = true;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
122
|
if (changed || !enabled) {
|
|
176
|
-
|
|
123
|
+
await pruneEmptyAncestors(targetRoot, path.resolve(repoPath));
|
|
177
124
|
}
|
|
178
125
|
if (JSON.stringify(sortedEntries(records)) !== JSON.stringify(sortedEntries(recorded))) {
|
|
179
126
|
await updateManifest(repoPath, (manifest) => withLinks(manifest, "claude", records));
|
|
@@ -50,8 +50,6 @@ export interface ProjectionInput {
|
|
|
50
50
|
runtimeDocuments?: readonly RenderedRuntimeDocument[];
|
|
51
51
|
/** Restricts runtime-tagged entries to these runtimes; every runtime when absent. */
|
|
52
52
|
runtimes?: readonly WrapRuntime[];
|
|
53
|
-
/** The layer directory of the audience the checkout is wrapped for; `null` or absent for base only. */
|
|
54
|
-
audienceLayerDir?: string | null;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
export interface ProjectionRemovalInput {
|
|
@@ -7,14 +7,8 @@ import {
|
|
|
7
7
|
syncCompanionUnattended,
|
|
8
8
|
type UnattendedSyncOutcome,
|
|
9
9
|
} from "../../runtime/companion-sync";
|
|
10
|
-
import {
|
|
10
|
+
import type { WrapState } from "../../runtime/wrap-state";
|
|
11
11
|
import { syncCompanionFiles } from "../../tools/setup";
|
|
12
|
-
import { renderAudienceLayer, type RenderedAudienceLayer } from "../../tools/setup/audience-layer";
|
|
13
|
-
import {
|
|
14
|
-
readAudienceSelection,
|
|
15
|
-
resolveAudience,
|
|
16
|
-
} from "../../tools/setup/dynamic-plugins/audiences";
|
|
17
|
-
import { buildSetupInstallationPlan } from "../../tools/setup/engine";
|
|
18
12
|
import type { CapabilityPlugin, LaunchPreflightContext } from "../../tools/setup/plugin";
|
|
19
13
|
import { attendedLockOptions, syncCompanionGit } from "./companion-git-sync";
|
|
20
14
|
import { readCompanionConfig } from "./config-store";
|
|
@@ -40,8 +34,7 @@ export type PrepareStepName =
|
|
|
40
34
|
| "file-sync"
|
|
41
35
|
| "launch-projection"
|
|
42
36
|
| "working-target"
|
|
43
|
-
| "preflight"
|
|
44
|
-
| "audience-layer";
|
|
37
|
+
| "preflight";
|
|
45
38
|
|
|
46
39
|
export interface PrepareStep {
|
|
47
40
|
step: PrepareStepName;
|
|
@@ -68,8 +61,6 @@ export interface PrepareResult {
|
|
|
68
61
|
failure?: unknown;
|
|
69
62
|
/** The wrap state the `working-target` step's pass left; a launch reads no manifest itself. */
|
|
70
63
|
wrapState?: WrapState;
|
|
71
|
-
/** The active audience's layer, rendered for an attended prepare only. */
|
|
72
|
-
audienceLayer?: RenderedAudienceLayer;
|
|
73
64
|
}
|
|
74
65
|
|
|
75
66
|
export interface PrepareSessionInput {
|
|
@@ -98,10 +89,9 @@ const SYNC_LOCK_TIMEOUT_MS = 5 * 60_000;
|
|
|
98
89
|
*/
|
|
99
90
|
async function runCapabilityPreflight(context: LaunchPreflightContext): Promise<void> {
|
|
100
91
|
const { config, providerId } = context;
|
|
101
|
-
/** Base only: an audience plugin's `forProvider` hooks never run. */
|
|
102
92
|
const preflights = getActiveDistribution()
|
|
103
|
-
.registry.
|
|
104
|
-
.flatMap((
|
|
93
|
+
.registry.getAll()
|
|
94
|
+
.flatMap((plugin) => {
|
|
105
95
|
if (plugin.kind !== "capability" || !plugin.isEnabled(config)) return [];
|
|
106
96
|
const capability = plugin as CapabilityPlugin;
|
|
107
97
|
const preflight = capability.forProvider?.[providerId]?.preflight;
|
|
@@ -147,11 +137,6 @@ export const sessionPrepareDeps = {
|
|
|
147
137
|
withCompanionLock,
|
|
148
138
|
/** As the launch decides it: Git may prompt only with a terminal on both ends. */
|
|
149
139
|
isInteractiveTerminal: () => !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
150
|
-
/** The launch's `MATE_AUDIENCE`; read by an attended prepare only. */
|
|
151
|
-
audienceSelection: () => readAudienceSelection(process.env),
|
|
152
|
-
/** The audience a checkout is wrapped for, from its manifest; `null` for base. */
|
|
153
|
-
wrappedAudience: (repoPath: string) => readWrapStateSync(repoPath).audience,
|
|
154
|
-
renderAudienceLayer,
|
|
155
140
|
};
|
|
156
141
|
|
|
157
142
|
function message(error: unknown): string {
|
|
@@ -164,7 +149,6 @@ class StepLog {
|
|
|
164
149
|
readonly warnings: string[] = [];
|
|
165
150
|
failure: unknown;
|
|
166
151
|
wrapState: WrapState | undefined;
|
|
167
|
-
audienceLayer: RenderedAudienceLayer | undefined;
|
|
168
152
|
stopped = false;
|
|
169
153
|
companionChanged = false;
|
|
170
154
|
|
|
@@ -337,71 +321,12 @@ async function runLocal(
|
|
|
337
321
|
});
|
|
338
322
|
}
|
|
339
323
|
|
|
340
|
-
/**
|
|
341
|
-
* The active audience's launch layer, after the base prepare. Only a managed
|
|
342
|
-
* launch reaches here: a bare session's prepare never renders one, whatever
|
|
343
|
-
* `MATE_AUDIENCE` its environment carries. An audience the companion does not
|
|
344
|
-
* declare renders nothing; hydration already warned about it.
|
|
345
|
-
*/
|
|
346
|
-
async function renderLayer(
|
|
347
|
-
input: PrepareSessionInput,
|
|
348
|
-
config: FrameworkConfig,
|
|
349
|
-
run: StepLog,
|
|
350
|
-
): Promise<void> {
|
|
351
|
-
const deps = sessionPrepareDeps;
|
|
352
|
-
/** Adopted from the wrap: the wrap delivers it, so the launch renders no layer of its own. */
|
|
353
|
-
const wrapped = input.repository ? deps.wrappedAudience(input.repository.path) : null;
|
|
354
|
-
if (wrapped) {
|
|
355
|
-
run.push({ step: "audience-layer", outcome: "ok", detail: `${wrapped}: from the wrap` });
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
const audience = resolveAudience(config, deps.audienceSelection());
|
|
359
|
-
if (!audience.ok || audience.active === null) return;
|
|
360
|
-
try {
|
|
361
|
-
const registry = getActiveDistribution().registry;
|
|
362
|
-
const layer = await deps.renderAudienceLayer({
|
|
363
|
-
companionPath: input.companionPath,
|
|
364
|
-
config,
|
|
365
|
-
audience: audience.active,
|
|
366
|
-
activeProviders: buildSetupInstallationPlan(config, registry.baseRegistrations())
|
|
367
|
-
.activeProviders,
|
|
368
|
-
registrations: registry.audienceRegistrations(audience.active),
|
|
369
|
-
});
|
|
370
|
-
run.audienceLayer = layer;
|
|
371
|
-
run.push({
|
|
372
|
-
step: "audience-layer",
|
|
373
|
-
outcome: "ok",
|
|
374
|
-
detail: `${layer.audience}: ${layer.rendered ? "rendered" : "current"}`,
|
|
375
|
-
});
|
|
376
|
-
} catch (error) {
|
|
377
|
-
run.fail("audience-layer", error);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* A checkout wrapped for one audience refuses a launch for another before
|
|
383
|
-
* anything runs: the wrap's on-disk content would reach that session.
|
|
384
|
-
*/
|
|
385
|
-
function audienceConflict(input: PrepareSessionInput, config: FrameworkConfig): string | null {
|
|
386
|
-
if (!input.repository) return null;
|
|
387
|
-
const wrapped = sessionPrepareDeps.wrappedAudience(input.repository.path);
|
|
388
|
-
if (!wrapped) return null;
|
|
389
|
-
const selected = resolveAudience(config, sessionPrepareDeps.audienceSelection());
|
|
390
|
-
if (!selected.ok || selected.active === null || selected.active === wrapped) return null;
|
|
391
|
-
return `${FRAMEWORK_NAME}: ${input.repository.id} is wrapped for audience "${wrapped}", but MATE_AUDIENCE selects "${selected.active}". Run \`${FRAMEWORK_NAME} wrap --audience ${selected.active}\` in ${input.repository.path}, or launch from another worktree wrapped for "${selected.active}".`;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
324
|
async function runAttended(
|
|
395
325
|
input: PrepareSessionInput,
|
|
396
326
|
config: FrameworkConfig,
|
|
397
327
|
run: StepLog,
|
|
398
328
|
): Promise<void> {
|
|
399
329
|
const deps = sessionPrepareDeps;
|
|
400
|
-
const conflict = audienceConflict(input, config);
|
|
401
|
-
if (conflict) {
|
|
402
|
-
run.fail("audience-layer", new LaunchPreflightError(conflict));
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
330
|
if (input.skipGit) {
|
|
406
331
|
run.push({ step: "git-sync", outcome: "skipped", detail: "--no-git" });
|
|
407
332
|
} else if (config.git !== "auto") {
|
|
@@ -422,8 +347,6 @@ async function runAttended(
|
|
|
422
347
|
}
|
|
423
348
|
}
|
|
424
349
|
await runLocal(input, config, run);
|
|
425
|
-
if (run.stopped) return;
|
|
426
|
-
await renderLayer(input, config, run);
|
|
427
350
|
}
|
|
428
351
|
|
|
429
352
|
/** The sync's own config read: only a fast-forward can have changed it. */
|
|
@@ -478,7 +401,6 @@ export async function prepareSession(input: PrepareSessionInput): Promise<Prepar
|
|
|
478
401
|
...(run.companionChanged ? { companionChanged: true as const } : {}),
|
|
479
402
|
...(run.failure === undefined ? {} : { failure: run.failure }),
|
|
480
403
|
...(run.wrapState ? { wrapState: run.wrapState } : {}),
|
|
481
|
-
...(run.audienceLayer ? { audienceLayer: run.audienceLayer } : {}),
|
|
482
404
|
});
|
|
483
405
|
|
|
484
406
|
const locked = await deps.withCompanionLock(input.companionPath, lockOptions(input), async () => {
|
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import { renderWorkingRuntimeDocuments } from "../../tools/setup";
|
|
4
|
-
import { renderAudienceLayer } from "../../tools/setup/audience-layer";
|
|
5
|
-
import { loadAudienceRegistrations } from "../../tools/setup/audience-registrations";
|
|
6
|
-
import { declaredAudiences } from "../../tools/setup/dynamic-plugins/audiences";
|
|
7
|
-
import { buildSetupInstallationPlan } from "../../tools/setup/engine";
|
|
8
|
-
import { getActiveDistribution } from "../../distribution";
|
|
9
|
-
import { audienceLayerDir } from "../../runtime/audience-layer";
|
|
10
|
-
import {
|
|
11
|
-
readManifest,
|
|
12
|
-
recordedAudience,
|
|
13
|
-
updateManifest,
|
|
14
|
-
withAudience,
|
|
15
|
-
} from "../../runtime/runtime-document-manifest";
|
|
16
4
|
import { readCompanionConfig } from "./config-store";
|
|
17
5
|
import { GlobalConfigStore } from "./global-config-store";
|
|
18
6
|
import { entryFailureMessage, firstFailure, type ProjectionChangedPaths } from "./projection-types";
|
|
19
7
|
import type { WrapRuntime } from "../../runtime/runtime-document-manifest";
|
|
20
8
|
import { readWrapState, type WrapState } from "../../runtime/wrap-state";
|
|
21
|
-
import type { NormalizedRegistration } from "../../tools/setup/registry";
|
|
22
9
|
import type { FrameworkConfig, LinkedRepository } from "./types";
|
|
23
10
|
import { project } from "./working-repo-projection";
|
|
24
11
|
|
|
@@ -26,8 +13,6 @@ export interface WrapPassDependencies {
|
|
|
26
13
|
renderWorkingRuntimeDocuments: typeof renderWorkingRuntimeDocuments;
|
|
27
14
|
readCompanionConfig: typeof readCompanionConfig;
|
|
28
15
|
project: typeof project;
|
|
29
|
-
loadAudienceRegistrations: typeof loadAudienceRegistrations;
|
|
30
|
-
renderAudienceLayer: typeof renderAudienceLayer;
|
|
31
16
|
/** The registered companions, whose stale skill links a re-pin withdraws; the user's registry when absent. */
|
|
32
17
|
globalConfigStore?: GlobalConfigStore;
|
|
33
18
|
}
|
|
@@ -36,8 +21,6 @@ export const wrapPassDeps: WrapPassDependencies = {
|
|
|
36
21
|
renderWorkingRuntimeDocuments,
|
|
37
22
|
readCompanionConfig,
|
|
38
23
|
project,
|
|
39
|
-
loadAudienceRegistrations,
|
|
40
|
-
renderAudienceLayer,
|
|
41
24
|
};
|
|
42
25
|
|
|
43
26
|
export interface WrapPassOptions {
|
|
@@ -45,12 +28,6 @@ export interface WrapPassOptions {
|
|
|
45
28
|
runtimes?: readonly WrapRuntime[];
|
|
46
29
|
/** The companion config the caller already read; read from files otherwise. */
|
|
47
30
|
config?: FrameworkConfig;
|
|
48
|
-
/**
|
|
49
|
-
* The audience to wrap the checkout for: a name records it, `null` withdraws
|
|
50
|
-
* it, absent keeps the one the manifest records. Never read from the
|
|
51
|
-
* environment: it is the only source of a bare session's audience.
|
|
52
|
-
*/
|
|
53
|
-
audience?: string | null;
|
|
54
31
|
}
|
|
55
32
|
|
|
56
33
|
export type WrapPassResult =
|
|
@@ -70,21 +47,9 @@ export type WrapPassResult =
|
|
|
70
47
|
runtimes: ReadonlySet<WrapRuntime>;
|
|
71
48
|
/** The wrap state as this pass left it. */
|
|
72
49
|
wrapState: WrapState;
|
|
73
|
-
/** The audience the checkout is wrapped for after this pass. */
|
|
74
|
-
audience: string | null;
|
|
75
50
|
}
|
|
76
51
|
| { kind: "failed"; message: string };
|
|
77
52
|
|
|
78
|
-
/** An audience a checkout may be wrapped for: declared by the companion, never the reserved base. */
|
|
79
|
-
function wrappableAudience(config: FrameworkConfig, audience: string): boolean {
|
|
80
|
-
return audience !== "base" && declaredAudiences(config).includes(audience);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function describeUnwrappable(config: FrameworkConfig, audience: string): string {
|
|
84
|
-
const declared = declaredAudiences(config).filter((name) => name !== "base");
|
|
85
|
-
return `audience "${audience}" cannot be wrapped: the companion declares ${declared.length > 0 ? declared.join(", ") : "no audiences"}.`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
53
|
/**
|
|
89
54
|
* One pass of the `wrap` scope, restricted to the requested runtimes by each
|
|
90
55
|
* entry's runtime tag. Best-effort per entry like every projection: a failed
|
|
@@ -98,50 +63,10 @@ export async function runWrapPass(
|
|
|
98
63
|
): Promise<WrapPassResult> {
|
|
99
64
|
const resolvedCompanionPath = path.resolve(companionPath);
|
|
100
65
|
const config = options.config ?? (await deps.readCompanionConfig(resolvedCompanionPath));
|
|
101
|
-
|
|
102
|
-
const audienceWarnings: string[] = [];
|
|
103
|
-
let audience: string | null;
|
|
104
|
-
if (options.audience === undefined) {
|
|
105
|
-
const recorded = recordedAudience(await readManifest(repository.path));
|
|
106
|
-
/** A recorded audience the companion no longer declares falls back to base, never fails a prepare. */
|
|
107
|
-
if (recorded && !wrappableAudience(config, recorded)) {
|
|
108
|
-
audienceWarnings.push(
|
|
109
|
-
`${describeUnwrappable(config, recorded)} ${repository.id} is wrapped for base only.`,
|
|
110
|
-
);
|
|
111
|
-
audience = null;
|
|
112
|
-
} else {
|
|
113
|
-
audience = recorded;
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
audience = options.audience;
|
|
117
|
-
if (audience && !wrappableAudience(config, audience)) {
|
|
118
|
-
return { kind: "failed", message: describeUnwrappable(config, audience) };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let wrapped: { audience: string; registrations: NormalizedRegistration[] } | undefined;
|
|
123
|
-
if (audience) {
|
|
124
|
-
const loaded = await deps.loadAudienceRegistrations(resolvedCompanionPath, config, audience);
|
|
125
|
-
audienceWarnings.push(...loaded.warnings);
|
|
126
|
-
await deps.renderAudienceLayer({
|
|
127
|
-
companionPath: resolvedCompanionPath,
|
|
128
|
-
config,
|
|
129
|
-
audience,
|
|
130
|
-
activeProviders: buildSetupInstallationPlan(
|
|
131
|
-
config,
|
|
132
|
-
getActiveDistribution().registry.baseRegistrations(),
|
|
133
|
-
).activeProviders,
|
|
134
|
-
registrations: loaded.registrations,
|
|
135
|
-
});
|
|
136
|
-
wrapped = { audience, registrations: loaded.registrations };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
66
|
const rendered = await deps.renderWorkingRuntimeDocuments(
|
|
140
67
|
resolvedCompanionPath,
|
|
141
68
|
config,
|
|
142
69
|
repository.path,
|
|
143
|
-
undefined,
|
|
144
|
-
wrapped,
|
|
145
70
|
);
|
|
146
71
|
const result = await deps.project("wrap", {
|
|
147
72
|
repoPath: repository.path,
|
|
@@ -149,7 +74,6 @@ export async function runWrapPass(
|
|
|
149
74
|
repository,
|
|
150
75
|
config,
|
|
151
76
|
runtimeDocuments: rendered.documents,
|
|
152
|
-
audienceLayerDir: audience ? audienceLayerDir(resolvedCompanionPath, audience) : null,
|
|
153
77
|
globalConfigStore: deps.globalConfigStore ?? new GlobalConfigStore(),
|
|
154
78
|
...(options.runtimes ? { runtimes: options.runtimes } : {}),
|
|
155
79
|
});
|
|
@@ -157,12 +81,6 @@ export async function runWrapPass(
|
|
|
157
81
|
const failure = firstFailure(result.outcomes);
|
|
158
82
|
if (failure) return { kind: "failed", message: entryFailureMessage(failure, repository.id) };
|
|
159
83
|
|
|
160
|
-
const before = recordedAudience(await readManifest(repository.path));
|
|
161
|
-
const audienceChanged = before !== audience;
|
|
162
|
-
if (audienceChanged) {
|
|
163
|
-
await updateManifest(repository.path, (manifest) => withAudience(manifest, audience));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
84
|
/** Read once under the caller's lock, so it is exactly what the pass recorded. */
|
|
167
85
|
let wrapState: WrapState;
|
|
168
86
|
try {
|
|
@@ -194,10 +112,7 @@ export async function runWrapPass(
|
|
|
194
112
|
.map((outcome) => outcome.path);
|
|
195
113
|
|
|
196
114
|
return {
|
|
197
|
-
kind:
|
|
198
|
-
audienceChanged || result.outcomes.some((outcome) => outcome.state === "written")
|
|
199
|
-
? "written"
|
|
200
|
-
: "current",
|
|
115
|
+
kind: result.outcomes.some((outcome) => outcome.state === "written") ? "written" : "current",
|
|
201
116
|
projectionRoot: result.root,
|
|
202
117
|
companionPath: resolvedCompanionPath,
|
|
203
118
|
documents: [...new Set(documents)],
|
|
@@ -207,7 +122,6 @@ export async function runWrapPass(
|
|
|
207
122
|
removed: result.outcomes.flatMap((outcome) => outcome.changed?.removed ?? []),
|
|
208
123
|
},
|
|
209
124
|
warnings: [
|
|
210
|
-
...audienceWarnings,
|
|
211
125
|
...rendered.warnings,
|
|
212
126
|
...result.outcomes.flatMap((outcome) => outcome.warnings ?? []),
|
|
213
127
|
/** A degradable failure does not fail the pass, but it is never silent. */
|
|
@@ -217,6 +131,5 @@ export async function runWrapPass(
|
|
|
217
131
|
],
|
|
218
132
|
runtimes,
|
|
219
133
|
wrapState,
|
|
220
|
-
audience,
|
|
221
134
|
};
|
|
222
135
|
}
|
package/src/runtime/guidance.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Versioned companion guidance contract. The CLI builds the guidance data
|
|
5
3
|
* from its playbooks at launch time and delivers it through the
|
|
@@ -18,12 +16,6 @@ export type MateGuidanceFile = {
|
|
|
18
16
|
companionGuidance: string;
|
|
19
17
|
codebaseExplorationGuidance: string;
|
|
20
18
|
errors: string[];
|
|
21
|
-
/**
|
|
22
|
-
* The active audience's layer, for a launch that hosts its own server only.
|
|
23
|
-
* The plugin applies it only from a payload it trusts; a masked payload
|
|
24
|
-
* carries none, so the shared service and bare sessions never see one.
|
|
25
|
-
*/
|
|
26
|
-
audienceLayer?: string;
|
|
27
19
|
};
|
|
28
20
|
|
|
29
21
|
export type GuidanceValidation = {
|
|
@@ -84,21 +76,12 @@ export function validateGuidanceData(value: unknown): GuidanceValidation {
|
|
|
84
76
|
errors.push("injected codebase exploration guidance is malformed");
|
|
85
77
|
}
|
|
86
78
|
|
|
87
|
-
const audienceLayer =
|
|
88
|
-
typeof value.audienceLayer === "string" && path.isAbsolute(value.audienceLayer)
|
|
89
|
-
? value.audienceLayer
|
|
90
|
-
: undefined;
|
|
91
|
-
if (value.audienceLayer !== undefined && audienceLayer === undefined) {
|
|
92
|
-
errors.push("injected audience layer is not an absolute path");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
79
|
return {
|
|
96
80
|
guidance: {
|
|
97
81
|
version: GUIDANCE_FILE_VERSION,
|
|
98
82
|
companionGuidance,
|
|
99
83
|
codebaseExplorationGuidance,
|
|
100
84
|
errors,
|
|
101
|
-
...(audienceLayer ? { audienceLayer } : {}),
|
|
102
85
|
},
|
|
103
86
|
errors,
|
|
104
87
|
};
|
package/src/runtime/policy.ts
CHANGED
|
@@ -40,17 +40,6 @@ function capabilityNames(value: unknown): string[] {
|
|
|
40
40
|
.filter((name): name is string => typeof name === "string");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* A base-set key from `audiences.base`, the shape a companion is saved in. The
|
|
45
|
-
* lenient twin of the setup normalizer, which `runtime/` cannot import.
|
|
46
|
-
*/
|
|
47
|
-
function baseAudienceKey(config: Record<string, unknown>, key: string): unknown {
|
|
48
|
-
const audiences = config.audiences;
|
|
49
|
-
if (!audiences || typeof audiences !== "object") return undefined;
|
|
50
|
-
const base = (audiences as Record<string, unknown>).base;
|
|
51
|
-
return base && typeof base === "object" ? (base as Record<string, unknown>)[key] : undefined;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
43
|
/**
|
|
55
44
|
* Synchronous so it composes into `readCompanionRuntimeContext`. A missing or
|
|
56
45
|
* unparseable companion config yields an inert policy rather than throwing.
|
|
@@ -67,9 +56,7 @@ export function readCompanionPolicy(companionPath: string): CompanionPolicy {
|
|
|
67
56
|
const config = parsed as Record<string, unknown>;
|
|
68
57
|
return {
|
|
69
58
|
allowedAgents: stringList(config.allowedAgents),
|
|
70
|
-
enabledCapabilities: capabilityNames(
|
|
71
|
-
config.capabilities ?? baseAudienceKey(config, "capabilities"),
|
|
72
|
-
),
|
|
59
|
+
enabledCapabilities: capabilityNames(config.capabilities),
|
|
73
60
|
gitAutoMode: config.git === "auto",
|
|
74
61
|
};
|
|
75
62
|
}
|
|
@@ -80,8 +80,6 @@ export interface Manifest {
|
|
|
80
80
|
readonly runtimes: Readonly<Record<string, WrapRuntime>>;
|
|
81
81
|
readonly copies: Readonly<Record<string, CopyRecord>>;
|
|
82
82
|
readonly links: Readonly<Record<string, LinkRecord>>;
|
|
83
|
-
/** The audience `mate wrap --audience` recorded; the only source of a bare session's audience. */
|
|
84
|
-
readonly audience?: string;
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
export const EMPTY_MANIFEST: Manifest = Object.freeze({
|
|
@@ -126,23 +124,9 @@ function manifestOf(parsed: Record<string, unknown>): Manifest {
|
|
|
126
124
|
runtimes: isRecord(parsed.runtimes) ? (parsed.runtimes as Manifest["runtimes"]) : {},
|
|
127
125
|
copies: isRecord(parsed.copies) ? (parsed.copies as Manifest["copies"]) : {},
|
|
128
126
|
links: isRecord(parsed.links) ? (parsed.links as Manifest["links"]) : {},
|
|
129
|
-
...(typeof parsed.audience === "string" && parsed.audience
|
|
130
|
-
? { audience: parsed.audience }
|
|
131
|
-
: {}),
|
|
132
127
|
};
|
|
133
128
|
}
|
|
134
129
|
|
|
135
|
-
/** The audience the checkout is wrapped for; `null` for base only. */
|
|
136
|
-
export function recordedAudience(manifest: Manifest): string | null {
|
|
137
|
-
return manifest.audience ?? null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Records the checkout's audience, or none with `null`. */
|
|
141
|
-
export function withAudience(manifest: Manifest, audience: string | null): Manifest {
|
|
142
|
-
const { audience: _previous, ...rest } = manifest;
|
|
143
|
-
return audience ? { ...rest, audience } : rest;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
130
|
function recordedRuntime(manifest: Manifest, documentPath: string): WrapRuntime {
|
|
147
131
|
return manifest.runtimes[documentPath] ?? documentRuntime(documentPath);
|
|
148
132
|
}
|
|
@@ -166,7 +150,6 @@ export function serializeManifest(manifest: Manifest): string | null {
|
|
|
166
150
|
...(recorded.length > 0 ? { runtimes } : {}),
|
|
167
151
|
...(copied.length > 0 ? { copies: manifest.copies } : {}),
|
|
168
152
|
...(linked.length > 0 ? { links: manifest.links } : {}),
|
|
169
|
-
...(manifest.audience ? { audience: manifest.audience } : {}),
|
|
170
153
|
},
|
|
171
154
|
null,
|
|
172
155
|
2,
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
parseManifestStrict,
|
|
15
15
|
readManifestText,
|
|
16
16
|
recordedListValues,
|
|
17
|
-
recordedAudience,
|
|
18
17
|
recordedMapKeys,
|
|
19
18
|
type Manifest,
|
|
20
19
|
type WrapRuntime,
|
|
@@ -31,8 +30,6 @@ export interface WrapState {
|
|
|
31
30
|
carriesClaudeHookGroup(group: ClaudeHookGroup): boolean;
|
|
32
31
|
/** MCP server names the wrap declared in `.opencode/opencode.json`; user servers are not listed. */
|
|
33
32
|
readonly openCodeMcpServers: readonly string[];
|
|
34
|
-
/** The audience the checkout is wrapped for; `null` for base only. */
|
|
35
|
-
readonly audience: string | null;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
export const WRAP_RUNTIMES: readonly WrapRuntime[] = ["claude", "opencode"];
|
|
@@ -64,7 +61,6 @@ export function wrapStateOf(manifest: Manifest): WrapState {
|
|
|
64
61
|
OPENCODE_CONFIG_DOCUMENT,
|
|
65
62
|
OPENCODE_MCP_SERVERS_AT,
|
|
66
63
|
),
|
|
67
|
-
audience: recordedAudience(manifest),
|
|
68
64
|
};
|
|
69
65
|
}
|
|
70
66
|
|