@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,22 +1,19 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
3
|
|
|
6
4
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
7
|
-
import { gitEnvironment } from "../../runtime/companion-git";
|
|
8
5
|
import { readProjectionFile } from "../../runtime/projection";
|
|
6
|
+
import { GlobalConfigStore } from "./global-config-store";
|
|
9
7
|
import { repoLocalDirPath } from "../../runtime/repo-local";
|
|
10
|
-
import type { GlobalConfigStore } from "./global-config-store";
|
|
11
8
|
import { projectionEntries } from "./projection-entries";
|
|
12
9
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "
|
|
10
|
+
copies as recordedCopies,
|
|
11
|
+
documents as recordedDocuments,
|
|
12
|
+
readManifest,
|
|
13
|
+
} from "../../runtime/runtime-document-manifest";
|
|
14
|
+
import { removeRuntimeDocuments } from "./projection-runtime-documents";
|
|
17
15
|
import {
|
|
18
16
|
firstFailure,
|
|
19
|
-
type RenderedRuntimeDocument,
|
|
20
17
|
type ProjectionDescription,
|
|
21
18
|
type ProjectionEntry,
|
|
22
19
|
type ProjectionEntryOutcome,
|
|
@@ -26,10 +23,10 @@ import {
|
|
|
26
23
|
type ProjectionRemovalResult,
|
|
27
24
|
type ProjectionResult,
|
|
28
25
|
type ProjectionScope,
|
|
26
|
+
type ProjectionWriteReport,
|
|
27
|
+
type ProjectionWriteState,
|
|
29
28
|
} from "./projection-types";
|
|
30
|
-
import type {
|
|
31
|
-
|
|
32
|
-
const execFileAsync = promisify(execFile);
|
|
29
|
+
import type { LinkedRepository } from "./types";
|
|
33
30
|
|
|
34
31
|
/**
|
|
35
32
|
* The owner of the Managed Projection: every path Mate places inside a Working
|
|
@@ -60,6 +57,22 @@ function outcome(
|
|
|
60
57
|
state,
|
|
61
58
|
...(error ? { error } : {}),
|
|
62
59
|
...(entry.degradable ? { degradable: true } : {}),
|
|
60
|
+
...(entry.runtime ? { runtime: entry.runtime } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One outcome shape for both forms a write may return; empty reports are omitted. */
|
|
65
|
+
function writeOutcome(
|
|
66
|
+
entry: ProjectionEntry,
|
|
67
|
+
written: ProjectionWriteState | ProjectionWriteReport,
|
|
68
|
+
): ProjectionEntryOutcome {
|
|
69
|
+
if (typeof written === "string") return outcome(entry, written);
|
|
70
|
+
const { state, warnings = [], changed } = written;
|
|
71
|
+
const anyChanged = changed && (changed.written.length > 0 || changed.removed.length > 0);
|
|
72
|
+
return {
|
|
73
|
+
...outcome(entry, state),
|
|
74
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
75
|
+
...(anyChanged ? { changed } : {}),
|
|
63
76
|
};
|
|
64
77
|
}
|
|
65
78
|
|
|
@@ -74,11 +87,13 @@ export async function project(
|
|
|
74
87
|
input: ProjectionInput,
|
|
75
88
|
): Promise<ProjectionResult> {
|
|
76
89
|
const outcomes: ProjectionEntryOutcome[] = [];
|
|
90
|
+
const runtimes = input.runtimes ? new Set(input.runtimes) : null;
|
|
77
91
|
for (const entry of projectionEntries()) {
|
|
78
92
|
if (!entry.scopes.includes(scope)) continue;
|
|
93
|
+
if (entry.runtime && runtimes && !runtimes.has(entry.runtime)) continue;
|
|
79
94
|
try {
|
|
80
95
|
// oxlint-disable-next-line no-await-in-loop -- catalogue order is the contract
|
|
81
|
-
outcomes.push(
|
|
96
|
+
outcomes.push(writeOutcome(entry, await entry.write(input))); // react-doctor-disable-line react-doctor/async-await-in-loop
|
|
82
97
|
} catch (error) {
|
|
83
98
|
outcomes.push(outcome(entry, "failed", toError(error)));
|
|
84
99
|
}
|
|
@@ -93,7 +108,7 @@ export async function project(
|
|
|
93
108
|
*/
|
|
94
109
|
export async function unproject(input: ProjectionRemovalInput): Promise<ProjectionRemovalResult> {
|
|
95
110
|
const outcomes: ProjectionEntryOutcome[] = [];
|
|
96
|
-
for (const entry of
|
|
111
|
+
for (const entry of projectionEntries().toReversed()) {
|
|
97
112
|
const { removal } = entry;
|
|
98
113
|
if (removal.by === "entry") {
|
|
99
114
|
outcomes.push(outcome(entry, "covered"));
|
|
@@ -105,7 +120,8 @@ export async function unproject(input: ProjectionRemovalInput): Promise<Projecti
|
|
|
105
120
|
}
|
|
106
121
|
try {
|
|
107
122
|
// oxlint-disable-next-line no-await-in-loop -- reverse catalogue order is the contract
|
|
108
|
-
|
|
123
|
+
const removed = outcome(entry, await removal.remove(input));
|
|
124
|
+
outcomes.push(removal.retains ? { ...removed, retains: removal.retains } : removed);
|
|
109
125
|
} catch (error) {
|
|
110
126
|
outcomes.push(outcome(entry, "failed", toError(error)));
|
|
111
127
|
}
|
|
@@ -113,39 +129,42 @@ export async function unproject(input: ProjectionRemovalInput): Promise<Projecti
|
|
|
113
129
|
return { root: repoLocalDirPath(input.repoPath), outcomes };
|
|
114
130
|
}
|
|
115
131
|
|
|
116
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Removes entries that wrap owns but an unwrap must withdraw independently.
|
|
134
|
+
* Sequential: several of them rewrite the manifest, and concurrent rewrites
|
|
135
|
+
* would restore each other's withdrawn records.
|
|
136
|
+
*/
|
|
117
137
|
async function removeWorkingRepositoryUnwrapEntries(
|
|
118
138
|
repoPath: string,
|
|
139
|
+
registeredCompanionPaths: string[],
|
|
119
140
|
): Promise<ProjectionEntryOutcome[]> {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}),
|
|
133
|
-
);
|
|
141
|
+
const outcomes: ProjectionEntryOutcome[] = [];
|
|
142
|
+
for (const entry of projectionEntries()) {
|
|
143
|
+
const removal = entry.removal;
|
|
144
|
+
if (!entry.removeOnUnwrap || removal.by !== "self") continue;
|
|
145
|
+
try {
|
|
146
|
+
// oxlint-disable-next-line no-await-in-loop -- each removal rewrites the manifest
|
|
147
|
+
outcomes.push(outcome(entry, await removal.remove({ repoPath, registeredCompanionPaths }))); // react-doctor-disable-line react-doctor/async-await-in-loop
|
|
148
|
+
} catch (error) {
|
|
149
|
+
outcomes.push(outcome(entry, "failed", toError(error)));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return outcomes;
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
/** Reads and reports; writes nothing. Freshness is a verdict above this module. */
|
|
137
156
|
export async function describe(repoPath: string): Promise<ProjectionDescription> {
|
|
138
|
-
const entries: ProjectionEntryPresence[] =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
const entries: ProjectionEntryPresence[] = await Promise.all(
|
|
158
|
+
projectionEntries().map(async (entry) => {
|
|
159
|
+
const present = await (entry.present
|
|
160
|
+
? entry.present(repoPath)
|
|
161
|
+
: fs
|
|
162
|
+
.access(path.join(path.resolve(repoPath), entry.path))
|
|
163
|
+
.then(() => true)
|
|
164
|
+
.catch(() => false));
|
|
165
|
+
return { id: entry.id, path: entry.path, kind: entry.kind, present };
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
149
168
|
return {
|
|
150
169
|
root: repoLocalDirPath(repoPath),
|
|
151
170
|
entries,
|
|
@@ -183,123 +202,12 @@ export async function projectWorkingRepository(
|
|
|
183
202
|
};
|
|
184
203
|
}
|
|
185
204
|
|
|
186
|
-
/**
|
|
187
|
-
* The rendered runtime documents, placed. Reported per document because the
|
|
188
|
-
* operator asked for these by name: a failure has to say which one failed while
|
|
189
|
-
* the projection already written stays where it is.
|
|
190
|
-
*/
|
|
191
|
-
export type RuntimeDocumentsResult =
|
|
192
|
-
| { kind: "written" | "current"; documents: string[] }
|
|
193
|
-
| { kind: "failed"; document: string; error: Error };
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Which of the given repo-relative paths Git already has in its index.
|
|
197
|
-
* Best-effort by construction: a directory that is no repository, a Git that is
|
|
198
|
-
* not installed, and a `ls-files` that fails for any other reason all answer
|
|
199
|
-
* "nothing tracked" — this feeds a warning, and a warning may not fail a wrap.
|
|
200
|
-
*/
|
|
201
|
-
async function trackedDocuments(repoPath: string, documentPaths: string[]): Promise<string[]> {
|
|
202
|
-
if (documentPaths.length === 0) return [];
|
|
203
|
-
try {
|
|
204
|
-
const { stdout } = await execFileAsync("git", ["ls-files", "-z", "--", ...documentPaths], {
|
|
205
|
-
cwd: path.resolve(repoPath),
|
|
206
|
-
encoding: "utf8",
|
|
207
|
-
env: gitEnvironment(),
|
|
208
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
209
|
-
});
|
|
210
|
-
return stdout.split("\0").filter(Boolean);
|
|
211
|
-
} catch {
|
|
212
|
-
return [];
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* The managed exclude block only hides a file Git does not already track, so a
|
|
218
|
-
* repository that commits one of these documents keeps it in the index and the
|
|
219
|
-
* regions land in a tracked file. Those regions carry machine-absolute
|
|
220
|
-
* companion paths — keys naming directories under this user's home — so a
|
|
221
|
-
* routine `git add -A` pushes one machine's layout to everyone else. Warn and
|
|
222
|
-
* write anyway: refusing would silently drop the projection the repository was
|
|
223
|
-
* wrapped for.
|
|
224
|
-
*
|
|
225
|
-
* Asked here rather than while rendering because the render also runs on every
|
|
226
|
-
* launch; the wrap pass is the one that is a deliberate act, so this is the
|
|
227
|
-
* scope where saying it once is information rather than noise.
|
|
228
|
-
*/
|
|
229
|
-
async function warnAboutTrackedDocuments(
|
|
230
|
-
repository: LinkedRepository,
|
|
231
|
-
runtimeDocuments: readonly RenderedRuntimeDocument[],
|
|
232
|
-
): Promise<void> {
|
|
233
|
-
const tracked = await trackedDocuments(
|
|
234
|
-
repository.path,
|
|
235
|
-
runtimeDocuments
|
|
236
|
-
.map((document) => document.path)
|
|
237
|
-
.filter((documentPath) => !isExternalDocument(documentPath)),
|
|
238
|
-
);
|
|
239
|
-
for (const documentPath of tracked) {
|
|
240
|
-
console.error(
|
|
241
|
-
`${FRAMEWORK_NAME}: warning: ${documentPath} is tracked by Git in ${repository.id}; ${FRAMEWORK_NAME} writes machine-absolute companion paths into it, so committing it would push this machine's paths to everyone else on the repository.`,
|
|
242
|
-
);
|
|
243
|
-
console.error(
|
|
244
|
-
`Untrack it with \`git rm --cached ${documentPath}\` if those paths should stay local.`,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export async function projectWorkingRuntimeDocuments(
|
|
250
|
-
companionPath: string,
|
|
251
|
-
repository: LinkedRepository,
|
|
252
|
-
config: FrameworkConfig,
|
|
253
|
-
runtimeDocuments: readonly RenderedRuntimeDocument[],
|
|
254
|
-
globalConfigStore?: GlobalConfigStore,
|
|
255
|
-
): Promise<RuntimeDocumentsResult> {
|
|
256
|
-
await warnAboutTrackedDocuments(repository, runtimeDocuments);
|
|
257
|
-
const result = await project("wrap", {
|
|
258
|
-
repoPath: repository.path,
|
|
259
|
-
companionPath: path.resolve(companionPath),
|
|
260
|
-
repository,
|
|
261
|
-
config,
|
|
262
|
-
runtimeDocuments,
|
|
263
|
-
globalConfigStore,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const failure = firstFailure(result.outcomes);
|
|
267
|
-
if (failure) {
|
|
268
|
-
return {
|
|
269
|
-
kind: "failed",
|
|
270
|
-
document: failure.path,
|
|
271
|
-
error: failure.error ?? new Error("runtime document write failed"),
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** The destinations the caller handed over; the rest of the pass is scaffolding. */
|
|
276
|
-
const destinations = new Set(
|
|
277
|
-
runtimeDocuments.map((document) => document.path.split("/").join(path.sep)),
|
|
278
|
-
);
|
|
279
|
-
const outcomes = result.outcomes.filter((outcome) => destinations.has(outcome.path));
|
|
280
|
-
return {
|
|
281
|
-
kind: outcomes.some((outcome) => outcome.state === "written") ? "written" : "current",
|
|
282
|
-
documents: [...new Set(outcomes.map((outcome) => outcome.path))],
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Whether `mate wrap` configured this Working Repository. Asked of the runtime
|
|
288
|
-
* document manifest rather than of the Projection Root, because the root is not
|
|
289
|
-
* evidence of a wrap: a launch and every Capability command write the
|
|
290
|
-
* projection pair too. The manifest is written only by a pass that placed a
|
|
291
|
-
* runtime document, and wrapping is the only pass that places one.
|
|
292
|
-
*/
|
|
293
|
-
export async function isWorkingRepositoryWrapped(repoPath: string): Promise<boolean> {
|
|
294
|
-
return (await recordedRuntimeDocuments(repoPath)).length > 0;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
205
|
export type UnwrapResult =
|
|
298
|
-
| { kind: "unwrapped" | "absent"; documents: string[] }
|
|
206
|
+
| { kind: "unwrapped" | "absent"; documents: string[]; copies: string[] }
|
|
299
207
|
| { kind: "failed"; document: string; error: Error };
|
|
300
208
|
|
|
301
209
|
/**
|
|
302
|
-
* The inverse of
|
|
210
|
+
* The inverse of a wrap's runtime documents: runtime documents and
|
|
303
211
|
* projected Claude skill links are withdrawn while the Projection Root, the
|
|
304
212
|
* Repository Link and the companion link are left exactly as they were. That
|
|
305
213
|
* asymmetry with `mate working cleanup` is the point — cleanup removes Mate's
|
|
@@ -311,10 +219,15 @@ export type UnwrapResult =
|
|
|
311
219
|
* Driven by the manifest, so it withdraws whatever the wrap that ran recorded,
|
|
312
220
|
* including a destination this release no longer renders.
|
|
313
221
|
*/
|
|
314
|
-
export async function unwrapWorkingRuntimeDocuments(
|
|
315
|
-
|
|
222
|
+
export async function unwrapWorkingRuntimeDocuments(
|
|
223
|
+
repoPath: string,
|
|
224
|
+
globalConfigStore: GlobalConfigStore = new GlobalConfigStore(),
|
|
225
|
+
): Promise<UnwrapResult> {
|
|
226
|
+
const manifest = await readManifest(repoPath);
|
|
227
|
+
const documents = recordedDocuments(manifest);
|
|
228
|
+
const copies = Object.keys(recordedCopies(manifest));
|
|
316
229
|
const removeWrapEntries = async (): Promise<ProjectionEntryOutcome[]> =>
|
|
317
|
-
removeWorkingRepositoryUnwrapEntries(repoPath);
|
|
230
|
+
removeWorkingRepositoryUnwrapEntries(repoPath, await globalConfigStore.list());
|
|
318
231
|
if (documents.length === 0) {
|
|
319
232
|
const outcomes = await removeWrapEntries();
|
|
320
233
|
const failure = firstFailure(outcomes);
|
|
@@ -326,8 +239,8 @@ export async function unwrapWorkingRuntimeDocuments(repoPath: string): Promise<U
|
|
|
326
239
|
};
|
|
327
240
|
}
|
|
328
241
|
return outcomes.some((entry) => entry.state === "removed")
|
|
329
|
-
? { kind: "unwrapped", documents: [] }
|
|
330
|
-
: { kind: "absent", documents: [] };
|
|
242
|
+
? { kind: "unwrapped", documents: [], copies }
|
|
243
|
+
: { kind: "absent", documents: [], copies: [] };
|
|
331
244
|
}
|
|
332
245
|
|
|
333
246
|
/**
|
|
@@ -349,7 +262,7 @@ export async function unwrapWorkingRuntimeDocuments(repoPath: string): Promise<U
|
|
|
349
262
|
error: failure.error ?? new Error("wrap entry removal failed"),
|
|
350
263
|
};
|
|
351
264
|
}
|
|
352
|
-
return { kind: "unwrapped", documents: removed };
|
|
265
|
+
return { kind: "unwrapped", documents: removed, copies };
|
|
353
266
|
}
|
|
354
267
|
|
|
355
268
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import { pathIsDirectory } from "
|
|
4
|
-
import { readCompanionRegistry } from "./
|
|
3
|
+
import { pathIsDirectory } from "../fs-utils";
|
|
4
|
+
import { readCompanionRegistry } from "./repository-link";
|
|
5
5
|
import { GlobalConfigStore } from "./global-config-store";
|
|
6
6
|
import type { LinkedRepository } from "./types";
|
|
7
7
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
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
|
+
import { readCompanionConfig } from "./config-store";
|
|
17
|
+
import { GlobalConfigStore } from "./global-config-store";
|
|
18
|
+
import { entryFailureMessage, firstFailure, type ProjectionChangedPaths } from "./projection-types";
|
|
19
|
+
import type { WrapRuntime } from "../../runtime/runtime-document-manifest";
|
|
20
|
+
import { readWrapState, type WrapState } from "../../runtime/wrap-state";
|
|
21
|
+
import type { NormalizedRegistration } from "../../tools/setup/registry";
|
|
22
|
+
import type { FrameworkConfig, LinkedRepository } from "./types";
|
|
23
|
+
import { project } from "./working-repo-projection";
|
|
24
|
+
|
|
25
|
+
export interface WrapPassDependencies {
|
|
26
|
+
renderWorkingRuntimeDocuments: typeof renderWorkingRuntimeDocuments;
|
|
27
|
+
readCompanionConfig: typeof readCompanionConfig;
|
|
28
|
+
project: typeof project;
|
|
29
|
+
loadAudienceRegistrations: typeof loadAudienceRegistrations;
|
|
30
|
+
renderAudienceLayer: typeof renderAudienceLayer;
|
|
31
|
+
/** The registered companions, whose stale skill links a re-pin withdraws; the user's registry when absent. */
|
|
32
|
+
globalConfigStore?: GlobalConfigStore;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const wrapPassDeps: WrapPassDependencies = {
|
|
36
|
+
renderWorkingRuntimeDocuments,
|
|
37
|
+
readCompanionConfig,
|
|
38
|
+
project,
|
|
39
|
+
loadAudienceRegistrations,
|
|
40
|
+
renderAudienceLayer,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface WrapPassOptions {
|
|
44
|
+
/** Reconcile only these runtimes; every active runtime when absent. */
|
|
45
|
+
runtimes?: readonly WrapRuntime[];
|
|
46
|
+
/** The companion config the caller already read; read from files otherwise. */
|
|
47
|
+
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
|
+
}
|
|
55
|
+
|
|
56
|
+
export type WrapPassResult =
|
|
57
|
+
| {
|
|
58
|
+
kind: "written" | "current";
|
|
59
|
+
projectionRoot: string;
|
|
60
|
+
companionPath: string;
|
|
61
|
+
/** The runtime documents this pass answered for, written or current. */
|
|
62
|
+
documents: string[];
|
|
63
|
+
/** The subset of `documents` this pass wrote. */
|
|
64
|
+
writtenDocuments: string[];
|
|
65
|
+
/** Every path an entry reported writing or withdrawing one by one. */
|
|
66
|
+
changed: ProjectionChangedPaths;
|
|
67
|
+
/** The render's warnings, then every entry's, for the caller to present. */
|
|
68
|
+
warnings: string[];
|
|
69
|
+
/** Active runtimes whose tagged entries this pass ran; never inferred from paths. */
|
|
70
|
+
runtimes: ReadonlySet<WrapRuntime>;
|
|
71
|
+
/** The wrap state as this pass left it. */
|
|
72
|
+
wrapState: WrapState;
|
|
73
|
+
/** The audience the checkout is wrapped for after this pass. */
|
|
74
|
+
audience: string | null;
|
|
75
|
+
}
|
|
76
|
+
| { kind: "failed"; message: string };
|
|
77
|
+
|
|
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
|
+
/**
|
|
89
|
+
* One pass of the `wrap` scope, restricted to the requested runtimes by each
|
|
90
|
+
* entry's runtime tag. Best-effort per entry like every projection: a failed
|
|
91
|
+
* entry fails the pass while whatever the other entries wrote stays in place.
|
|
92
|
+
*/
|
|
93
|
+
export async function runWrapPass(
|
|
94
|
+
companionPath: string,
|
|
95
|
+
repository: LinkedRepository,
|
|
96
|
+
options: WrapPassOptions = {},
|
|
97
|
+
deps: WrapPassDependencies = wrapPassDeps,
|
|
98
|
+
): Promise<WrapPassResult> {
|
|
99
|
+
const resolvedCompanionPath = path.resolve(companionPath);
|
|
100
|
+
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
|
+
const rendered = await deps.renderWorkingRuntimeDocuments(
|
|
140
|
+
resolvedCompanionPath,
|
|
141
|
+
config,
|
|
142
|
+
repository.path,
|
|
143
|
+
undefined,
|
|
144
|
+
wrapped,
|
|
145
|
+
);
|
|
146
|
+
const result = await deps.project("wrap", {
|
|
147
|
+
repoPath: repository.path,
|
|
148
|
+
companionPath: resolvedCompanionPath,
|
|
149
|
+
repository,
|
|
150
|
+
config,
|
|
151
|
+
runtimeDocuments: rendered.documents,
|
|
152
|
+
audienceLayerDir: audience ? audienceLayerDir(resolvedCompanionPath, audience) : null,
|
|
153
|
+
globalConfigStore: deps.globalConfigStore ?? new GlobalConfigStore(),
|
|
154
|
+
...(options.runtimes ? { runtimes: options.runtimes } : {}),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const failure = firstFailure(result.outcomes);
|
|
158
|
+
if (failure) return { kind: "failed", message: entryFailureMessage(failure, repository.id) };
|
|
159
|
+
|
|
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
|
+
/** Read once under the caller's lock, so it is exactly what the pass recorded. */
|
|
167
|
+
let wrapState: WrapState;
|
|
168
|
+
try {
|
|
169
|
+
wrapState = await readWrapState(repository.path);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
kind: "failed",
|
|
173
|
+
message: `could not read the runtime document manifest for ${repository.id}: ${(error as Error).message}`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Inactive runtimes' entries run too, to withdraw; that is not covering them. */
|
|
177
|
+
const active = new Set(config.allowedAgents ?? []);
|
|
178
|
+
const runtimes = new Set(
|
|
179
|
+
result.outcomes.flatMap((outcome) =>
|
|
180
|
+
outcome.runtime && outcome.state !== "skipped" && active.has(outcome.runtime)
|
|
181
|
+
? [outcome.runtime]
|
|
182
|
+
: [],
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
/** The destinations the surfaces rendered; the rest of the pass is scaffolding. */
|
|
187
|
+
const destinations = new Set(
|
|
188
|
+
rendered.documents.map((document) => document.path.split("/").join(path.sep)),
|
|
189
|
+
);
|
|
190
|
+
const placed = result.outcomes.filter((outcome) => destinations.has(outcome.path));
|
|
191
|
+
const documents = placed.map((outcome) => outcome.path);
|
|
192
|
+
const writtenDocuments = placed
|
|
193
|
+
.filter((outcome) => outcome.state === "written")
|
|
194
|
+
.map((outcome) => outcome.path);
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
kind:
|
|
198
|
+
audienceChanged || result.outcomes.some((outcome) => outcome.state === "written")
|
|
199
|
+
? "written"
|
|
200
|
+
: "current",
|
|
201
|
+
projectionRoot: result.root,
|
|
202
|
+
companionPath: resolvedCompanionPath,
|
|
203
|
+
documents: [...new Set(documents)],
|
|
204
|
+
writtenDocuments: [...new Set(writtenDocuments)],
|
|
205
|
+
changed: {
|
|
206
|
+
written: result.outcomes.flatMap((outcome) => outcome.changed?.written ?? []),
|
|
207
|
+
removed: result.outcomes.flatMap((outcome) => outcome.changed?.removed ?? []),
|
|
208
|
+
},
|
|
209
|
+
warnings: [
|
|
210
|
+
...audienceWarnings,
|
|
211
|
+
...rendered.warnings,
|
|
212
|
+
...result.outcomes.flatMap((outcome) => outcome.warnings ?? []),
|
|
213
|
+
/** A degradable failure does not fail the pass, but it is never silent. */
|
|
214
|
+
...result.outcomes
|
|
215
|
+
.filter((outcome) => outcome.state === "failed" && outcome.degradable)
|
|
216
|
+
.map((outcome) => entryFailureMessage(outcome, repository.id)),
|
|
217
|
+
],
|
|
218
|
+
runtimes,
|
|
219
|
+
wrapState,
|
|
220
|
+
audience,
|
|
221
|
+
};
|
|
222
|
+
}
|