@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,44 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
8
|
+
import { gitEnvironment } from "../../runtime/companion-git";
|
|
9
|
+
import {
|
|
10
|
+
documentRegions,
|
|
11
|
+
readManifest,
|
|
12
|
+
updateManifest,
|
|
13
|
+
withDocument,
|
|
14
|
+
withoutDocuments,
|
|
15
|
+
type ManagedRegion,
|
|
16
|
+
type RenderedRuntimeDocument,
|
|
17
|
+
} from "../../runtime/runtime-document-manifest";
|
|
18
|
+
import { writeAtomic } from "../../runtime/write-atomic";
|
|
7
19
|
import { pruneEmptyAncestors } from "../../tools/setup/utils";
|
|
8
|
-
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
CLAUDE_LOCAL_CONFIG_DOCUMENT,
|
|
23
|
+
CLAUDE_MCP_DOCUMENT,
|
|
24
|
+
CLAUDE_SETTINGS_DOCUMENT,
|
|
25
|
+
OPENCODE_CONFIG_DOCUMENT,
|
|
26
|
+
} from "../../runtime/runtime-document-manifest";
|
|
9
27
|
|
|
10
28
|
/**
|
|
11
29
|
* Places the documents an Agent Runtime discovers by its own upward walk from
|
|
12
30
|
* the current directory. A Runtime Surface renders one as a value; this module
|
|
13
31
|
* writes it, records the regions it wrote at the Projection Root, and removes
|
|
14
32
|
* exactly those again — which is what makes a working-target document swept by
|
|
15
|
-
* `mate working cleanup` without cleanup holding a list of them.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const MANIFEST_FILE = "runtime-documents.json";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The three destinations, repo-relative with POSIX separators. Declared here
|
|
22
|
-
* rather than beside the renderers so the entry catalogue names a destination
|
|
23
|
-
* without reaching into an Agent Runtime's Runtime Surface.
|
|
24
|
-
*/
|
|
25
|
-
export const CLAUDE_SETTINGS_DOCUMENT = ".claude/settings.local.json";
|
|
26
|
-
export const CLAUDE_MCP_DOCUMENT = ".mcp.json";
|
|
27
|
-
export const OPENCODE_CONFIG_DOCUMENT = ".opencode/opencode.json";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The one destination outside the Working Repository. Claude Code has no
|
|
31
|
-
* project file that declares a pre-approved MCP server: `.mcp.json` servers sit
|
|
32
|
-
* "pending approval" until a human accepts them in a session, and the settings
|
|
33
|
-
* document can only enable a server `.mcp.json` already declares. Local scope —
|
|
34
|
-
* `projects[<repo>].mcpServers` in the user's `~/.claude.json`, what
|
|
35
|
-
* `claude mcp add --scope local` writes — is the only channel that is live on
|
|
36
|
-
* first use.
|
|
37
|
-
*
|
|
38
|
-
* Written `~`-prefixed rather than resolved so the manifest recording it stays
|
|
39
|
-
* machine-independent, and so an external target is identifiable as one.
|
|
33
|
+
* `mate working cleanup` without cleanup holding a list of them. The manifest
|
|
34
|
+
* — format, I/O and queries — belongs to `runtime/runtime-document-manifest`.
|
|
40
35
|
*/
|
|
41
|
-
export const CLAUDE_LOCAL_CONFIG_DOCUMENT = "~/.claude.json";
|
|
42
36
|
|
|
43
37
|
/**
|
|
44
38
|
* A document Mate does not own the surroundings of. Mate's regions are stripped
|
|
@@ -50,6 +44,38 @@ export function isExternalDocument(documentPath: string): boolean {
|
|
|
50
44
|
return documentPath.startsWith("~/");
|
|
51
45
|
}
|
|
52
46
|
|
|
47
|
+
const execFileAsync = promisify(execFile);
|
|
48
|
+
|
|
49
|
+
/** Whether Git tracks the repo-relative path; any `ls-files` failure reads as untracked. */
|
|
50
|
+
async function gitTracks(repoPath: string, relativePath: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
const { stdout } = await execFileAsync("git", ["ls-files", "-z", "--", relativePath], {
|
|
53
|
+
cwd: path.resolve(repoPath),
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
env: gitEnvironment(),
|
|
56
|
+
});
|
|
57
|
+
return stdout.split("\0").includes(relativePath);
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Excludes don't hide tracked files, so regions with machine-absolute paths
|
|
65
|
+
* would land in a committed document: warn, still write. Best-effort: any
|
|
66
|
+
* `ls-files` failure reads as "not tracked".
|
|
67
|
+
*/
|
|
68
|
+
export async function trackedDocumentWarnings(
|
|
69
|
+
repoPath: string,
|
|
70
|
+
documentPath: string,
|
|
71
|
+
repositoryId: string,
|
|
72
|
+
): Promise<string[]> {
|
|
73
|
+
if (!(await gitTracks(repoPath, documentPath))) return [];
|
|
74
|
+
return [
|
|
75
|
+
`${documentPath} is tracked by Git in ${repositoryId}; ${FRAMEWORK_NAME} writes machine-absolute companion paths into it, so committing it would push this machine's paths to everyone else on the repository. Untrack it with \`git rm --cached ${documentPath}\` if those paths should stay local.`,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
/**
|
|
54
80
|
* Injectable so a test never writes to the real `~/.claude.json`. An external
|
|
55
81
|
* target is the one destination outside a temporary fixture, so it is also the
|
|
@@ -59,14 +85,6 @@ export const runtimeDocumentDeps = {
|
|
|
59
85
|
homeDir: (): string => os.homedir(),
|
|
60
86
|
};
|
|
61
87
|
|
|
62
|
-
interface Manifest {
|
|
63
|
-
documents: Record<string, ManagedRegion[]>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function manifestPath(repoPath: string): string {
|
|
67
|
-
return path.join(repoLocalDirPath(repoPath), MANIFEST_FILE);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
88
|
function documentPathIn(repoPath: string, documentPath: string): string {
|
|
71
89
|
if (isExternalDocument(documentPath)) {
|
|
72
90
|
return path.join(runtimeDocumentDeps.homeDir(), ...documentPath.slice("~/".length).split("/"));
|
|
@@ -97,26 +115,9 @@ async function readRaw(filePath: string): Promise<string | null> {
|
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
117
|
|
|
100
|
-
function parseObject(raw: string | null): Record<string, unknown> {
|
|
101
|
-
if (raw === null) return {};
|
|
102
|
-
try {
|
|
103
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
104
|
-
if (isRecord(parsed)) return parsed;
|
|
105
|
-
} catch {
|
|
106
|
-
/* unparseable — start from an empty object */
|
|
107
|
-
}
|
|
108
|
-
return {};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
118
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* the manifest — Mate wrote it and can rebuild it — and ruinous here: a torn
|
|
115
|
-
* read of `~/.claude.json` would be written back as the handful of keys Mate
|
|
116
|
-
* contributes, taking Claude Code's auth, every project's history and its MCP
|
|
117
|
-
* approvals with it, silently and without a backup. Throwing leaves the file
|
|
118
|
-
* as it was and hands the entry catch in `project()` a failed outcome to
|
|
119
|
-
* report, which is the answer an operator can act on.
|
|
119
|
+
* Unparseable throws, never reads as empty: writing back a torn
|
|
120
|
+
* `~/.claude.json` would wipe the user's auth and history.
|
|
120
121
|
*/
|
|
121
122
|
function parseDocument(raw: string | null, target: string): Record<string, unknown> {
|
|
122
123
|
if (raw === null) return {};
|
|
@@ -130,40 +131,6 @@ function parseDocument(raw: string | null, target: string): Record<string, unkno
|
|
|
130
131
|
return parsed;
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
/**
|
|
134
|
-
* Writes via a sibling temp file + `fs.rename()` so an interrupted write cannot
|
|
135
|
-
* leave a half-written document behind. A reader racing this one — a Claude
|
|
136
|
-
* Code session holding `~/.claude.json` open — observes the old file or the new
|
|
137
|
-
* one, never a truncated one.
|
|
138
|
-
*/
|
|
139
|
-
async function writeAtomic(target: string, contents: string): Promise<void> {
|
|
140
|
-
const tempPath = `${target}.${crypto.randomUUID()}.tmp`;
|
|
141
|
-
try {
|
|
142
|
-
await fs.writeFile(tempPath, contents, "utf8");
|
|
143
|
-
await fs.rename(tempPath, target);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
await fs.rm(tempPath, { force: true }).catch(() => {});
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function readManifest(repoPath: string): Promise<Manifest> {
|
|
151
|
-
const parsed = parseObject(await readRaw(manifestPath(repoPath)));
|
|
152
|
-
return {
|
|
153
|
-
documents: isRecord(parsed.documents) ? (parsed.documents as Manifest["documents"]) : {},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function writeManifest(repoPath: string, manifest: Manifest): Promise<void> {
|
|
158
|
-
const target = manifestPath(repoPath);
|
|
159
|
-
if (Object.keys(manifest.documents).length === 0) {
|
|
160
|
-
await fs.unlink(target).catch(() => {});
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
164
|
-
await writeAtomic(target, serialize(manifest));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
134
|
/** The container holding a region's final key, created on demand when writing. */
|
|
168
135
|
function containerFor(
|
|
169
136
|
document: Record<string, unknown>,
|
|
@@ -185,6 +152,11 @@ function containerFor(
|
|
|
185
152
|
return node;
|
|
186
153
|
}
|
|
187
154
|
|
|
155
|
+
function keyPresent(document: Record<string, unknown>, at: string[]): boolean {
|
|
156
|
+
const container = containerFor(document, at, false);
|
|
157
|
+
return container !== null && Object.hasOwn(container, at.at(-1)!);
|
|
158
|
+
}
|
|
159
|
+
|
|
188
160
|
function applyRegion(document: Record<string, unknown>, region: ManagedRegion): void {
|
|
189
161
|
const container = containerFor(document, region.at, true)!;
|
|
190
162
|
const key = region.at.at(-1)!;
|
|
@@ -295,19 +267,22 @@ export async function placeRuntimeDocument(
|
|
|
295
267
|
const raw = await readRaw(target);
|
|
296
268
|
const document = parseDocument(raw, target);
|
|
297
269
|
|
|
298
|
-
const
|
|
299
|
-
for (const region of
|
|
300
|
-
|
|
270
|
+
const prior = documentRegions(await readManifest(repoPath), documentPath);
|
|
271
|
+
for (const region of prior ?? []) revertRegion(document, region);
|
|
272
|
+
/** After the revert, a key still present is the human's: a yielding region gives way to it. */
|
|
273
|
+
const applied = rendered.regions.filter(
|
|
274
|
+
(region) => !(region.kind === "value" && region.yields && keyPresent(document, region.at)),
|
|
275
|
+
);
|
|
276
|
+
for (const region of applied) applyRegion(document, region);
|
|
301
277
|
const next = serialize(document);
|
|
302
278
|
|
|
303
|
-
const
|
|
304
|
-
const recorded = JSON.stringify(manifest.documents[documentPath]) === JSON.stringify(regions);
|
|
279
|
+
const recorded = JSON.stringify(prior) === JSON.stringify(applied);
|
|
305
280
|
if (next === raw && recorded) return "current";
|
|
306
281
|
|
|
282
|
+
/** The file first: a failed write leaves the manifest naming what is really there. */
|
|
307
283
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
308
284
|
await writeAtomic(target, next);
|
|
309
|
-
manifest
|
|
310
|
-
await writeManifest(repoPath, manifest);
|
|
285
|
+
await updateManifest(repoPath, (manifest) => withDocument(manifest, documentPath, applied));
|
|
311
286
|
return "written";
|
|
312
287
|
}
|
|
313
288
|
|
|
@@ -319,13 +294,11 @@ export async function removeRuntimeDocument(
|
|
|
319
294
|
repoPath: string,
|
|
320
295
|
documentPath: string,
|
|
321
296
|
): Promise<"removed" | "absent"> {
|
|
322
|
-
const
|
|
323
|
-
const regions = manifest.documents[documentPath];
|
|
297
|
+
const regions = documentRegions(await readManifest(repoPath), documentPath);
|
|
324
298
|
if (!regions) return "absent";
|
|
325
299
|
|
|
326
300
|
await revertDocumentOnDisk(repoPath, documentPath, regions);
|
|
327
|
-
|
|
328
|
-
await writeManifest(repoPath, manifest);
|
|
301
|
+
await updateManifest(repoPath, (manifest) => withoutDocuments(manifest, [documentPath]));
|
|
329
302
|
return "removed";
|
|
330
303
|
}
|
|
331
304
|
|
|
@@ -358,10 +331,10 @@ async function revertDocumentOnDisk(
|
|
|
358
331
|
}
|
|
359
332
|
|
|
360
333
|
/**
|
|
361
|
-
* Withdraws several destinations against a single
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
334
|
+
* Withdraws several destinations against a single manifest update.
|
|
335
|
+
* `removeRuntimeDocument` per destination cannot be run concurrently — each
|
|
336
|
+
* rewrites the whole manifest, so the last write restores the keys the others
|
|
337
|
+
* removed — and running it sequentially rewrites the manifest once per
|
|
365
338
|
* document. Reverting the files together and recording the outcome once is both
|
|
366
339
|
* safe and one write.
|
|
367
340
|
*
|
|
@@ -375,13 +348,13 @@ export async function removeRuntimeDocuments(
|
|
|
375
348
|
documentPaths: readonly string[],
|
|
376
349
|
): Promise<{ removed: string[]; absent: string[]; error?: { document: string; error: Error } }> {
|
|
377
350
|
const manifest = await readManifest(repoPath);
|
|
378
|
-
const recorded = documentPaths.filter((candidate) => manifest
|
|
379
|
-
const absent = documentPaths.filter((candidate) => manifest
|
|
351
|
+
const recorded = documentPaths.filter((candidate) => documentRegions(manifest, candidate));
|
|
352
|
+
const absent = documentPaths.filter((candidate) => !documentRegions(manifest, candidate));
|
|
380
353
|
if (recorded.length === 0) return { removed: [], absent };
|
|
381
354
|
|
|
382
355
|
const settled = await Promise.allSettled(
|
|
383
356
|
recorded.map((documentPath) =>
|
|
384
|
-
revertDocumentOnDisk(repoPath, documentPath, manifest
|
|
357
|
+
revertDocumentOnDisk(repoPath, documentPath, documentRegions(manifest, documentPath)!),
|
|
385
358
|
),
|
|
386
359
|
);
|
|
387
360
|
|
|
@@ -396,29 +369,9 @@ export async function removeRuntimeDocuments(
|
|
|
396
369
|
};
|
|
397
370
|
continue;
|
|
398
371
|
}
|
|
399
|
-
delete manifest.documents[documentPath];
|
|
400
372
|
removed.push(documentPath);
|
|
401
373
|
}
|
|
402
374
|
|
|
403
|
-
await
|
|
375
|
+
await updateManifest(repoPath, (current) => withoutDocuments(current, removed));
|
|
404
376
|
return { removed, absent, ...(failure ? { error: failure } : {}) };
|
|
405
377
|
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Every destination the manifest records, in the order it recorded them. This
|
|
409
|
-
* is what makes a Working Repository "wrapped": the manifest is written only by
|
|
410
|
-
* a pass that placed a runtime document, and `mate wrap` is the only pass that
|
|
411
|
-
* places one. Reported as data so neither the launch refusal nor `mate unwrap`
|
|
412
|
-
* holds a list of destinations that the entry catalogue could outgrow.
|
|
413
|
-
*/
|
|
414
|
-
export async function recordedRuntimeDocuments(repoPath: string): Promise<string[]> {
|
|
415
|
-
return Object.keys((await readManifest(repoPath)).documents);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Present when Mate recorded regions for it, not merely when the file exists. */
|
|
419
|
-
export async function runtimeDocumentPresent(
|
|
420
|
-
repoPath: string,
|
|
421
|
-
documentPath: string,
|
|
422
|
-
): Promise<boolean> {
|
|
423
|
-
return (await readManifest(repoPath)).documents[documentPath] !== undefined;
|
|
424
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GlobalConfigStore } from "./global-config-store";
|
|
2
2
|
import type { ProjectionFile } from "../../runtime/projection";
|
|
3
|
+
import type { RenderedRuntimeDocument, WrapRuntime } from "../../runtime/runtime-document-manifest";
|
|
3
4
|
import type { CompanionSource, FrameworkConfig, LinkedRepository } from "./types";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -24,6 +25,8 @@ export type ProjectionEntryId =
|
|
|
24
25
|
| "repo-local-framework"
|
|
25
26
|
| "repo-local-registry"
|
|
26
27
|
| "projection-pair"
|
|
28
|
+
| "openspec-store-exclude"
|
|
29
|
+
| "openspec-store-pointer"
|
|
27
30
|
| "claude-skill-links"
|
|
28
31
|
| "workspace-document"
|
|
29
32
|
| "capability-excludes"
|
|
@@ -32,38 +35,8 @@ export type ProjectionEntryId =
|
|
|
32
35
|
| "claude-runtime-document"
|
|
33
36
|
| "claude-local-mcp-document"
|
|
34
37
|
| "mcp-runtime-document"
|
|
35
|
-
| "opencode-runtime-document"
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A region of a runtime document that Mate maintains. JSON carries no comment
|
|
39
|
-
* syntax, so the region is recorded rather than marked in the file: `list`
|
|
40
|
-
* appends values not already there, `map` sets entries by key, `value` sets one
|
|
41
|
-
* key — and removal takes back exactly those, leaving anything a human added.
|
|
42
|
-
*
|
|
43
|
-
* `stripPrefix` is the one exception to "removal takes back exactly what was
|
|
44
|
-
* written": it deletes keys Mate never wrote, and removal does not restore
|
|
45
|
-
* them. It exists for keys an older Mate wrote and a current one must not
|
|
46
|
-
* leave behind, so a re-wrap heals a repository an old version configured.
|
|
47
|
-
*/
|
|
48
|
-
export type ManagedRegion =
|
|
49
|
-
| { readonly at: string[]; readonly kind: "list"; readonly values: unknown[] }
|
|
50
|
-
| {
|
|
51
|
-
readonly at: string[];
|
|
52
|
-
readonly kind: "map";
|
|
53
|
-
readonly entries: Record<string, unknown>;
|
|
54
|
-
readonly stripPrefix?: string;
|
|
55
|
-
}
|
|
56
|
-
| { readonly at: string[]; readonly kind: "value"; readonly value: unknown };
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* What a Runtime Surface hands the Projection Root: the managed content of one
|
|
60
|
-
* document, without a destination. The surface renders; the entry places.
|
|
61
|
-
*/
|
|
62
|
-
export interface RenderedRuntimeDocument {
|
|
63
|
-
/** Repo-relative with POSIX separators, or `~/`-prefixed for a user-global target. */
|
|
64
|
-
readonly path: string;
|
|
65
|
-
readonly regions: readonly ManagedRegion[];
|
|
66
|
-
}
|
|
38
|
+
| "opencode-runtime-document"
|
|
39
|
+
| "opencode-companion-copies";
|
|
67
40
|
|
|
68
41
|
export interface ProjectionInput {
|
|
69
42
|
repoPath: string;
|
|
@@ -75,6 +48,10 @@ export interface ProjectionInput {
|
|
|
75
48
|
globalConfigStore?: GlobalConfigStore;
|
|
76
49
|
/** Rendered by the active Runtime Surfaces; placed by the `wrap` scope's entries. */
|
|
77
50
|
runtimeDocuments?: readonly RenderedRuntimeDocument[];
|
|
51
|
+
/** Restricts runtime-tagged entries to these runtimes; every runtime when absent. */
|
|
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;
|
|
78
55
|
}
|
|
79
56
|
|
|
80
57
|
export interface ProjectionRemovalInput {
|
|
@@ -86,6 +63,23 @@ export interface ProjectionRemovalInput {
|
|
|
86
63
|
export type ProjectionWriteState = "written" | "current" | "skipped";
|
|
87
64
|
export type ProjectionRemoveState = "removed" | "absent" | "retained" | "covered";
|
|
88
65
|
|
|
66
|
+
/** Repo-relative paths an entry maintaining several files wrote or withdrew. */
|
|
67
|
+
export interface ProjectionChangedPaths {
|
|
68
|
+
written: string[];
|
|
69
|
+
removed: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What a write may return instead of a bare state, when it has something to
|
|
74
|
+
* tell the operator. The entry reports; it never prints — the caller presents
|
|
75
|
+
* warnings and changed paths without knowing which entry produced them.
|
|
76
|
+
*/
|
|
77
|
+
export interface ProjectionWriteReport {
|
|
78
|
+
state: ProjectionWriteState;
|
|
79
|
+
warnings?: string[];
|
|
80
|
+
changed?: ProjectionChangedPaths;
|
|
81
|
+
}
|
|
82
|
+
|
|
89
83
|
/**
|
|
90
84
|
* Every entry names its inverse. `self` removes its own path, `entry` is covered
|
|
91
85
|
* by another entry's removal, `retained` is deliberately left behind and says
|
|
@@ -95,6 +89,8 @@ export type ProjectionEntryRemoval =
|
|
|
95
89
|
| {
|
|
96
90
|
readonly by: "self";
|
|
97
91
|
remove(input: ProjectionRemovalInput): Promise<"removed" | "absent">;
|
|
92
|
+
/** What removing this path deliberately leaves outside the repository, named in the outcome. */
|
|
93
|
+
readonly retains?: string;
|
|
98
94
|
}
|
|
99
95
|
| { readonly by: "entry"; readonly entry: ProjectionEntryId }
|
|
100
96
|
| { readonly by: "retained"; readonly because: string };
|
|
@@ -105,8 +101,10 @@ export interface ProjectionEntry {
|
|
|
105
101
|
/** Repo-relative, POSIX separators. An entry spanning siblings names its primary. */
|
|
106
102
|
readonly path: string;
|
|
107
103
|
readonly scopes: readonly ProjectionScope[];
|
|
104
|
+
/** Set on entries that belong to one Agent Runtime; see {@link ProjectionInput.runtimes}. */
|
|
105
|
+
readonly runtime?: WrapRuntime;
|
|
108
106
|
/** `skipped` when an input this entry needs was not supplied. */
|
|
109
|
-
write(input: ProjectionInput): Promise<ProjectionWriteState>;
|
|
107
|
+
write(input: ProjectionInput): Promise<ProjectionWriteState | ProjectionWriteReport>;
|
|
110
108
|
/**
|
|
111
109
|
* Whether a failed write is reported without failing the operation that
|
|
112
110
|
* triggered the projection. Only for an entry something else already covers:
|
|
@@ -132,6 +130,14 @@ export interface ProjectionEntryOutcome {
|
|
|
132
130
|
state: ProjectionWriteState | ProjectionRemoveState | "failed";
|
|
133
131
|
error?: Error;
|
|
134
132
|
degradable?: boolean;
|
|
133
|
+
/** The entry's runtime tag; absent for runtime-neutral entries. */
|
|
134
|
+
runtime?: WrapRuntime;
|
|
135
|
+
/** Present only when the entry had something to say. */
|
|
136
|
+
warnings?: string[];
|
|
137
|
+
/** Present only when the entry wrote or withdrew paths it reports one by one. */
|
|
138
|
+
changed?: ProjectionChangedPaths;
|
|
139
|
+
/** Present only on a removal that leaves something outside the repository behind. */
|
|
140
|
+
retains?: string;
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
export interface ProjectionResult {
|
|
@@ -159,7 +165,14 @@ export interface ProjectionDescription {
|
|
|
159
165
|
projection: ProjectionFile | null;
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
/**
|
|
168
|
+
/** Skips a degradable entry's failure: it never fails a pass, only shows in the outcomes. */
|
|
163
169
|
export function firstFailure(outcomes: ProjectionEntryOutcome[]): ProjectionEntryOutcome | null {
|
|
164
170
|
return outcomes.find((outcome) => outcome.state === "failed" && !outcome.degradable) ?? null;
|
|
165
171
|
}
|
|
172
|
+
|
|
173
|
+
/** Names the failed entry: "the projection" alone would not say what to fix. */
|
|
174
|
+
export function entryFailureMessage(outcome: ProjectionEntryOutcome, repositoryId: string): string {
|
|
175
|
+
const reason = outcome.error?.message ?? "projection failed";
|
|
176
|
+
const target = outcome.id === "projection-pair" ? "the projection" : outcome.path;
|
|
177
|
+
return `failed to write ${target} for ${repositoryId}: ${reason}`;
|
|
178
|
+
}
|