@uniqbit/mate-core 0.15.5 → 0.16.0-canary.1

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.
Files changed (186) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +2 -2
  2. package/claude-plugin/hooks/hooks.json +3 -6
  3. package/claude-plugin/hooks/session-guidance.mjs +8 -0
  4. package/claude-plugin/hooks/ts-loader.mjs +19 -0
  5. package/package.json +7 -5
  6. package/src/cli/commands/artifact/artifact.ts +19 -3
  7. package/src/cli/commands/artifact/finish/command.ts +183 -57
  8. package/src/cli/commands/artifact/finish/engine.ts +80 -91
  9. package/src/cli/commands/artifact/finish/finisher.ts +26 -33
  10. package/src/cli/commands/artifact/finish/git.ts +34 -23
  11. package/src/cli/commands/artifact/finish/index.ts +9 -3
  12. package/src/cli/commands/artifact/finish/openspec.ts +225 -97
  13. package/src/cli/commands/artifact/pending/command.ts +175 -0
  14. package/src/cli/commands/artifact/pending/discovery.ts +249 -0
  15. package/src/cli/commands/artifact/pending/index.ts +17 -0
  16. package/src/cli/commands/cap/index-cmd.ts +9 -1
  17. package/src/cli/commands/cap/index.ts +2 -6
  18. package/src/cli/commands/cap/tokensave.ts +4 -4
  19. package/src/cli/commands/companion/companion.ts +5 -1
  20. package/src/cli/commands/companion/link.ts +2 -2
  21. package/src/cli/commands/companion/sync.ts +92 -0
  22. package/src/cli/commands/doctor.ts +0 -3
  23. package/src/cli/commands/launch/shared.ts +23 -5
  24. package/src/cli/commands/report/collector.ts +72 -78
  25. package/src/cli/commands/report/contract.ts +40 -1
  26. package/src/cli/commands/report/highlight.ts +27 -0
  27. package/src/cli/commands/report/index.ts +11 -18
  28. package/src/cli/commands/report/renderer.ts +199 -2
  29. package/src/cli/commands/report/types.ts +26 -1
  30. package/src/cli/commands/shared/companion-selection.ts +107 -10
  31. package/src/cli/commands/studio/areas.ts +68 -0
  32. package/src/cli/commands/studio/index.ts +69 -0
  33. package/src/cli/commands/studio/inventory.ts +55 -0
  34. package/src/cli/commands/studio/mate-inventory.ts +43 -0
  35. package/src/cli/commands/studio/openspec-cli.ts +198 -0
  36. package/src/cli/commands/studio/payload.ts +184 -0
  37. package/src/cli/commands/studio/routes.ts +2 -0
  38. package/src/cli/commands/studio/selection.ts +61 -0
  39. package/src/cli/commands/studio/server.ts +201 -0
  40. package/src/cli/commands/studio/snapshot.ts +63 -0
  41. package/src/cli/commands/studio/topology.ts +199 -0
  42. package/src/cli/commands/studio/views/client.ts +197 -0
  43. package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
  44. package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
  46. package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
  47. package/src/cli/commands/studio/views/document.tsx +256 -0
  48. package/src/cli/commands/studio/views/error.tsx +20 -0
  49. package/src/cli/commands/studio/views/model.ts +34 -0
  50. package/src/cli/commands/studio/views/pairings.tsx +38 -0
  51. package/src/cli/commands/studio/views/skills/index.tsx +87 -0
  52. package/src/cli/commands/studio/views/specs/index.tsx +101 -0
  53. package/src/cli/commands/studio/views/styles.ts +389 -0
  54. package/src/cli/commands/studio/views/warnings.tsx +21 -0
  55. package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
  56. package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
  57. package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
  58. package/src/cli/commands/unwrap.ts +70 -0
  59. package/src/cli/commands/wrap.ts +164 -0
  60. package/src/cli/main.ts +68 -19
  61. package/src/cli/parse-flags.ts +36 -11
  62. package/src/cli/usage.ts +12 -3
  63. package/src/framework.ts +1 -7
  64. package/src/hooks/session-banner.ts +64 -11
  65. package/src/hooks/session-guidance.ts +40 -0
  66. package/src/hooks/validate-artifact-path.ts +108 -35
  67. package/src/lib/fs-utils.ts +9 -0
  68. package/src/lib/install.ts +33 -0
  69. package/src/lib/orchestrator/adapters/base.ts +14 -125
  70. package/src/lib/orchestrator/adapters/claude.ts +0 -11
  71. package/src/lib/orchestrator/adapters/opencode.ts +2 -32
  72. package/src/lib/orchestrator/companion-git-sync.ts +94 -84
  73. package/src/lib/orchestrator/config-store.ts +2 -21
  74. package/src/lib/orchestrator/editor.ts +12 -22
  75. package/src/lib/orchestrator/framework-context.ts +17 -6
  76. package/src/lib/orchestrator/global-config-store.ts +1 -1
  77. package/src/lib/orchestrator/launcher.ts +97 -7
  78. package/src/lib/orchestrator/opencode-guidance.ts +4 -56
  79. package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
  80. package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
  81. package/src/lib/orchestrator/projection-companion-link.ts +62 -0
  82. package/src/lib/orchestrator/projection-entries.ts +377 -0
  83. package/src/lib/orchestrator/projection-record.ts +56 -0
  84. package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
  85. package/src/lib/orchestrator/projection-types.ts +169 -0
  86. package/src/lib/orchestrator/repo-local-registry.ts +37 -133
  87. package/src/lib/orchestrator/repo-local-store.ts +96 -0
  88. package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
  89. package/src/lib/orchestrator/types.ts +1 -0
  90. package/src/lib/orchestrator/working-repo-projection.ts +366 -0
  91. package/src/lib/orchestrator/workspace-inventory.ts +1 -1
  92. package/src/lib/package-paths.ts +11 -1
  93. package/src/lib/public-npm.ts +2 -1
  94. package/src/lib/update-checker.ts +15 -9
  95. package/src/opencode/companion-hooks.ts +89 -245
  96. package/src/opencode/companion-policy.ts +35 -10
  97. package/src/opencode/index.ts +1 -0
  98. package/src/opencode/projected-guidance.ts +56 -0
  99. package/src/opencode/tui.tsx +13 -4
  100. package/src/playbooks/companion-guidance.ts +32 -116
  101. package/src/plugins.ts +0 -1
  102. package/src/runtime/companion-git-state.ts +156 -0
  103. package/src/runtime/companion-git.ts +203 -0
  104. package/src/runtime/companion-guidance.ts +222 -0
  105. package/src/runtime/companion-sync.ts +298 -0
  106. package/src/runtime/env-names.ts +30 -0
  107. package/src/runtime/env.ts +67 -35
  108. package/src/runtime/framework.ts +10 -0
  109. package/src/runtime/freshness.ts +58 -0
  110. package/src/runtime/index.ts +104 -0
  111. package/src/runtime/install.ts +30 -0
  112. package/src/runtime/policy.ts +66 -0
  113. package/src/runtime/projected-guidance.ts +45 -0
  114. package/src/runtime/projection.ts +224 -0
  115. package/src/runtime/repo-local.ts +64 -0
  116. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
  117. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
  118. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
  119. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
  120. package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
  121. package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
  122. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  123. package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
  124. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
  125. package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
  126. package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
  127. package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
  128. package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
  129. package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  130. package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  131. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
  132. package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
  133. package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
  134. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +156 -0
  135. package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
  136. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
  137. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
  138. package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
  139. package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
  140. package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
  141. package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  142. package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  143. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
  144. package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
  145. package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
  146. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +156 -0
  147. package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
  148. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
  149. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
  150. package/src/templates/report-assets/README.md +32 -0
  151. package/src/templates/report-assets/mermaid.LICENSE +21 -0
  152. package/src/templates/report-assets/mermaid.min.js +4376 -0
  153. package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
  154. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
  155. package/src/tools/setup/capabilities/graphify.ts +16 -7
  156. package/src/tools/setup/capabilities/openspec.ts +63 -56
  157. package/src/tools/setup/capabilities/tokensave.ts +116 -2
  158. package/src/tools/setup/engine.ts +34 -6
  159. package/src/tools/setup/mate.ts +42 -13
  160. package/src/tools/setup/plugin.ts +9 -0
  161. package/src/tools/setup/plugins/guidance.ts +11 -1
  162. package/src/tools/setup/providers/claude-format.ts +49 -4
  163. package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
  164. package/src/tools/setup/providers/claude.ts +55 -220
  165. package/src/tools/setup/providers/opencode.ts +41 -14
  166. package/src/tools/setup/runtime-documents.ts +174 -0
  167. package/src/tools/setup/surface-target.ts +50 -0
  168. package/src/tools/setup/working-repo-cleanup.ts +33 -26
  169. package/src/tools/setup/working-repo-local-state.ts +21 -1
  170. package/src/tools/setup.ts +25 -3
  171. package/wrappers/bin/graphify +57 -8
  172. package/wrappers/bin/openspec +50 -3
  173. package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
  174. package/src/cli/commands/cap/headroom.ts +0 -52
  175. package/src/cli/commands/workspace/list.ts +0 -25
  176. package/src/cli/commands/workspace/materialize.ts +0 -46
  177. package/src/cli/commands/workspace/workspace.ts +0 -22
  178. package/src/hooks/artifact-finish-nudge.ts +0 -244
  179. package/src/lib/orchestrator/headroom/proxy.ts +0 -116
  180. package/src/lib/orchestrator/workspace-materialize.ts +0 -80
  181. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
  182. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
  183. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
  184. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
  185. package/src/tools/setup/capabilities/headroom.ts +0 -57
  186. /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
@@ -0,0 +1,366 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { FRAMEWORK_NAME } from "../../framework";
5
+ import { readProjectionFile } from "../../runtime/projection";
6
+ import { repoLocalDirPath } from "../../runtime/repo-local";
7
+ import { companionGitSyncDeps } from "./companion-git-sync";
8
+ import type { GlobalConfigStore } from "./global-config-store";
9
+ import { projectionEntries } from "./projection-entries";
10
+ import {
11
+ isExternalDocument,
12
+ recordedRuntimeDocuments,
13
+ removeRuntimeDocuments,
14
+ } from "./projection-runtime-documents";
15
+ import {
16
+ firstFailure,
17
+ type RenderedRuntimeDocument,
18
+ type ProjectionDescription,
19
+ type ProjectionEntry,
20
+ type ProjectionEntryOutcome,
21
+ type ProjectionEntryPresence,
22
+ type ProjectionInput,
23
+ type ProjectionRemovalInput,
24
+ type ProjectionRemovalResult,
25
+ type ProjectionResult,
26
+ type ProjectionScope,
27
+ } from "./projection-types";
28
+ import type { FrameworkConfig, LinkedRepository } from "./types";
29
+
30
+ /**
31
+ * The owner of the Managed Projection: every path Mate places inside a Working
32
+ * Repository is written and removed from here, through the declarations in
33
+ * `projection-entries.ts`. This module holds no Agent Runtime format knowledge —
34
+ * that is what keeps a second runtime writing the same destination a new
35
+ * declaration rather than a branch in here.
36
+ */
37
+
38
+ export type ProjectionWriteResult =
39
+ | { kind: "written"; projectionRoot: string; companionPath: string }
40
+ | { kind: "current"; projectionRoot: string; companionPath: string }
41
+ | { kind: "failed"; error: Error };
42
+
43
+ function toError(error: unknown): Error {
44
+ return error instanceof Error ? error : new Error(String(error));
45
+ }
46
+
47
+ function outcome(
48
+ entry: ProjectionEntry,
49
+ state: ProjectionEntryOutcome["state"],
50
+ error?: Error,
51
+ ): ProjectionEntryOutcome {
52
+ return {
53
+ id: entry.id,
54
+ path: entry.path,
55
+ kind: entry.kind,
56
+ state,
57
+ ...(error ? { error } : {}),
58
+ ...(entry.degradable ? { degradable: true } : {}),
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Writes the entries the scope declares, in catalogue order so the managed
64
+ * exclude block precedes anything it covers. Best-effort per entry: one failure
65
+ * neither aborts the rest nor throws, so a caller that must fail reads the
66
+ * reported outcomes instead.
67
+ */
68
+ export async function project(
69
+ scope: ProjectionScope,
70
+ input: ProjectionInput,
71
+ ): Promise<ProjectionResult> {
72
+ const outcomes: ProjectionEntryOutcome[] = [];
73
+ for (const entry of projectionEntries()) {
74
+ if (!entry.scopes.includes(scope)) continue;
75
+ try {
76
+ // oxlint-disable-next-line no-await-in-loop -- catalogue order is the contract
77
+ outcomes.push(outcome(entry, await entry.write(input)));
78
+ } catch (error) {
79
+ outcomes.push(outcome(entry, "failed", toError(error)));
80
+ }
81
+ }
82
+ return { root: repoLocalDirPath(input.repoPath), scope, outcomes };
83
+ }
84
+
85
+ /**
86
+ * Removes every declared entry regardless of which scope wrote it, walking the
87
+ * catalogue in reverse. An entry covered by another entry's removal, or
88
+ * deliberately retained, is reported as such rather than being silently absent.
89
+ */
90
+ export async function unproject(input: ProjectionRemovalInput): Promise<ProjectionRemovalResult> {
91
+ const outcomes: ProjectionEntryOutcome[] = [];
92
+ for (const entry of [...projectionEntries()].reverse()) {
93
+ const { removal } = entry;
94
+ if (removal.by === "entry") {
95
+ outcomes.push(outcome(entry, "covered"));
96
+ continue;
97
+ }
98
+ if (removal.by === "retained") {
99
+ outcomes.push(outcome(entry, "retained"));
100
+ continue;
101
+ }
102
+ try {
103
+ // oxlint-disable-next-line no-await-in-loop -- reverse catalogue order is the contract
104
+ outcomes.push(outcome(entry, await removal.remove(input)));
105
+ } catch (error) {
106
+ outcomes.push(outcome(entry, "failed", toError(error)));
107
+ }
108
+ }
109
+ return { root: repoLocalDirPath(input.repoPath), outcomes };
110
+ }
111
+
112
+ /** Removes entries that wrap owns but an unwrap must withdraw independently. */
113
+ export async function removeWorkingRepositoryUnwrapEntries(
114
+ repoPath: string,
115
+ ): Promise<ProjectionEntryOutcome[]> {
116
+ const entries = projectionEntries().filter(
117
+ (entry) => entry.removeOnUnwrap && entry.removal.by === "self",
118
+ );
119
+ return Promise.all(
120
+ entries.map(async (entry) => {
121
+ const removal = entry.removal;
122
+ if (removal.by !== "self") return outcome(entry, "failed", new Error("invalid removal"));
123
+ try {
124
+ return outcome(entry, await removal.remove({ repoPath }));
125
+ } catch (error) {
126
+ return outcome(entry, "failed", toError(error));
127
+ }
128
+ }),
129
+ );
130
+ }
131
+
132
+ /** Reads and reports; writes nothing. Freshness is a verdict above this module. */
133
+ export async function describe(repoPath: string): Promise<ProjectionDescription> {
134
+ const entries: ProjectionEntryPresence[] = [];
135
+ for (const entry of projectionEntries()) {
136
+ const present = entry.present
137
+ ? // oxlint-disable-next-line no-await-in-loop -- reported in catalogue order
138
+ await entry.present(repoPath)
139
+ : await fs
140
+ .access(path.join(path.resolve(repoPath), entry.path))
141
+ .then(() => true)
142
+ .catch(() => false);
143
+ entries.push({ id: entry.id, path: entry.path, kind: entry.kind, present });
144
+ }
145
+ return {
146
+ root: repoLocalDirPath(repoPath),
147
+ entries,
148
+ projection: readProjectionFile(repoPath),
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Makes the Managed Projection durable at the Working Repository's Projection
154
+ * Root so an Unmanaged Session can resolve it from files. Reports its outcome
155
+ * rather than swallowing it: callers that asked for the write explicitly need
156
+ * to fail on it, while resolution paths stay best-effort through the wrapper
157
+ * below. Callers must have established that the companion is not a guess.
158
+ */
159
+ export async function projectWorkingRepository(
160
+ companionPath: string,
161
+ repository: LinkedRepository,
162
+ ): Promise<ProjectionWriteResult> {
163
+ const resolvedCompanionPath = path.resolve(companionPath);
164
+ const result = await project("session", {
165
+ repoPath: repository.path,
166
+ companionPath: resolvedCompanionPath,
167
+ repository,
168
+ });
169
+
170
+ const failure = firstFailure(result.outcomes);
171
+ if (failure) return { kind: "failed", error: failure.error ?? new Error("projection failed") };
172
+
173
+ /** The projection pair is what "the projection" means to these callers. */
174
+ const pair = result.outcomes.find((candidate) => candidate.id === "projection-pair");
175
+ return {
176
+ kind: pair?.state === "written" ? "written" : "current",
177
+ projectionRoot: result.root,
178
+ companionPath: resolvedCompanionPath,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * The rendered runtime documents, placed. Reported per document because the
184
+ * operator asked for these by name: a failure has to say which one failed while
185
+ * the projection already written stays where it is.
186
+ */
187
+ export type RuntimeDocumentsResult =
188
+ | { kind: "written" | "current"; documents: string[] }
189
+ | { kind: "failed"; document: string; error: Error };
190
+
191
+ /**
192
+ * Which of the given repo-relative paths Git already has in its index. Asked
193
+ * through the same runner the companion sync uses rather than a second spawn.
194
+ * Best-effort by construction: a directory that is no repository, a Git that is
195
+ * not installed, and a `ls-files` that fails for any other reason all answer
196
+ * "nothing tracked" — this feeds a warning, and a warning may not fail a wrap.
197
+ */
198
+ async function trackedDocuments(repoPath: string, documentPaths: string[]): Promise<string[]> {
199
+ if (documentPaths.length === 0) return [];
200
+ try {
201
+ const result = await companionGitSyncDeps.runGit(
202
+ ["ls-files", "-z", "--", ...documentPaths],
203
+ path.resolve(repoPath),
204
+ );
205
+ if (result.status !== 0) return [];
206
+ return result.stdout.split("\0").filter(Boolean);
207
+ } catch {
208
+ return [];
209
+ }
210
+ }
211
+
212
+ /**
213
+ * The managed exclude block only hides a file Git does not already track, so a
214
+ * repository that commits one of these documents keeps it in the index and the
215
+ * regions land in a tracked file. Those regions carry machine-absolute
216
+ * companion paths — keys naming directories under this user's home — so a
217
+ * routine `git add -A` pushes one machine's layout to everyone else. Warn and
218
+ * write anyway: refusing would silently drop the projection the repository was
219
+ * wrapped for.
220
+ *
221
+ * Asked here rather than while rendering because the render also runs on every
222
+ * launch; the wrap pass is the one that is a deliberate act, so this is the
223
+ * scope where saying it once is information rather than noise.
224
+ */
225
+ async function warnAboutTrackedDocuments(
226
+ repository: LinkedRepository,
227
+ runtimeDocuments: readonly RenderedRuntimeDocument[],
228
+ ): Promise<void> {
229
+ const tracked = await trackedDocuments(
230
+ repository.path,
231
+ runtimeDocuments
232
+ .map((document) => document.path)
233
+ .filter((documentPath) => !isExternalDocument(documentPath)),
234
+ );
235
+ for (const documentPath of tracked) {
236
+ console.error(
237
+ `${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.`,
238
+ );
239
+ console.error(
240
+ `Untrack it with \`git rm --cached ${documentPath}\` if those paths should stay local.`,
241
+ );
242
+ }
243
+ }
244
+
245
+ export async function projectWorkingRuntimeDocuments(
246
+ companionPath: string,
247
+ repository: LinkedRepository,
248
+ config: FrameworkConfig,
249
+ runtimeDocuments: readonly RenderedRuntimeDocument[],
250
+ globalConfigStore?: GlobalConfigStore,
251
+ ): Promise<RuntimeDocumentsResult> {
252
+ await warnAboutTrackedDocuments(repository, runtimeDocuments);
253
+ const result = await project("wrap", {
254
+ repoPath: repository.path,
255
+ companionPath: path.resolve(companionPath),
256
+ repository,
257
+ config,
258
+ runtimeDocuments,
259
+ globalConfigStore,
260
+ });
261
+
262
+ const failure = firstFailure(result.outcomes);
263
+ if (failure) {
264
+ return {
265
+ kind: "failed",
266
+ document: failure.path,
267
+ error: failure.error ?? new Error("runtime document write failed"),
268
+ };
269
+ }
270
+
271
+ /** The destinations the caller handed over; the rest of the pass is scaffolding. */
272
+ const destinations = new Set(
273
+ runtimeDocuments.map((document) => document.path.split("/").join(path.sep)),
274
+ );
275
+ const outcomes = result.outcomes.filter((outcome) => destinations.has(outcome.path));
276
+ return {
277
+ kind: outcomes.some((outcome) => outcome.state === "written") ? "written" : "current",
278
+ documents: [...new Set(outcomes.map((outcome) => outcome.path))],
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Whether `mate wrap` configured this Working Repository. Asked of the runtime
284
+ * document manifest rather than of the Projection Root, because the root is not
285
+ * evidence of a wrap: a launch and every Capability command write the
286
+ * projection pair too. The manifest is written only by a pass that placed a
287
+ * runtime document, and wrapping is the only pass that places one.
288
+ */
289
+ export async function isWorkingRepositoryWrapped(repoPath: string): Promise<boolean> {
290
+ return (await recordedRuntimeDocuments(repoPath)).length > 0;
291
+ }
292
+
293
+ export type UnwrapResult =
294
+ | { kind: "unwrapped" | "absent"; documents: string[] }
295
+ | { kind: "failed"; document: string; error: Error };
296
+
297
+ /**
298
+ * The inverse of {@link projectWorkingRuntimeDocuments}: runtime documents and
299
+ * projected Claude skill links are withdrawn while the Projection Root, the
300
+ * Repository Link and the companion link are left exactly as they were. That
301
+ * asymmetry with `mate working cleanup` is the point — cleanup removes Mate's
302
+ * whole local integration including the Repository Link, while unwrapping takes
303
+ * back only what wrapping added, leaving a linked repository that was never
304
+ * wrapped. That is what makes unwrapping the way back to a Managed Session
305
+ * rather than a teardown.
306
+ *
307
+ * Driven by the manifest, so it withdraws whatever the wrap that ran recorded,
308
+ * including a destination this release no longer renders.
309
+ */
310
+ export async function unwrapWorkingRuntimeDocuments(repoPath: string): Promise<UnwrapResult> {
311
+ const documents = await recordedRuntimeDocuments(repoPath);
312
+ const removeWrapEntries = async (): Promise<ProjectionEntryOutcome[]> =>
313
+ removeWorkingRepositoryUnwrapEntries(repoPath);
314
+ if (documents.length === 0) {
315
+ const outcomes = await removeWrapEntries();
316
+ const failure = firstFailure(outcomes);
317
+ if (failure) {
318
+ return {
319
+ kind: "failed",
320
+ document: failure.path,
321
+ error: failure.error ?? new Error("wrap entry removal failed"),
322
+ };
323
+ }
324
+ return outcomes.some((entry) => entry.state === "removed")
325
+ ? { kind: "unwrapped", documents: [] }
326
+ : { kind: "absent", documents: [] };
327
+ }
328
+
329
+ /**
330
+ * One manifest read and one manifest write for the whole withdrawal. A
331
+ * per-document call could not be run concurrently — each rewrites the whole
332
+ * manifest, so the last write would restore the keys the others removed, and
333
+ * a surviving key reads back as still wrapped — leaving a launch refused after
334
+ * a successful unwrap.
335
+ */
336
+ const { removed, error } = await removeRuntimeDocuments(repoPath, documents);
337
+ /** Reported after the successful withdrawals are already durable. */
338
+ if (error) return { kind: "failed", document: error.document, error: error.error };
339
+ const outcomes = await removeWrapEntries();
340
+ const failure = firstFailure(outcomes);
341
+ if (failure) {
342
+ return {
343
+ kind: "failed",
344
+ document: failure.path,
345
+ error: failure.error ?? new Error("wrap entry removal failed"),
346
+ };
347
+ }
348
+ return { kind: "unwrapped", documents: removed };
349
+ }
350
+
351
+ /**
352
+ * Best-effort like `backfillCompanionRegistration`: a read-only Working
353
+ * Repository warns and the caller still proceeds.
354
+ */
355
+ export async function projectWorkingRepositoryBestEffort(
356
+ companionPath: string,
357
+ repository: LinkedRepository,
358
+ ): Promise<ProjectionWriteResult> {
359
+ const result = await projectWorkingRepository(companionPath, repository);
360
+ if (result.kind === "failed") {
361
+ console.error(
362
+ `${FRAMEWORK_NAME}: warning: failed to write the projection for ${repository.id}: ${result.error.message}`,
363
+ );
364
+ }
365
+ return result;
366
+ }
@@ -5,7 +5,7 @@ import { readCompanionRegistry } from "./companion-registry-reader";
5
5
  import { GlobalConfigStore } from "./global-config-store";
6
6
  import type { LinkedRepository } from "./types";
7
7
 
8
- /** Envelope version for `mate workspace list --json`; bump on any breaking shape change. */
8
+ /** Envelope version for the aggregate inventory shape; bump on any breaking shape change. */
9
9
  export const WORKSPACE_INVENTORY_SCHEMA_VERSION = 1;
10
10
 
11
11
  export type CompanionInventoryHealth = "ready" | "missing" | "unreadable";
@@ -3,9 +3,19 @@ import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
4
 
5
5
  import { getActiveDistribution } from "../distribution";
6
+ import { mateInstallPath } from "../runtime/install";
6
7
 
7
8
  const require = createRequire(import.meta.url);
8
9
 
10
+ /**
11
+ * Root of the installed mate-core package. A stamp input for the durable
12
+ * projection: a mate installed at a different path resolves different wrapper
13
+ * and plugin assets.
14
+ */
15
+ export function getMateInstallPath(): string {
16
+ return mateInstallPath();
17
+ }
18
+
9
19
  /**
10
20
  * Resolved wrapper directory: a distribution asset root that ships
11
21
  * `wrappers/bin` wins over core's bundled default.
@@ -35,7 +45,7 @@ export function getClaudePluginRoot(): string {
35
45
  export const CLAUDE_PLUGIN_HOOK_SHIMS = [
36
46
  "validate-artifact-path.mjs",
37
47
  "session-banner.mjs",
38
- "artifact-finish-nudge.mjs",
48
+ "session-guidance.mjs",
39
49
  ] as const;
40
50
 
41
51
  /**
@@ -92,9 +92,10 @@ function withPublicNpmConfigSync<T>(
92
92
  export async function fetchPublicPackageVersion(
93
93
  packageName: string,
94
94
  registry = PUBLIC_NPM_REGISTRY,
95
+ distTag = "latest",
95
96
  ): Promise<string> {
96
97
  const { stdout } = await withPublicNpmConfig(packageName, registry, (env) =>
97
- publicNpmDeps.execFile("npm", ["view", packageName, "version"], {
98
+ publicNpmDeps.execFile("npm", ["view", `${packageName}@${distTag}`, "version"], {
98
99
  timeout: 10_000,
99
100
  env,
100
101
  }),
@@ -1,13 +1,13 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
+ import semver from "semver";
5
+
4
6
  import { getActiveDistribution, type DistributionUpdateConfig } from "../distribution";
5
7
  import { FRAMEWORK_NAME } from "../framework";
6
8
  import { fetchPublicPackageVersion, PUBLIC_NPM_REGISTRY } from "./public-npm";
7
9
  import { YamlFileStore } from "./orchestrator/yaml-file-store";
8
10
 
9
- const parse = (v: string): number[] => v.split(".").slice(0, 3).map(Number);
10
-
11
11
  interface UpdateState {
12
12
  lastChecked: string;
13
13
  latestVersion: string | null;
@@ -31,11 +31,12 @@ const updateStateFileSlug = (packageName: string): string =>
31
31
  */
32
32
  export class UpdateStateStore extends YamlFileStore<UpdateState> {
33
33
  constructor(packageName: string = getUpdateConfig().packageName) {
34
+ const channelSuffix = getUpdateChannel() === "canary" ? "-canary" : "";
34
35
  super(
35
36
  path.join(
36
37
  os.homedir(),
37
38
  `.${FRAMEWORK_NAME}`,
38
- `update-state-${updateStateFileSlug(packageName)}.yaml`,
39
+ `update-state-${updateStateFileSlug(packageName)}${channelSuffix}.yaml`,
39
40
  ),
40
41
  );
41
42
  }
@@ -62,12 +63,15 @@ export function isCanaryVersion(version: string = getCurrentVersion()): boolean
62
63
  return version.includes("-canary");
63
64
  }
64
65
 
66
+ export type UpdateChannel = "latest" | "canary";
67
+
68
+ export function getUpdateChannel(version: string = getCurrentVersion()): UpdateChannel {
69
+ return isCanaryVersion(version) ? "canary" : "latest";
70
+ }
71
+
65
72
  export function isNewer(latest: string, current: string): boolean {
66
- const [la, lb, lc] = parse(latest);
67
- const [ca, cb, cc] = parse(current);
68
- if (la !== ca) return la > ca;
69
- if (lb !== cb) return lb > cb;
70
- return lc > cc;
73
+ if (!semver.valid(latest) || !semver.valid(current)) return false;
74
+ return semver.gt(latest, current);
71
75
  }
72
76
 
73
77
  export async function showUpdateBannerIfAvailable(store: UpdateStateStore): Promise<void> {
@@ -109,7 +113,9 @@ export async function enforceUpdateIfRequired(store: UpdateStateStore): Promise<
109
113
 
110
114
  export async function fetchLatestVersion(): Promise<string> {
111
115
  const { packageName, registry } = getUpdateConfig();
112
- return fetchPublicPackageVersion(packageName, registry);
116
+ const latestVersion = await fetchPublicPackageVersion(packageName, registry, getUpdateChannel());
117
+ if (!semver.valid(latestVersion)) throw new Error("npm returned an invalid version");
118
+ return latestVersion;
113
119
  }
114
120
 
115
121
  export function scheduleBackgroundCheck(store: UpdateStateStore): Promise<void> {