@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,249 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import type { WorkingTreeChange } from "../finish/git";
5
+
6
+ /** Whether an archived change still has uncommitted content in the companion working tree. */
7
+ export type CommitState = "committed" | "uncommitted";
8
+
9
+ /** An archive whose delta specs name a canonical spec. Evidence, never proof of authorship. */
10
+ export interface SpecAttribution {
11
+ /** Dated archive directory name, e.g. `2026-09-07-acme`. */
12
+ anchor: string;
13
+ state: CommitState;
14
+ }
15
+
16
+ /** An uncommitted canonical spec no pending change accounts for, with its attribution. */
17
+ export interface UnattributedSpec {
18
+ /** Companion-relative POSIX path of the canonical spec. */
19
+ path: string;
20
+ /** Whether the spec is new or already tracked and modified. */
21
+ kind: WorkingTreeChange;
22
+ /** Every archive naming this spec, oldest anchor first; empty when none does. */
23
+ touchedByArchives: SpecAttribution[];
24
+ }
25
+
26
+ export interface UncommittedSpecChange {
27
+ /** Companion-relative POSIX path of the canonical spec. */
28
+ path: string;
29
+ /** Whether the spec is new or already tracked and modified. */
30
+ kind: WorkingTreeChange;
31
+ }
32
+
33
+ export interface ArchiveEntry {
34
+ /** Change name with the archive date prefix stripped. */
35
+ name: string;
36
+ /** Dated archive directory name, e.g. `2026-09-07-acme`. */
37
+ anchor: string;
38
+ /** Companion-relative POSIX path of the archive directory. */
39
+ path: string;
40
+ /** Tag `mate artifact publish` creates for this anchor. */
41
+ tag: string;
42
+ /** Uncommitted owned directories that make this change pending. */
43
+ uncommittedPaths: string[];
44
+ /** Uncommitted canonical specs this change's delta specs applied to. */
45
+ uncommittedSpecs: string[];
46
+ /** Uncommitted canonical specs with their working-tree change kind. */
47
+ uncommittedSpecChanges: UncommittedSpecChange[];
48
+ state: CommitState;
49
+ }
50
+
51
+ /** Only dated archive directories are publishable; everything else is ignored. */
52
+ const ARCHIVE_ANCHOR_PATTERN = /^\d{4}-\d{2}-\d{2}-.+$/;
53
+ const ARCHIVE_DATE_PREFIX = /^\d{4}-\d{2}-\d{2}-/;
54
+
55
+ export const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
56
+ export const SPECS_RELATIVE_DIR = "openspec/specs";
57
+
58
+ /** The tag `mate artifact publish` creates, never a recomputed date. */
59
+ export function finishMarker(anchor: string): string {
60
+ return `openspec/${anchor}`;
61
+ }
62
+
63
+ /** Porcelain collapses an untracked directory to `dir/`; compare without that suffix. */
64
+ function withoutTrailingSlash(candidate: string): string {
65
+ return candidate.endsWith("/") ? candidate.slice(0, -1) : candidate;
66
+ }
67
+
68
+ /** True when the reported path is the owned path, sits inside it, or collapsed it. */
69
+ function touches(worktreePath: string, ownedPath: string): boolean {
70
+ const reported = withoutTrailingSlash(worktreePath);
71
+ return (
72
+ reported === ownedPath ||
73
+ reported.startsWith(`${ownedPath}/`) ||
74
+ ownedPath.startsWith(`${reported}/`)
75
+ );
76
+ }
77
+
78
+ function matching(uncommittedPaths: string[], owned: string[]): string[] {
79
+ return [
80
+ ...new Set(
81
+ uncommittedPaths.filter((worktreePath) =>
82
+ owned.some((ownedPath) => touches(worktreePath, ownedPath)),
83
+ ),
84
+ ),
85
+ ].toSorted();
86
+ }
87
+
88
+ function matchingDirectories(uncommittedPaths: string[], owned: string[]): string[] {
89
+ return owned
90
+ .flatMap((ownedPath) =>
91
+ uncommittedPaths.some((worktreePath) => touches(worktreePath, ownedPath))
92
+ ? [ownedPath.endsWith("/") ? ownedPath : `${ownedPath}/`]
93
+ : [],
94
+ )
95
+ .toSorted();
96
+ }
97
+
98
+ function changeKindFor(
99
+ worktreePath: string,
100
+ kinds: Readonly<Record<string, WorkingTreeChange>>,
101
+ ): WorkingTreeChange {
102
+ const direct = kinds[worktreePath] ?? kinds[withoutTrailingSlash(worktreePath)];
103
+ if (direct) return direct;
104
+
105
+ return (
106
+ Object.entries(kinds).find(([candidate]) =>
107
+ touches(worktreePath, withoutTrailingSlash(candidate)),
108
+ )?.[1] ?? "modified"
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Canonical specs the change's delta specs applied to. Only directory entry names under
114
+ * the archive's `specs/` tree are read — never any file contents — so archived prose
115
+ * cannot influence discovery.
116
+ */
117
+ async function canonicalSpecsFor(companionPath: string, anchor: string): Promise<string[]> {
118
+ const collectDeltaSpecFiles = async (directory: string, relative = ""): Promise<string[]> => {
119
+ const entries = await fs.readdir(directory, { withFileTypes: true });
120
+ const nested = await Promise.all(
121
+ entries.map(async (entry) => {
122
+ const entryRelative = path.posix.join(relative, entry.name);
123
+ if (entry.isDirectory()) {
124
+ return collectDeltaSpecFiles(path.join(directory, entry.name), entryRelative);
125
+ }
126
+ return entry.isFile() ? [path.posix.join(SPECS_RELATIVE_DIR, entryRelative)] : [];
127
+ }),
128
+ );
129
+ return nested.flat();
130
+ };
131
+ try {
132
+ const specs = await collectDeltaSpecFiles(
133
+ path.join(companionPath, ARCHIVE_RELATIVE_DIR, anchor, "specs"),
134
+ );
135
+ return specs.toSorted();
136
+ } catch {
137
+ // A change without delta specs still owns its archive and active directories.
138
+ return [];
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Lists every dated archive directory with its commit state. State comes from the paths a
144
+ * change owns outright — its archive directory and the active directory archiving deleted
145
+ * — because canonical specs are shared: a spec today's work modified would otherwise
146
+ * resurrect every long-published change that ever touched it. Owned specs are still
147
+ * reported on a pending entry so the publication scope is visible. Ordering is the stable
148
+ * anchor sort (oldest date first).
149
+ */
150
+ export async function discoverArchives(
151
+ companionPath: string,
152
+ uncommittedPaths: string[],
153
+ changeKinds: Readonly<Record<string, WorkingTreeChange>> = {},
154
+ ): Promise<ArchiveEntry[]> {
155
+ let entries;
156
+ try {
157
+ entries = await fs.readdir(path.join(companionPath, "openspec", "changes", "archive"), {
158
+ withFileTypes: true,
159
+ });
160
+ } catch {
161
+ return [];
162
+ }
163
+
164
+ const anchors = entries
165
+ .filter((entry) => entry.isDirectory() && ARCHIVE_ANCHOR_PATTERN.test(entry.name))
166
+ .map((entry) => entry.name)
167
+ .toSorted();
168
+
169
+ return Promise.all(
170
+ anchors.map(async (anchor) => {
171
+ const name = anchor.replace(ARCHIVE_DATE_PREFIX, "");
172
+ const ownedPaths = [`${ARCHIVE_RELATIVE_DIR}/${anchor}`, `openspec/changes/${name}`];
173
+ const owned = matching(uncommittedPaths, ownedPaths);
174
+ const ownedDirectories = matchingDirectories(uncommittedPaths, ownedPaths);
175
+ const specs =
176
+ owned.length > 0
177
+ ? matching(uncommittedPaths, await canonicalSpecsFor(companionPath, anchor))
178
+ : [];
179
+ const specChanges = specs.map((specPath) => ({
180
+ path: specPath,
181
+ kind: changeKindFor(specPath, changeKinds),
182
+ }));
183
+ return {
184
+ name,
185
+ anchor,
186
+ path: `${ARCHIVE_RELATIVE_DIR}/${anchor}`,
187
+ tag: finishMarker(anchor),
188
+ uncommittedPaths: ownedDirectories,
189
+ uncommittedSpecs: specs,
190
+ uncommittedSpecChanges: specChanges,
191
+ state: (owned.length > 0 ? "uncommitted" : "committed") as CommitState,
192
+ };
193
+ }),
194
+ );
195
+ }
196
+
197
+ export function pendingArchives(archives: ArchiveEntry[]): ArchiveEntry[] {
198
+ return archives.filter((archive) => archive.state === "uncommitted");
199
+ }
200
+
201
+ /**
202
+ * Every archive with the canonical specs its delta specs name, in the caller's archive
203
+ * order. Built once per call and only when something is unattributed, so the common
204
+ * `pending` invocation keeps its current I/O.
205
+ */
206
+ async function attributionIndex(
207
+ companionPath: string,
208
+ archives: ArchiveEntry[],
209
+ ): Promise<Array<SpecAttribution & { specs: string[] }>> {
210
+ return Promise.all(
211
+ archives.map(async (archive) => ({
212
+ anchor: archive.anchor,
213
+ state: archive.state,
214
+ specs: await canonicalSpecsFor(companionPath, archive.anchor),
215
+ })),
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Uncommitted canonical specs that no pending change accounts for, each attributed to
221
+ * every archive whose delta specs name it. An attributed spec belongs to a change whose
222
+ * archive is already committed and can be resumed by publishing that change by name; an
223
+ * unattributed one belongs to work that is not archived yet. `archives` is the full
224
+ * archive list, not just the pending ones, because attribution's whole point is naming
225
+ * the committed archives pending discovery excludes.
226
+ */
227
+ export async function unattributedSpecs(
228
+ companionPath: string,
229
+ uncommittedPaths: string[],
230
+ archives: ArchiveEntry[],
231
+ changeKinds: Readonly<Record<string, WorkingTreeChange>> = {},
232
+ ): Promise<UnattributedSpec[]> {
233
+ const claimed = new Set(pendingArchives(archives).flatMap((entry) => entry.uncommittedSpecs));
234
+ const specPaths = uncommittedPaths
235
+ .filter(
236
+ (worktreePath) => touches(worktreePath, SPECS_RELATIVE_DIR) && !claimed.has(worktreePath),
237
+ )
238
+ .toSorted();
239
+ if (specPaths.length === 0) return [];
240
+
241
+ const index = await attributionIndex(companionPath, archives);
242
+ return specPaths.map((specPath) => ({
243
+ path: specPath,
244
+ kind: changeKindFor(specPath, changeKinds),
245
+ touchedByArchives: index.flatMap(({ anchor, state, specs }) =>
246
+ specs.some((spec) => touches(specPath, spec)) ? [{ anchor, state }] : [],
247
+ ),
248
+ }));
249
+ }
@@ -0,0 +1,17 @@
1
+ export { runArtifactPendingCommand } from "./command";
2
+ export type { PendingCommandDeps, PendingResult } from "./command";
3
+ export {
4
+ ARCHIVE_RELATIVE_DIR,
5
+ SPECS_RELATIVE_DIR,
6
+ discoverArchives,
7
+ finishMarker,
8
+ pendingArchives,
9
+ unattributedSpecs,
10
+ } from "./discovery";
11
+ export type {
12
+ ArchiveEntry,
13
+ CommitState,
14
+ SpecAttribution,
15
+ UnattributedSpec,
16
+ UncommittedSpecChange,
17
+ } from "./discovery";
@@ -8,6 +8,7 @@ import {
8
8
  import { resolveForCapability } from "../../../lib/orchestrator/framework-context";
9
9
  import type { CapabilityConfig } from "../../../lib/orchestrator/types";
10
10
  import {
11
+ ensureTokensaveBranchingPosture,
11
12
  ensureTokensaveStoreExcluded,
12
13
  TOKENSAVE_STORE_DIR,
13
14
  } from "../../../tools/setup/capabilities/tokensave";
@@ -24,6 +25,7 @@ export interface SyncCapDeps {
24
25
  loadCapabilities?: () => Promise<CapabilityConfig[]>;
25
26
  isTokensaveInitialized?: () => Promise<boolean>;
26
27
  ensureTokensaveStoreExcluded?: (repoPath: string) => Promise<void>;
28
+ ensureTokensaveBranchingPosture?: (repoPath: string) => Promise<void>;
27
29
  runGraphify?: CapRunner;
28
30
  runTokensave?: CapRunner;
29
31
  onStepStart?: (step: SyncCapStep) => void;
@@ -122,6 +124,8 @@ export async function runIndexCapCommand(args: string[], deps: SyncCapDeps = {})
122
124
  const ensureCompanion = deps.ensureUnambiguousCompanion ?? ensureUnambiguousCompanion;
123
125
  const isTokensaveInitialized = deps.isTokensaveInitialized ?? defaultIsTokensaveInitialized;
124
126
  const ensureStoreExcluded = deps.ensureTokensaveStoreExcluded ?? ensureTokensaveStoreExcluded;
127
+ const ensureBranchingPosture =
128
+ deps.ensureTokensaveBranchingPosture ?? ensureTokensaveBranchingPosture;
125
129
  const runGraphify = deps.runGraphify ?? runGraphifyCapCommand;
126
130
  const runTokensave = deps.runTokensave ?? runTokensaveCapCommand;
127
131
 
@@ -152,18 +156,22 @@ export async function runIndexCapCommand(args: string[], deps: SyncCapDeps = {})
152
156
 
153
157
  if (!options.includeGraphify && hasTokensaveCapability(capabilities)) {
154
158
  onStepStart("tokensave");
159
+ const repoPath = process.env.MATE_REPO_PATH ?? process.cwd();
155
160
  if (!(await isTokensaveInitialized())) {
156
- await ensureStoreExcluded(process.env.MATE_REPO_PATH ?? process.cwd());
161
+ await ensureStoreExcluded(repoPath);
157
162
  const initOk = await runCapStep(() => runTokensave(["init"]));
158
163
  if (!initOk) {
159
164
  failed = true;
160
165
  onStepDone("tokensave", false);
161
166
  } else {
167
+ /** The store config only exists once `tokensave init` has created it. */
168
+ await ensureBranchingPosture(repoPath);
162
169
  const syncOk = await runCapStep(() => runTokensave(["sync"]));
163
170
  if (!syncOk) failed = true;
164
171
  onStepDone("tokensave", syncOk);
165
172
  }
166
173
  } else {
174
+ await ensureBranchingPosture(repoPath);
167
175
  const syncOk = await runCapStep(() => runTokensave(["sync"]));
168
176
  if (!syncOk) failed = true;
169
177
  onStepDone("tokensave", syncOk);
@@ -1,5 +1,4 @@
1
1
  import { runGraphifyCapCommand } from "./graphify";
2
- import { runHeadroomCapCommand } from "./headroom";
3
2
  import { runOpenspecCapCommand } from "./openspec";
4
3
  import { runIndexCapCommand } from "./index-cmd";
5
4
  import { runPluginCliCommand } from "../../plugin-commands";
@@ -7,7 +6,7 @@ import { runPluginCliCommand } from "../../plugin-commands";
7
6
  /**
8
7
  * @command mate cap <capability> [...args]
9
8
  * @description Dispatches to a capability-scoped subcommand: `graphify`,
10
- * `headroom`, `openspec` (each forwards `[...args]` to the underlying CLI tool,
9
+ * `openspec` (each forwards `[...args]` to the underlying CLI tool,
11
10
  * scoped to the resolved companion/working repo), or `index` (re-syncs
12
11
  * capability indexes such as tokensave/graphify after code changes; `sync` is
13
12
  * accepted as a compatibility alias for `index`). Names not claimed by the
@@ -21,7 +20,7 @@ export async function runCapCommand(
21
20
  ): Promise<void> {
22
21
  if (!capabilityName) {
23
22
  process.stderr.write(
24
- "mate: capability name required. Use `mate cap <openspec|graphify|headroom|index|sync>`.\n",
23
+ "mate: capability name required. Use `mate cap <openspec|graphify|index|sync>`.\n",
25
24
  );
26
25
  process.exitCode = 1;
27
26
  return;
@@ -31,9 +30,6 @@ export async function runCapCommand(
31
30
  case "graphify":
32
31
  await runGraphifyCapCommand(args);
33
32
  return;
34
- case "headroom":
35
- await runHeadroomCapCommand(args);
36
- return;
37
33
  case "openspec":
38
34
  await runOpenspecCapCommand(args);
39
35
  return;
@@ -6,7 +6,7 @@ import { ensureTokensaveInstalled } from "../../../tools/setup/capabilities/toke
6
6
 
7
7
  interface TokensaveCapDeps {
8
8
  resolveForLaunch?: typeof resolveForLaunch;
9
- ensureInstalled?: (repoPath: string) => Promise<boolean>;
9
+ ensureInstalled?: (repoPath: string, options?: { upgrade?: boolean }) => Promise<boolean>;
10
10
  spawn?: typeof spawnSync;
11
11
  }
12
12
 
@@ -15,8 +15,8 @@ interface TokensaveCapDeps {
15
15
  * @description Forwards `[...args]` to the `tokensave` CLI, run from the
16
16
  * active working repository. Requires the `tokensave` capability to be enabled
17
17
  * in `.mate/config/framework.yaml` and a registered working repo for the
18
- * current `MATE_REPO_ID`. Ensures the `tokensave` binary is installed in that
19
- * repo (via {@link ensureTokensaveInstalled}) before forwarding.
18
+ * current `MATE_REPO_ID`. Ensures the `tokensave` binary is installed and
19
+ * upgraded in that repo (via {@link ensureTokensaveInstalled}) before forwarding.
20
20
  * @remarks The child process's exit code is propagated via `process.exitCode`.
21
21
  */
22
22
  export async function runTokensaveCapCommand(
@@ -49,7 +49,7 @@ export async function runTokensaveCapCommand(
49
49
  return;
50
50
  }
51
51
 
52
- if (!(await ensureInstalled(repo.path))) {
52
+ if (!(await ensureInstalled(repo.path, { upgrade: true }))) {
53
53
  process.exitCode = 1;
54
54
  return;
55
55
  }
@@ -2,7 +2,8 @@ import { usage } from "../../usage";
2
2
  import { runCompanionLinkCommand } from "./link";
3
3
  import { runSetupCommand } from "../setup";
4
4
  import { runCompanionListCommand } from "./list";
5
- import { runWorkspaceOpenCommand } from "../workspace/open";
5
+ import { runCompanionSyncCommand } from "./sync";
6
+ import { runWorkspaceOpenCommand } from "./open";
6
7
  import { runCompanionTuiCommand } from "./tui";
7
8
 
8
9
  export async function runCompanionCommand(
@@ -19,6 +20,9 @@ export async function runCompanionCommand(
19
20
  case "list":
20
21
  await runCompanionListCommand(argv);
21
22
  return;
23
+ case "sync":
24
+ await runCompanionSyncCommand();
25
+ return;
22
26
  case "open":
23
27
  await runWorkspaceOpenCommand();
24
28
  return;
@@ -239,8 +239,8 @@ export async function runCompanionLinkCommandWithDeps(
239
239
  };
240
240
 
241
241
  // Writes both the companion-side registry entry (companionRootedStore, the
242
- // source of truth for `mate workspace list`) and the repo-local pointer
243
- // (CompanionStore's internal dual write) in one call.
242
+ // source of truth for the aggregate pairing inventory) and the repo-local
243
+ // pointer (CompanionStore's internal dual write) in one call.
244
244
  const registerRepository =
245
245
  deps.registerRepository ??
246
246
  ((nextRepository, options) =>
@@ -0,0 +1,92 @@
1
+ import { FRAMEWORK_NAME } from "../../../framework";
2
+ import { syncCompanionGit } from "../../../lib/orchestrator/companion-git-sync";
3
+ import { LaunchPreflightError } from "../../../lib/orchestrator/types";
4
+ import { resolveCompanionRuntime } from "../../../runtime/env";
5
+ import { readCompanionPolicy } from "../../../runtime/policy";
6
+ import { resolveCompanionPath, resolveRepoPath } from "./tui";
7
+
8
+ export interface CompanionSyncDeps {
9
+ resolveCompanionPath: typeof resolveCompanionPath;
10
+ resolveProjectedCompanionPath: (cwd: string) => string | undefined;
11
+ resolveRepoPath: typeof resolveRepoPath;
12
+ readCompanionPolicy: typeof readCompanionPolicy;
13
+ syncCompanionGit: typeof syncCompanionGit;
14
+ }
15
+
16
+ export const companionSyncDeps: CompanionSyncDeps = {
17
+ resolveCompanionPath,
18
+ resolveProjectedCompanionPath: (cwd) =>
19
+ resolveCompanionRuntime(process.env, cwd).context.companionPath || undefined,
20
+ resolveRepoPath,
21
+ readCompanionPolicy,
22
+ syncCompanionGit,
23
+ };
24
+
25
+ /**
26
+ * The Projection Root answers for a Wrapped Repository, which can never take
27
+ * the launch path — so the command must resolve there too, not only through the
28
+ * repo-local registry the launch uses.
29
+ */
30
+ async function resolveCompanion(cwd: string): Promise<string | undefined> {
31
+ const resolved = await companionSyncDeps.resolveCompanionPath(cwd);
32
+ if (resolved) return resolved;
33
+ return companionSyncDeps.resolveProjectedCompanionPath(cwd);
34
+ }
35
+
36
+ /**
37
+ * @command mate companion sync
38
+ * @description Runs the companion Git synchronization a launch preflight
39
+ * performs, against the companion resolved for the current directory. Runs Git
40
+ * interactively so credentials and conflicts can be answered.
41
+ */
42
+ export async function runCompanionSyncCommand(): Promise<void> {
43
+ const cwd = process.cwd();
44
+ const companionPath = await resolveCompanion(cwd);
45
+ if (!companionPath) {
46
+ process.stderr.write(
47
+ `${FRAMEWORK_NAME}: no companion resolves for this directory. Run \`${FRAMEWORK_NAME} companion link\` first.\n`,
48
+ );
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+
53
+ /** An explicit request does not override a companion configured not to have one. */
54
+ if (!companionSyncDeps.readCompanionPolicy(companionPath).gitAutoMode) {
55
+ process.stderr.write(
56
+ [
57
+ `${FRAMEWORK_NAME}: the companion's Git policy disables synchronization.`,
58
+ ` companion: ${companionPath}`,
59
+ ` Set \`git: auto\` in the companion's framework configuration to enable it.`,
60
+ "",
61
+ ].join("\n"),
62
+ );
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+
67
+ const repoPath = await companionSyncDeps.resolveRepoPath(cwd);
68
+
69
+ try {
70
+ const result = await companionSyncDeps.syncCompanionGit(
71
+ companionPath,
72
+ repoPath === companionPath ? undefined : repoPath,
73
+ true,
74
+ );
75
+ if (result.skipped) {
76
+ process.stdout.write(
77
+ `${FRAMEWORK_NAME}: ${companionPath} is not a Git working tree; nothing to synchronize.\n`,
78
+ );
79
+ return;
80
+ }
81
+ process.stdout.write(
82
+ `${FRAMEWORK_NAME}: synchronized ${companionPath} (${result.changed ? "updated" : "no changes"}).\n`,
83
+ );
84
+ } catch (error) {
85
+ if (error instanceof LaunchPreflightError) {
86
+ process.stderr.write(`${error.message}\n`);
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+ throw error;
91
+ }
92
+ }
@@ -103,9 +103,6 @@ function getSelectedToolChecks(config: {
103
103
  if (selectedCapabilities.has("tokensave")) {
104
104
  checks.push({ name: "tokensave", command: "tokensave", source: "capability" });
105
105
  }
106
- if (selectedCapabilities.has("headroom")) {
107
- checks.push({ name: "headroom", command: "headroom", source: "capability" });
108
- }
109
106
  if (selectedCapabilities.has("rtk")) {
110
107
  checks.push({ name: "rtk", command: "rtk", source: "capability" });
111
108
  }
@@ -24,6 +24,20 @@ export interface ParsedLaunchArgs {
24
24
 
25
25
  export { ensureUnambiguousCompanion, launchAmbiguityDeps };
26
26
 
27
+ export interface LaunchCommandDeps {
28
+ createLauncher: () => Pick<FrameworkLauncher, "prepare">;
29
+ createProgress: typeof createStartupProgress;
30
+ runIndexCapCommand: typeof runIndexCapCommand;
31
+ confirm: typeof confirm;
32
+ }
33
+
34
+ export const launchCommandDeps: LaunchCommandDeps = {
35
+ createLauncher: () => new FrameworkLauncher(),
36
+ createProgress: createStartupProgress,
37
+ runIndexCapCommand,
38
+ confirm,
39
+ };
40
+
27
41
  export function parseDirectLaunchArgs(argv: string[]): ParsedLaunchArgs {
28
42
  const separatorIndex = argv.indexOf("--");
29
43
  if (separatorIndex < 0) return { agentArgs: argv };
@@ -72,15 +86,19 @@ export async function runLaunchToolCommand(
72
86
  skipGit?: boolean;
73
87
  } = {},
74
88
  ): Promise<void> {
75
- const showProgress = !!(process.stdout.isTTY && process.stdin.isTTY);
76
- const progress = showProgress ? createStartupProgress(`Starting ${tool}`) : undefined;
89
+ const interactiveGit = !!(process.stdin.isTTY && process.stdout.isTTY);
90
+ const progress = interactiveGit
91
+ ? launchCommandDeps.createProgress(`Starting ${tool}`)
92
+ : undefined;
77
93
 
78
94
  try {
79
95
  progress?.start("sync", STEP_LABELS.sync);
80
- const prepared = await new FrameworkLauncher().prepare({
96
+ progress?.stop();
97
+ const prepared = await launchCommandDeps.createLauncher().prepare({
81
98
  tool,
82
99
  args,
83
100
  skipGit: options.skipGit,
101
+ interactiveGit,
84
102
  });
85
103
  progress?.succeed("sync");
86
104
  // Stop the Ink UI before running index steps: they spawn graphify/tokensave
@@ -89,14 +107,14 @@ export async function runLaunchToolCommand(
89
107
  progress?.stop();
90
108
 
91
109
  if (!options.skipConfirmation) {
92
- const ok = await confirm("Continue? [y/N] ");
110
+ const ok = await launchCommandDeps.confirm("Continue? [y/N] ");
93
111
  if (!ok) {
94
112
  process.stderr.write("Aborted.\n");
95
113
  process.exit(1);
96
114
  }
97
115
  }
98
116
 
99
- await runIndexCapCommand([], {
117
+ await launchCommandDeps.runIndexCapCommand([], {
100
118
  onStepStart: (step) => process.stdout.write(`${STEP_LABELS[step]}...\n`),
101
119
  onStepDone: (step, ok) => process.stdout.write(`${ok ? "✓" : "✗"} ${STEP_LABELS[step]}\n`),
102
120
  });