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

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 (183) 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 +6 -4
  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/companion/companion.ts +5 -1
  19. package/src/cli/commands/companion/link.ts +2 -2
  20. package/src/cli/commands/companion/sync.ts +92 -0
  21. package/src/cli/commands/doctor.ts +0 -3
  22. package/src/cli/commands/launch/shared.ts +23 -5
  23. package/src/cli/commands/report/collector.ts +72 -78
  24. package/src/cli/commands/report/contract.ts +40 -1
  25. package/src/cli/commands/report/highlight.ts +27 -0
  26. package/src/cli/commands/report/index.ts +11 -18
  27. package/src/cli/commands/report/renderer.ts +199 -2
  28. package/src/cli/commands/report/types.ts +26 -1
  29. package/src/cli/commands/shared/companion-selection.ts +107 -10
  30. package/src/cli/commands/studio/areas.ts +68 -0
  31. package/src/cli/commands/studio/index.ts +69 -0
  32. package/src/cli/commands/studio/inventory.ts +55 -0
  33. package/src/cli/commands/studio/mate-inventory.ts +43 -0
  34. package/src/cli/commands/studio/openspec-cli.ts +198 -0
  35. package/src/cli/commands/studio/payload.ts +184 -0
  36. package/src/cli/commands/studio/routes.ts +2 -0
  37. package/src/cli/commands/studio/selection.ts +61 -0
  38. package/src/cli/commands/studio/server.ts +201 -0
  39. package/src/cli/commands/studio/snapshot.ts +63 -0
  40. package/src/cli/commands/studio/topology.ts +199 -0
  41. package/src/cli/commands/studio/views/client.ts +197 -0
  42. package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
  43. package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
  44. package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
  45. package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
  46. package/src/cli/commands/studio/views/document.tsx +256 -0
  47. package/src/cli/commands/studio/views/error.tsx +20 -0
  48. package/src/cli/commands/studio/views/model.ts +34 -0
  49. package/src/cli/commands/studio/views/pairings.tsx +38 -0
  50. package/src/cli/commands/studio/views/skills/index.tsx +87 -0
  51. package/src/cli/commands/studio/views/specs/index.tsx +101 -0
  52. package/src/cli/commands/studio/views/styles.ts +389 -0
  53. package/src/cli/commands/studio/views/warnings.tsx +21 -0
  54. package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
  55. package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
  56. package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
  57. package/src/cli/commands/unwrap.ts +70 -0
  58. package/src/cli/commands/wrap.ts +164 -0
  59. package/src/cli/main.ts +68 -19
  60. package/src/cli/parse-flags.ts +36 -11
  61. package/src/cli/usage.ts +12 -3
  62. package/src/framework.ts +1 -7
  63. package/src/hooks/session-banner.ts +64 -11
  64. package/src/hooks/session-guidance.ts +40 -0
  65. package/src/hooks/validate-artifact-path.ts +108 -35
  66. package/src/lib/fs-utils.ts +9 -0
  67. package/src/lib/install.ts +33 -0
  68. package/src/lib/orchestrator/adapters/base.ts +14 -125
  69. package/src/lib/orchestrator/adapters/claude.ts +0 -11
  70. package/src/lib/orchestrator/adapters/opencode.ts +2 -32
  71. package/src/lib/orchestrator/companion-git-sync.ts +94 -84
  72. package/src/lib/orchestrator/config-store.ts +2 -21
  73. package/src/lib/orchestrator/editor.ts +12 -22
  74. package/src/lib/orchestrator/framework-context.ts +17 -6
  75. package/src/lib/orchestrator/global-config-store.ts +1 -1
  76. package/src/lib/orchestrator/launcher.ts +97 -7
  77. package/src/lib/orchestrator/opencode-guidance.ts +4 -56
  78. package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
  79. package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
  80. package/src/lib/orchestrator/projection-companion-link.ts +62 -0
  81. package/src/lib/orchestrator/projection-entries.ts +377 -0
  82. package/src/lib/orchestrator/projection-record.ts +56 -0
  83. package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
  84. package/src/lib/orchestrator/projection-types.ts +169 -0
  85. package/src/lib/orchestrator/repo-local-registry.ts +37 -133
  86. package/src/lib/orchestrator/repo-local-store.ts +96 -0
  87. package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
  88. package/src/lib/orchestrator/types.ts +1 -0
  89. package/src/lib/orchestrator/working-repo-projection.ts +366 -0
  90. package/src/lib/orchestrator/workspace-inventory.ts +1 -1
  91. package/src/lib/package-paths.ts +11 -1
  92. package/src/opencode/companion-hooks.ts +89 -245
  93. package/src/opencode/companion-policy.ts +35 -10
  94. package/src/opencode/index.ts +1 -0
  95. package/src/opencode/projected-guidance.ts +56 -0
  96. package/src/opencode/tui.tsx +13 -4
  97. package/src/playbooks/companion-guidance.ts +32 -116
  98. package/src/plugins.ts +0 -1
  99. package/src/runtime/companion-git-state.ts +156 -0
  100. package/src/runtime/companion-git.ts +203 -0
  101. package/src/runtime/companion-guidance.ts +222 -0
  102. package/src/runtime/companion-sync.ts +298 -0
  103. package/src/runtime/env-names.ts +30 -0
  104. package/src/runtime/env.ts +67 -35
  105. package/src/runtime/framework.ts +10 -0
  106. package/src/runtime/freshness.ts +58 -0
  107. package/src/runtime/index.ts +104 -0
  108. package/src/runtime/install.ts +30 -0
  109. package/src/runtime/policy.ts +66 -0
  110. package/src/runtime/projected-guidance.ts +45 -0
  111. package/src/runtime/projection.ts +224 -0
  112. package/src/runtime/repo-local.ts +64 -0
  113. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
  114. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
  115. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
  116. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
  117. package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
  118. package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
  119. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  120. package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
  121. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
  122. package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
  123. package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
  124. package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
  125. package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
  126. package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  127. package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  128. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
  129. package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
  130. package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
  131. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +147 -0
  132. package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
  133. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
  134. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
  135. package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
  136. package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
  137. package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
  138. package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
  139. package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
  140. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
  141. package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
  142. package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
  143. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +147 -0
  144. package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
  145. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
  146. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
  147. package/src/templates/report-assets/README.md +32 -0
  148. package/src/templates/report-assets/mermaid.LICENSE +21 -0
  149. package/src/templates/report-assets/mermaid.min.js +4376 -0
  150. package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
  151. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
  152. package/src/tools/setup/capabilities/graphify.ts +16 -7
  153. package/src/tools/setup/capabilities/openspec.ts +63 -56
  154. package/src/tools/setup/capabilities/tokensave.ts +47 -0
  155. package/src/tools/setup/engine.ts +34 -6
  156. package/src/tools/setup/mate.ts +42 -13
  157. package/src/tools/setup/plugin.ts +9 -0
  158. package/src/tools/setup/plugins/guidance.ts +11 -1
  159. package/src/tools/setup/providers/claude-format.ts +49 -4
  160. package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
  161. package/src/tools/setup/providers/claude.ts +55 -220
  162. package/src/tools/setup/providers/opencode.ts +41 -14
  163. package/src/tools/setup/runtime-documents.ts +174 -0
  164. package/src/tools/setup/surface-target.ts +50 -0
  165. package/src/tools/setup/working-repo-cleanup.ts +33 -26
  166. package/src/tools/setup/working-repo-local-state.ts +21 -1
  167. package/src/tools/setup.ts +25 -3
  168. package/wrappers/bin/graphify +57 -8
  169. package/wrappers/bin/openspec +50 -3
  170. package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
  171. package/src/cli/commands/cap/headroom.ts +0 -52
  172. package/src/cli/commands/workspace/list.ts +0 -25
  173. package/src/cli/commands/workspace/materialize.ts +0 -46
  174. package/src/cli/commands/workspace/workspace.ts +0 -22
  175. package/src/hooks/artifact-finish-nudge.ts +0 -244
  176. package/src/lib/orchestrator/headroom/proxy.ts +0 -116
  177. package/src/lib/orchestrator/workspace-materialize.ts +0 -80
  178. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
  179. package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
  180. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
  181. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
  182. package/src/tools/setup/capabilities/headroom.ts +0 -57
  183. /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
@@ -1,10 +1,19 @@
1
1
  // Git index and merge operations must remain sequential.
2
2
  // oxlint-disable no-await-in-loop
3
- import { execFile as execFileCallback } from "node:child_process";
3
+ import { execFile as execFileCallback, spawn as spawnChild } from "node:child_process";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { promisify } from "node:util";
7
7
 
8
+ import {
9
+ describeGitFailure,
10
+ gitEnvironment,
11
+ isAuthenticationFailure,
12
+ outputLines,
13
+ resolveUpstreamTargetWith,
14
+ type GitResult,
15
+ } from "../../runtime/companion-git";
16
+ import { recordCompanionSync } from "../../runtime/companion-git-state";
8
17
  import { LaunchPreflightError } from "./types";
9
18
 
10
19
  const execFile = promisify(execFileCallback);
@@ -12,13 +21,17 @@ const execFile = promisify(execFileCallback);
12
21
  /** Bounded but far above any diagnostic output; default 1 MiB kills git mid-merge on large trees. */
13
22
  const GIT_MAX_BUFFER = 64 * 1024 * 1024;
14
23
 
15
- export interface GitCommandResult {
16
- status: number;
17
- stdout: string;
18
- stderr: string;
19
- }
24
+ export type GitCommandResult = GitResult;
25
+
26
+ export { describeGitFailure };
20
27
 
21
- export type GitRunner = (args: readonly string[], cwd: string) => Promise<GitCommandResult>;
28
+ export type GitExecutionMode = "captured" | "interactive";
29
+
30
+ export type GitRunner = (
31
+ args: readonly string[],
32
+ cwd: string,
33
+ mode?: GitExecutionMode,
34
+ ) => Promise<GitCommandResult>;
22
35
 
23
36
  export interface CompanionGitSyncResult {
24
37
  skipped: boolean;
@@ -69,19 +82,39 @@ export class CompanionGitSyncError extends LaunchPreflightError {
69
82
  }
70
83
 
71
84
  export const companionGitSyncDeps: { runGit: GitRunner } = {
72
- runGit: async (args, cwd) => {
85
+ runGit: async (args, cwd, mode = "captured") => {
86
+ const captured = gitEnvironment(process.env);
87
+ const { GIT_TERMINAL_PROMPT: _prompt, ...interactive } = captured;
88
+ const env = mode === "captured" ? captured : interactive;
89
+
90
+ if (mode === "interactive") {
91
+ return new Promise((resolve) => {
92
+ let settled = false;
93
+ const finish = (result: GitCommandResult) => {
94
+ if (settled) return;
95
+ settled = true;
96
+ resolve(result);
97
+ };
98
+
99
+ const child = spawnChild("git", [...args], {
100
+ cwd,
101
+ env,
102
+ stdio: "inherit",
103
+ });
104
+ child.once("error", (error) => {
105
+ finish({ status: 1, stdout: "", stderr: error.message });
106
+ });
107
+ child.once("close", (status) => {
108
+ finish({ status: status ?? 1, stdout: "", stderr: "" });
109
+ });
110
+ });
111
+ }
112
+
73
113
  try {
74
- const {
75
- GIT_DIR: _gitDir,
76
- GIT_WORK_TREE: _gitWorkTree,
77
- GIT_COMMON_DIR: _gitCommonDir,
78
- GIT_INDEX_FILE: _gitIndexFile,
79
- ...gitEnv
80
- } = process.env;
81
114
  const result = await execFile("git", [...args], {
82
115
  cwd,
83
116
  encoding: "utf8",
84
- env: gitEnv,
117
+ env,
85
118
  maxBuffer: GIT_MAX_BUFFER,
86
119
  });
87
120
  return { status: 0, stdout: String(result.stdout), stderr: String(result.stderr) };
@@ -114,32 +147,14 @@ function isManagedPath(filePath: string): boolean {
114
147
  return MANAGED_ROOTS.some((root) => normalized === root || normalized.startsWith(`${root}/`));
115
148
  }
116
149
 
117
- function outputLines(stdout: string): string[] {
118
- return stdout
119
- .split("\n")
120
- .map((line) => line.trim())
121
- .filter(Boolean);
122
- }
123
-
124
- /** Matches Git progress-meter lines such as `Updating files: 76% (11340/14790)` or `..., done.` */
125
- const GIT_PROGRESS_LINE = /^\S.*?: +\d+% \(\d+\/\d+\)(?:, done\.)?$/;
126
-
127
- function stripGitProgress(text: string): string {
128
- return text
129
- .split(/\r\n|\r|\n/)
130
- .filter((line) => !GIT_PROGRESS_LINE.test(line.trim()))
131
- .join("\n")
132
- .trim();
133
- }
134
-
135
- export function describeGitFailure(result: GitCommandResult): string {
136
- return stripGitProgress(result.stderr) || stripGitProgress(result.stdout) || "unknown Git error";
137
- }
138
-
139
150
  export class CompanionGitSync {
140
151
  constructor(private readonly runGit: GitRunner = companionGitSyncDeps.runGit) {}
141
152
 
142
- async sync(companionPath: string, workingRepoPath?: string): Promise<CompanionGitSyncResult> {
153
+ async sync(
154
+ companionPath: string,
155
+ workingRepoPath?: string,
156
+ interactiveGit = false,
157
+ ): Promise<CompanionGitSyncResult> {
143
158
  if (!(await this.assertSafeCompanionRoot(companionPath, workingRepoPath))) {
144
159
  return { skipped: true, changed: false, companionPath };
145
160
  }
@@ -157,13 +172,30 @@ export class CompanionGitSync {
157
172
  }
158
173
  const dirty = outputLines(before.stdout).length > 0;
159
174
 
160
- const fetch = await this.command(companionPath, ["fetch", target.remote, target.branch]);
175
+ const fetch = await this.command(
176
+ companionPath,
177
+ ["fetch", target.remote, target.branch],
178
+ interactiveGit ? "interactive" : undefined,
179
+ );
161
180
  if (fetch.status !== 0) {
181
+ const authenticationFailure = isAuthenticationFailure(fetch);
182
+ const recovery = interactiveGit
183
+ ? authenticationFailure
184
+ ? "Retry the launch and complete Git authentication, or use the `--no-git` bypass."
185
+ : "Check the Git or SSH diagnostics above, then retry the launch."
186
+ : authenticationFailure
187
+ ? "Provide credentials through a non-interactive Git credential helper or SSH agent, retry from a terminal, or use the `--no-git` bypass."
188
+ : "Check the remote and network, or resolve the issue before retrying.";
189
+ const detail = this.detail(fetch);
190
+ const failureDetail =
191
+ interactiveGit && detail === "unknown Git error"
192
+ ? "Git fetch failed; review the Git or SSH diagnostics above."
193
+ : detail;
162
194
  throw this.failure(
163
195
  companionPath,
164
- `Unable to fetch ${target.ref}: ${this.detail(fetch)}`,
196
+ `Unable to fetch ${target.ref}: ${failureDetail}`,
165
197
  [],
166
- "Check the remote and network, or resolve the issue before retrying.",
198
+ recovery,
167
199
  );
168
200
  }
169
201
 
@@ -207,6 +239,8 @@ export class CompanionGitSync {
207
239
  }
208
240
 
209
241
  const headAfter = await this.command(companionPath, ["rev-parse", "HEAD"]);
242
+ /** Best-effort bookkeeping: a launch that synchronized must not fail on it. */
243
+ recordCompanionSync(companionPath);
210
244
  return {
211
245
  skipped: false,
212
246
  changed: headBefore.stdout.trim() !== headAfter.stdout.trim(),
@@ -319,35 +353,13 @@ export class CompanionGitSync {
319
353
  }
320
354
  }
321
355
 
356
+ /**
357
+ * Delegates to `runtime/`: the blind half resolves the same ref, and the two
358
+ * must never disagree about which upstream they mean.
359
+ */
322
360
  private async resolveSyncTarget(companionPath: string): Promise<SyncTarget> {
323
- const upstream = await this.command(companionPath, [
324
- "rev-parse",
325
- "--abbrev-ref",
326
- "--symbolic-full-name",
327
- "@{u}",
328
- ]);
329
- const configured = this.parseRemoteRef(upstream.stdout);
330
- if (upstream.status === 0 && configured) return configured;
331
-
332
- const remoteHead = await this.command(companionPath, [
333
- "symbolic-ref",
334
- "--short",
335
- "refs/remotes/origin/HEAD",
336
- ]);
337
- const fromRemoteHead = this.parseRemoteRef(remoteHead.stdout);
338
- if (remoteHead.status === 0 && fromRemoteHead) return fromRemoteHead;
339
-
340
- for (const branch of ["main", "master"]) {
341
- const exists = await this.command(companionPath, [
342
- "show-ref",
343
- "--verify",
344
- "--quiet",
345
- `refs/remotes/origin/${branch}`,
346
- ]);
347
- if (exists.status === 0) {
348
- return { remote: "origin", branch, ref: `origin/${branch}` };
349
- }
350
- }
361
+ const target = await resolveUpstreamTargetWith((args) => this.command(companionPath, args));
362
+ if (target) return target;
351
363
 
352
364
  throw this.failure(
353
365
  companionPath,
@@ -357,17 +369,6 @@ export class CompanionGitSync {
357
369
  );
358
370
  }
359
371
 
360
- private parseRemoteRef(stdout: string): SyncTarget | undefined {
361
- const ref = stdout.trim();
362
- const separator = ref.indexOf("/");
363
- if (separator <= 0 || separator === ref.length - 1) return undefined;
364
- return {
365
- remote: ref.slice(0, separator),
366
- branch: ref.slice(separator + 1),
367
- ref,
368
- };
369
- }
370
-
371
372
  private async assertNoUnfinishedOperation(companionPath: string): Promise<void> {
372
373
  const unfinished = [
373
374
  "MERGE_HEAD",
@@ -464,8 +465,12 @@ export class CompanionGitSync {
464
465
  }
465
466
  }
466
467
 
467
- private async command(companionPath: string, args: readonly string[]): Promise<GitCommandResult> {
468
- return this.runGit(args, companionPath);
468
+ private async command(
469
+ companionPath: string,
470
+ args: readonly string[],
471
+ mode?: GitExecutionMode,
472
+ ): Promise<GitCommandResult> {
473
+ return mode ? this.runGit(args, companionPath, mode) : this.runGit(args, companionPath);
469
474
  }
470
475
 
471
476
  private detail(result: GitCommandResult): string {
@@ -485,6 +490,11 @@ export class CompanionGitSync {
485
490
  export async function syncCompanionGit(
486
491
  companionPath: string,
487
492
  workingRepoPath?: string,
493
+ interactiveGit = false,
488
494
  ): Promise<CompanionGitSyncResult> {
489
- return new CompanionGitSync(companionGitSyncDeps.runGit).sync(companionPath, workingRepoPath);
495
+ return new CompanionGitSync(companionGitSyncDeps.runGit).sync(
496
+ companionPath,
497
+ workingRepoPath,
498
+ interactiveGit,
499
+ );
490
500
  }
@@ -5,8 +5,6 @@ import { getDefaultSetupSelections } from "./setup-compatibilities";
5
5
  import { YamlFileStore } from "./yaml-file-store";
6
6
  import { ConfigError, type FrameworkConfig } from "./types";
7
7
 
8
- export const RTK_CAPABILITY_SPLIT_MIGRATION = "rtk-capability-split-v1";
9
-
10
8
  const HUB_MEMBER_SOURCE_KINDS = ["git", "local"] as const;
11
9
 
12
10
  function defaultConfigPath(): string {
@@ -57,20 +55,6 @@ export function mergeWithDefaults(existing: FrameworkConfig): FrameworkConfig {
57
55
  };
58
56
  }
59
57
 
60
- export function migrateRtkCapabilitySplit(config: FrameworkConfig): FrameworkConfig {
61
- const migrations = config.migrations ?? [];
62
- if (migrations.includes(RTK_CAPABILITY_SPLIT_MIGRATION)) return config;
63
-
64
- const capabilities = config.capabilities ?? [];
65
- const hasHeadroom = capabilities.some((capability) => capability.name === "headroom");
66
- const hasRtk = capabilities.some((capability) => capability.name === "rtk");
67
- return {
68
- ...config,
69
- capabilities: hasHeadroom && !hasRtk ? [...capabilities, { name: "rtk" }] : capabilities,
70
- migrations: [...migrations, RTK_CAPABILITY_SPLIT_MIGRATION],
71
- };
72
- }
73
-
74
58
  /** Validates the shape of a hub manifest before it is used. */
75
59
  export function validateHubConfig(config: FrameworkConfig): void {
76
60
  if (config.type !== "hub") {
@@ -166,14 +150,11 @@ export class ConfigStore extends YamlFileStore<FrameworkConfig> {
166
150
  const merged = mergeWithDefaults(migrateProfilesToAllowedAgents(await super.load()));
167
151
  validateHubConfig(merged);
168
152
  validatePluginDeclarations(merged);
169
- const needsMigration = !(merged.migrations ?? []).includes(RTK_CAPABILITY_SPLIT_MIGRATION);
170
- const config = migrateRtkCapabilitySplit(merged);
171
- if (needsMigration) await this.save(config);
172
- return config;
153
+ return merged;
173
154
  }
174
155
 
175
156
  protected async onMissing(): Promise<FrameworkConfig> {
176
- const config = migrateRtkCapabilitySplit(defaultConfig());
157
+ const config = defaultConfig();
177
158
  await this.save(config);
178
159
  return config;
179
160
  }
@@ -1,8 +1,10 @@
1
1
  import { execFileSync, spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import fs from "node:fs/promises";
4
3
  import path from "node:path";
5
4
 
5
+ import { workspaceDocumentPath } from "./projection-entries";
6
+ import { project } from "./working-repo-projection";
7
+
6
8
  type SpawnLike = typeof spawn;
7
9
 
8
10
  const CURSOR_FALLBACK_PATHS = [
@@ -75,9 +77,8 @@ export function writeMissingEditorCliGuidance(
75
77
  write(`mate: run "Install '${cli}' command in PATH" from the Command Palette to enable this.\n`);
76
78
  }
77
79
 
78
- export function editorWorkspacePath(repoPath: string): string {
79
- return path.join(path.resolve(repoPath), ".mate", "workspace.code-workspace");
80
- }
80
+ /** The Projection Root owns the path; this is the editor's name for it. */
81
+ export const editorWorkspacePath = workspaceDocumentPath;
81
82
 
82
83
  export interface WorkspaceDocument {
83
84
  workspacePath: string;
@@ -87,29 +88,18 @@ export interface WorkspaceDocument {
87
88
  /**
88
89
  * Writes the generated `.mate/workspace.code-workspace` file (working
89
90
  * repository first, companion second) and returns its path and folder
90
- * order. Shared by editor-launching `injectEditorFolder` and the
91
- * non-launching `workspace materialize` command — both must produce the
92
- * same document for the same pairing.
91
+ * order. Called by editor-launching `injectEditorFolder`, which backs
92
+ * `mate companion open`.
93
93
  */
94
94
  export async function writeWorkspaceDocument(
95
95
  companionPath: string,
96
96
  repoPath: string,
97
97
  ): Promise<WorkspaceDocument> {
98
- const folders: [string, string] = [path.resolve(repoPath), path.resolve(companionPath)];
99
- const workspacePath = editorWorkspacePath(repoPath);
100
- await fs.mkdir(path.dirname(workspacePath), { recursive: true });
101
- await fs.writeFile(
102
- workspacePath,
103
- `${JSON.stringify(
104
- {
105
- folders: folders.map((candidate) => ({ path: candidate })),
106
- },
107
- null,
108
- 2,
109
- )}\n`,
110
- "utf8",
111
- );
112
- return { workspacePath, folders };
98
+ await project("workspace", { repoPath, companionPath });
99
+ return {
100
+ workspacePath: workspaceDocumentPath(repoPath),
101
+ folders: [path.resolve(repoPath), path.resolve(companionPath)],
102
+ };
113
103
  }
114
104
 
115
105
  export async function injectEditorFolder(
@@ -10,6 +10,7 @@ import { readCompanionRegistry } from "./companion-registry-reader";
10
10
  import { ConfigStore } from "./config-store";
11
11
  import { GlobalConfigStore } from "./global-config-store";
12
12
  import { findRepoLocalLinkedRepository } from "./repo-local-registry";
13
+ import { projectWorkingRepositoryBestEffort } from "./working-repo-projection";
13
14
  import {
14
15
  AmbiguousCompanionError,
15
16
  ConfigError,
@@ -216,6 +217,7 @@ export async function resolveForLaunch(
216
217
  const repository = (await findRepoLocalLinkedRepository(cwd)) ?? undefined;
217
218
  if (repository) {
218
219
  await backfillCompanionRegistration(resolution.match.companionPath, repository);
220
+ await projectWorkingRepositoryBestEffort(resolution.match.companionPath, repository);
219
221
  }
220
222
  const context = await withResolvedHub(
221
223
  makeContext(resolution.match.companionPath, "working-repo", repository),
@@ -280,14 +282,23 @@ export async function resolveForCapability(
280
282
  return { ...ctx, repositoryId: "" };
281
283
  }
282
284
 
283
- const match = await new CompanionResolver(globalConfigStore).resolve(cwd);
285
+ /**
286
+ * `resolveWithDiagnostics` only to learn whether the match is ambiguous;
287
+ * `resolve()` is this same call with `logFailures`, so the returned match is
288
+ * unchanged. Unlike `resolveForLaunch` this does not refuse on ambiguity —
289
+ * it only declines to make a guessed companion durable.
290
+ */
291
+ const resolution = await new CompanionResolver(globalConfigStore).resolveWithDiagnostics(cwd, {
292
+ logFailures: true,
293
+ });
294
+ const match = resolution.match;
284
295
  if (match) {
296
+ const repository = (await findRepoLocalLinkedRepository(cwd)) ?? undefined;
297
+ if (repository && resolution.ambiguousMatches.length <= 1) {
298
+ await projectWorkingRepositoryBestEffort(match.companionPath, repository);
299
+ }
285
300
  const context = await withResolvedHub(
286
- makeContext(
287
- match.companionPath,
288
- "working-repo",
289
- (await findRepoLocalLinkedRepository(cwd)) ?? undefined,
290
- ),
301
+ makeContext(match.companionPath, "working-repo", repository),
291
302
  );
292
303
  return {
293
304
  ...context,
@@ -2,7 +2,7 @@ import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
4
  import { FRAMEWORK_NAME } from "../../framework";
5
- import { pathIsDirectory } from "./repo-local-registry";
5
+ import { pathIsDirectory } from "../fs-utils";
6
6
  import { YamlFileStore } from "./yaml-file-store";
7
7
 
8
8
  interface CompanionEntry {
@@ -1,6 +1,7 @@
1
1
  // oxlint-disable no-underscore-dangle
2
- import { syncCompanionFiles, syncWorkingRepoClaudeSettings } from "../../tools/setup";
2
+ import { syncCompanionFiles } from "../../tools/setup";
3
3
  import { getActiveDistribution } from "../../distribution";
4
+ import { FRAMEWORK_NAME } from "../../framework";
4
5
  import type { CapabilityPlugin, LaunchPreflightContext } from "../../tools/setup/plugin";
5
6
  import type { LaunchAdapter, AdapterContext } from "./adapters/base";
6
7
  import { ClaudeAdapter } from "./adapters/claude";
@@ -8,6 +9,17 @@ import { OpenCodeAdapter } from "./adapters/opencode";
8
9
  import { CompanionStore } from "./companion-store";
9
10
  import { syncCompanionGit } from "./companion-git-sync";
10
11
  import { resolveForLaunch, type LaunchContext } from "./framework-context";
12
+ import {
13
+ firstFailure,
14
+ type ProjectionEntryOutcome,
15
+ type ProjectionInput,
16
+ type ProjectionResult,
17
+ } from "./projection-types";
18
+ import {
19
+ isWorkingRepositoryWrapped,
20
+ project,
21
+ projectWorkingRepositoryBestEffort,
22
+ } from "./working-repo-projection";
11
23
  import {
12
24
  RepositoryNotSelectedError,
13
25
  LaunchPreflightError,
@@ -38,10 +50,54 @@ interface ResolvedLaunchState {
38
50
 
39
51
  export const launcherDeps = {
40
52
  syncCompanionFiles,
41
- syncWorkingRepoClaudeSettings,
53
+ /** The Managed Projection's launch scope; the working repo is written only here. */
54
+ projectWorkingRepo: (input: ProjectionInput) => project("launch", input),
55
+ /**
56
+ * The Projection Root a launch guarantees for itself. `resolveForLaunch` also
57
+ * writes it, but only on the branch that resolved a companion from the
58
+ * Working Repository; asking here makes the guarantee independent of which
59
+ * branch answered. Best-effort — a launch carries its own configuration.
60
+ */
61
+ refreshProjectionRoot: projectWorkingRepositoryBestEffort,
42
62
  syncCompanionGit,
63
+ isWrapped: isWorkingRepositoryWrapped,
43
64
  };
44
65
 
66
+ /**
67
+ * A wrapped Working Repository delivers the companion to sessions the operator
68
+ * starts themselves, through documents the Agent Runtime discovers by its own
69
+ * upward walk. A Managed Session loads those *and* everything the launch
70
+ * injects through `--settings`, `--mcp-config` and `--plugin-dir`, which
71
+ * ADR-014 established can never be withdrawn. Two delivery channels that cannot
72
+ * be merged must therefore not both be live for one repository, so wrapping and
73
+ * launching are exclusive rather than reconciled (ADR-015, superseding
74
+ * ADR-014's judgement that the duplication is bounded enough to accept).
75
+ *
76
+ * Raised before every write the launch itself performs: the companion Git sync,
77
+ * the companion file sync, the launch scope's projection and the runtime
78
+ * invocation. Companion resolution has already refreshed the Projection Root by
79
+ * this point — `resolveForLaunch` writes it while resolving — but that write is
80
+ * idempotent and byte-identical to the one `mate wrap` performs, so it cannot
81
+ * change what the repository holds.
82
+ */
83
+ function wrappedRepositoryRefusal(repository: LinkedRepository): string {
84
+ return [
85
+ `${FRAMEWORK_NAME}: ${repository.id} is wrapped — \`${FRAMEWORK_NAME} claude\` and \`${FRAMEWORK_NAME} opencode\` do not run in a wrapped working repository.`,
86
+ "Start the session yourself: `claude` or `opencode` — the wrap delivers the companion guidance.",
87
+ `Or restore managed launches: \`${FRAMEWORK_NAME} unwrap\`.`,
88
+ ].join("\n");
89
+ }
90
+
91
+ /**
92
+ * The shape `projectWorkingRepositoryBestEffort` reports a failed projection
93
+ * in, narrowed to the entry that failed: the launch scope writes several, so
94
+ * "the projection" alone would not tell the operator what to fix.
95
+ */
96
+ function projectionFailure(outcome: ProjectionEntryOutcome, repository: LinkedRepository): string {
97
+ const error = outcome.error ?? new Error("projection failed");
98
+ return `failed to write ${outcome.path} for ${repository.id}: ${error.message}`;
99
+ }
100
+
45
101
  export class FrameworkLauncher {
46
102
  private readonly adapters = new Map<string, LaunchAdapter>([
47
103
  ["claude", new ClaudeAdapter()],
@@ -62,15 +118,27 @@ export class FrameworkLauncher {
62
118
  const state = await this.resolveLaunchState(request);
63
119
  const adapterContext = this.makeAdapterContext(state);
64
120
 
121
+ if (await launcherDeps.isWrapped(state.repository.path)) {
122
+ throw new LaunchPreflightError(wrappedRepositoryRefusal(state.repository));
123
+ }
124
+
65
125
  if (!request.skipGit && state.config.git === "auto") {
66
- await launcherDeps.syncCompanionGit(state.companionPath, state.repository.path);
126
+ await launcherDeps.syncCompanionGit(
127
+ state.companionPath,
128
+ state.repository.path,
129
+ request.interactiveGit ?? false,
130
+ );
67
131
  }
68
132
  await launcherDeps.syncCompanionFiles(state.companionPath, state.config, state.repository.path);
69
- await launcherDeps.syncWorkingRepoClaudeSettings(
70
- state.repository.path,
71
- state.companionPath,
72
- state.config,
133
+ this.answerForProjection(
134
+ await launcherDeps.projectWorkingRepo({
135
+ repoPath: state.repository.path,
136
+ companionPath: state.companionPath,
137
+ config: state.config,
138
+ }),
139
+ state.repository,
73
140
  );
141
+ await launcherDeps.refreshProjectionRoot(state.companionPath, state.repository);
74
142
  await this.runCapabilityPreflight(state, request.tool);
75
143
  await state.adapter.validateLaunch(adapterContext);
76
144
 
@@ -115,6 +183,28 @@ export class FrameworkLauncher {
115
183
  };
116
184
  }
117
185
 
186
+ /**
187
+ * `project` reports its failures rather than throwing, so the launch scope's
188
+ * caller is the one that has to answer for them. A non-degradable entry is a
189
+ * guardrail the session was promised — writing the companion into the
190
+ * runtime's allow-list, for one — so the launch fails instead of starting
191
+ * without it. A degradable entry warns in the voice
192
+ * {@link projectWorkingRepositoryBestEffort} uses, and the launch proceeds.
193
+ */
194
+ private answerForProjection(result: ProjectionResult, repository: LinkedRepository): void {
195
+ for (const outcome of result.outcomes) {
196
+ if (outcome.state !== "failed" || !outcome.degradable) continue;
197
+ console.error(`${FRAMEWORK_NAME}: warning: ${projectionFailure(outcome, repository)}`);
198
+ }
199
+
200
+ const failure = firstFailure(result.outcomes);
201
+ if (failure) {
202
+ throw new LaunchPreflightError(
203
+ `${FRAMEWORK_NAME}: ${projectionFailure(failure, repository)}`,
204
+ );
205
+ }
206
+ }
207
+
118
208
  private makeAdapterContext(state: ResolvedLaunchState): AdapterContext {
119
209
  return {
120
210
  repository: state.repository,
@@ -1,58 +1,6 @@
1
- import { GUIDANCE_FILE_VERSION, type MateGuidanceFile } from "../../runtime/guidance";
2
-
3
- import {
4
- buildCodebaseExplorationGuidanceSection,
5
- buildCompanionPolicyXml,
6
- hasGraphifyCapability,
7
- hasTokensaveCapability,
8
- } from "../../playbooks/companion-guidance";
9
- import type { CapabilityConfig } from "./types";
10
-
11
1
  /**
12
- * Build the companion guidance payload delivered to the OpenCode plugin
13
- * through the `MATE_GUIDANCE_JSON` launch environment variable. The guidance
14
- * text carries `$MATE_*` placeholders; the plugin materializes them from the
15
- * session environment, so the same payload shape serves every companion.
16
- *
17
- * Real capabilities are passed through (not just the graphify/tokensave
18
- * flags) so capability-gated companion-policy rules — e.g. openspec-finish —
19
- * render exactly as they do for the Claude provider.
2
+ * The OpenCode guidance payload. Built in `runtime/` so the plugin can build
3
+ * the same payload from the Projection Root when no launch injected one; this
4
+ * module stays as the orchestrator-side name the launch adapter imports.
20
5
  */
21
- export function buildOpenCodeGuidance(capabilities: CapabilityConfig[]): MateGuidanceFile {
22
- const companionGuidance = buildCompanionPolicyXml(
23
- {
24
- companionPath: "$MATE_ARTIFACT_PATH",
25
- repository: {
26
- id: "$MATE_REPO_ID",
27
- path: "$MATE_REPO_PATH",
28
- },
29
- allowedAgents: [],
30
- capabilities,
31
- },
32
- { wrapperBinPath: "$MATE_WRAPPER_BIN_PATH" },
33
- );
34
- const graphifyEnabled = hasGraphifyCapability(capabilities);
35
- const tokensaveEnabled = hasTokensaveCapability(capabilities);
36
- const codebaseExplorationGuidance = buildCodebaseExplorationGuidanceSection({
37
- useGraphify: graphifyEnabled,
38
- useTokensave: tokensaveEnabled,
39
- });
40
- const errors: string[] = [];
41
-
42
- if (!companionGuidance.includes("<companion-policy ")) {
43
- errors.push("companion guidance was not injected");
44
- }
45
- if (
46
- (graphifyEnabled || tokensaveEnabled) &&
47
- !codebaseExplorationGuidance.includes("<codebase-exploration-rules ")
48
- ) {
49
- errors.push("codebase exploration guidance was not injected");
50
- }
51
-
52
- return {
53
- version: GUIDANCE_FILE_VERSION,
54
- companionGuidance,
55
- codebaseExplorationGuidance,
56
- errors,
57
- };
58
- }
6
+ export { buildOpenCodeGuidance } from "../../runtime/companion-guidance";