@uniqbit/mate-core 0.18.5 → 1.0.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 (209) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/package.json +10 -2
  4. package/src/brand/art.generated.ts +69 -0
  5. package/src/brand/index.ts +12 -0
  6. package/src/brand/palette.ts +19 -0
  7. package/src/brand/svg.generated.ts +8 -0
  8. package/src/brand/terminal.ts +83 -0
  9. package/src/brand/types.ts +33 -0
  10. package/src/cli/commands/artifact/artifact.ts +8 -0
  11. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  12. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  13. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  14. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  15. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  16. package/src/cli/commands/artifact/finish/command.ts +33 -3
  17. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  18. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  19. package/src/cli/commands/artifact/finish/openspec.ts +177 -138
  20. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  21. package/src/cli/commands/artifact/pending/command.ts +31 -3
  22. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  23. package/src/cli/commands/cap/graphify.ts +52 -3
  24. package/src/cli/commands/cap/index-cmd.ts +8 -1
  25. package/src/cli/commands/cap/openspec.ts +13 -25
  26. package/src/cli/commands/companion/open.ts +56 -39
  27. package/src/cli/commands/companion/update.ts +75 -42
  28. package/src/cli/commands/doctor.ts +176 -3
  29. package/src/cli/commands/plugin/install.ts +9 -4
  30. package/src/cli/commands/plugin/plugin.ts +3 -2
  31. package/src/cli/commands/report/adapter.ts +11 -2
  32. package/src/cli/commands/report/contract.ts +21 -0
  33. package/src/cli/commands/report/highlight.ts +34 -4
  34. package/src/cli/commands/report/markdown.ts +110 -0
  35. package/src/cli/commands/report/palette.ts +118 -0
  36. package/src/cli/commands/report/renderer.ts +456 -153
  37. package/src/cli/commands/report/types.ts +16 -0
  38. package/src/cli/commands/setup.ts +7 -15
  39. package/src/cli/commands/studio/client-asset.ts +16 -0
  40. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  41. package/src/cli/commands/studio/routes.ts +2 -0
  42. package/src/cli/commands/studio/selection.ts +19 -1
  43. package/src/cli/commands/studio/server.ts +42 -1
  44. package/src/cli/commands/studio/views/client.ts +6 -616
  45. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  46. package/src/cli/commands/studio/views/document.tsx +137 -69
  47. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  48. package/src/cli/commands/studio/views/palette.ts +134 -0
  49. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  50. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  51. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  52. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  53. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  54. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  55. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  56. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  57. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  58. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  59. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  60. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  61. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  62. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  63. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  64. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  65. package/src/cli/commands/studio/views/styles.ts +217 -206
  66. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  67. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  68. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  69. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  70. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  71. package/src/cli/commands/wrap.ts +5 -13
  72. package/src/cli/main.ts +4 -1
  73. package/src/cli/parse-flags.ts +25 -7
  74. package/src/cli/setup-selector.tsx +38 -162
  75. package/src/cli/usage.ts +4 -0
  76. package/src/hooks/companion-instructions.ts +190 -0
  77. package/src/hooks/hook-io.ts +63 -0
  78. package/src/hooks/session-banner.ts +21 -29
  79. package/src/hooks/session-guidance.ts +9 -17
  80. package/src/hooks/session-prepare.ts +115 -28
  81. package/src/hooks/validate-artifact-path.ts +82 -24
  82. package/src/index.ts +4 -0
  83. package/src/lib/change-layout.ts +154 -0
  84. package/src/lib/components/wizard-header.tsx +3 -1
  85. package/src/lib/context-mode-package.ts +24 -11
  86. package/src/lib/delivery/archive-change.ts +154 -0
  87. package/src/lib/delivery/branches.ts +108 -0
  88. package/src/lib/delivery/environment.ts +29 -0
  89. package/src/lib/delivery/forge.ts +279 -0
  90. package/src/lib/delivery/overlap.ts +202 -0
  91. package/src/lib/delivery/record-delivery.ts +196 -0
  92. package/src/lib/delivery/record.ts +343 -0
  93. package/src/lib/delivery/refresh.ts +174 -0
  94. package/src/lib/delivery/repository-access.ts +97 -0
  95. package/src/lib/delivery/stages.ts +111 -0
  96. package/src/lib/delivery/token.ts +58 -0
  97. package/src/lib/frontmatter.ts +18 -0
  98. package/src/lib/opencode-plugin-package.ts +5 -5
  99. package/src/lib/openspec-cli.ts +116 -0
  100. package/src/lib/openspec-scopes.ts +86 -0
  101. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  102. package/src/lib/orchestrator/adapters/claude.ts +48 -40
  103. package/src/lib/orchestrator/adapters/opencode.ts +2 -0
  104. package/src/lib/orchestrator/editor.ts +9 -9
  105. package/src/lib/orchestrator/openspec-store.ts +14 -54
  106. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  107. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  108. package/src/lib/orchestrator/projection-claude-skills.ts +19 -137
  109. package/src/lib/orchestrator/projection-entries.ts +45 -14
  110. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  111. package/src/lib/orchestrator/projection-types.ts +3 -0
  112. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  113. package/src/lib/orchestrator/session-prepare.ts +45 -5
  114. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  115. package/src/lib/orchestrator/types.ts +2 -2
  116. package/src/lib/orchestrator/wrap-pass.ts +22 -1
  117. package/src/lib/package-paths.ts +8 -0
  118. package/src/opencode/companion-hooks.ts +64 -5
  119. package/src/opencode/companion-policy.ts +37 -18
  120. package/src/opencode/projected-guidance.ts +4 -1
  121. package/src/opencode/session-prepare.ts +2 -1
  122. package/src/opencode/tui.tsx +4 -2
  123. package/src/runtime/artifact-refusal.ts +63 -0
  124. package/src/runtime/claude-delivery-plan.ts +89 -7
  125. package/src/runtime/commit-attribution-guard.ts +181 -0
  126. package/src/runtime/companion-guidance.ts +15 -3
  127. package/src/runtime/companion-instructions.ts +340 -0
  128. package/src/runtime/error-message.ts +4 -0
  129. package/src/runtime/index.ts +5 -0
  130. package/src/runtime/managed-block.ts +117 -0
  131. package/src/runtime/markdown-scan.ts +266 -0
  132. package/src/runtime/policy.ts +15 -1
  133. package/src/runtime/projected-flag.ts +7 -0
  134. package/src/runtime/projected-guidance.ts +1 -0
  135. package/src/runtime/runtime-document-manifest.ts +62 -11
  136. package/src/runtime/secret-guard.ts +148 -0
  137. package/src/runtime/secret-patterns.ts +164 -0
  138. package/src/runtime/secret-scan.ts +90 -0
  139. package/src/runtime/session-kind.ts +13 -0
  140. package/src/runtime/shell-heredoc.ts +81 -0
  141. package/src/runtime/wrap-state.ts +36 -2
  142. package/src/runtime/write-atomic.ts +64 -0
  143. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  144. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  145. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  146. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  147. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  148. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  149. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  150. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  151. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  152. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  153. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  154. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  155. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  156. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  157. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  158. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  159. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  160. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  161. package/src/templates/mate-skills/{claude/mate-artifact-publish → agents/mate-publish-artifact}/SKILL.md +91 -23
  162. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  163. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  164. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  165. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  166. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  167. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  168. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  169. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  170. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  171. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  172. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  173. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  174. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  175. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  176. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  177. package/src/templates/mate-skills/{agents/mate-artifact-publish → claude/mate-publish-artifact}/SKILL.md +91 -23
  178. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  179. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  180. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  181. package/src/templates/mate-skills/openspec-references.ts +140 -0
  182. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  183. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  184. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  185. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  186. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  187. package/src/tools/setup/capabilities/graphify.ts +12 -0
  188. package/src/tools/setup/capabilities/openspec.ts +151 -270
  189. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  190. package/src/tools/setup/context-services.ts +9 -17
  191. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  192. package/src/tools/setup/engine.ts +97 -21
  193. package/src/tools/setup/managed-output.ts +51 -0
  194. package/src/tools/setup/mate.ts +86 -11
  195. package/src/tools/setup/plugin.ts +35 -0
  196. package/src/tools/setup/plugins/guidance.ts +7 -4
  197. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  198. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  199. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  200. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  201. package/src/tools/setup/providers/claude.ts +284 -8
  202. package/src/tools/setup/providers/opencode.ts +180 -1
  203. package/src/tools/setup/utils.ts +14 -4
  204. package/src/tools/setup.ts +126 -10
  205. package/wrappers/bin/graphify +52 -4
  206. package/wrappers/bin/openspec +33 -9
  207. package/wrappers/lib/graphify-gate.mjs +8 -0
  208. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  209. package/src/tools/setup/runtime-documents.ts +0 -291
@@ -4,10 +4,16 @@ import path from "node:path";
4
4
 
5
5
  import { resolveForCapability } from "../../../lib/orchestrator/framework-context";
6
6
  import type { CapabilityConfig } from "../../../lib/orchestrator/types";
7
+ import { resolveSecretPolicy } from "../../../runtime/policy";
7
8
  import {
8
9
  GRAPHIFY_OUTPUT_SUBDIR,
9
10
  GRAPHIFY_STORE_SEGMENT,
10
11
  } from "../../../tools/setup/capabilities/graphify";
12
+ import {
13
+ ensureGraphifySecretExclusion,
14
+ graphifySecretExcludeArgs,
15
+ pruneGraphifySecrets,
16
+ } from "../../../tools/setup/capabilities/graphify-secrets";
11
17
  import { ensureUnambiguousCompanion } from "../shared/companion-selection";
12
18
 
13
19
  export interface GraphifyPaths {
@@ -17,6 +23,23 @@ export interface GraphifyPaths {
17
23
 
18
24
  export interface GraphifyCapDeps {
19
25
  ensureUnambiguousCompanion?: (cwd: string) => Promise<boolean>;
26
+ resolveForCapability?: typeof resolveForCapability;
27
+ spawn?: typeof spawnSync;
28
+ }
29
+
30
+ /** Subcommands that read the source tree into the graph; `update` also runs as `<path> --update`. */
31
+ const GRAPHIFY_EXTRACTION_COMMANDS = new Set(["extract", "update", "watch"]);
32
+
33
+ export function isGraphifyExtraction(commandArgs: string[]): boolean {
34
+ return GRAPHIFY_EXTRACTION_COMMANDS.has(commandArgs[0] ?? "") || commandArgs.includes("--update");
35
+ }
36
+
37
+ /** `extract` and `<path> --update` take `--exclude`; the `update` subcommand rejects it. */
38
+ function acceptsExcludeArgs(commandArgs: string[]): boolean {
39
+ return (
40
+ commandArgs[0] === "extract" ||
41
+ (commandArgs[0] !== "update" && commandArgs.includes("--update"))
42
+ );
20
43
  }
21
44
 
22
45
  const GRAPHIFY_PATH_COMMANDS = new Set([
@@ -142,8 +165,11 @@ export async function runGraphifyCapCommand(
142
165
  return;
143
166
  }
144
167
 
145
- const { companionPath, repositoryId, repository, configStore, workingRepoStore } =
146
- await resolveForCapability(process.cwd());
168
+ const resolve = deps.resolveForCapability ?? resolveForCapability;
169
+ const spawn = deps.spawn ?? spawnSync;
170
+ const { companionPath, repositoryId, repository, configStore, workingRepoStore } = await resolve(
171
+ process.cwd(),
172
+ );
147
173
  const config = await configStore.load();
148
174
 
149
175
  const capability = (config.capabilities ?? []).find(
@@ -166,11 +192,28 @@ export async function runGraphifyCapCommand(
166
192
  const paths = deriveGraphifyPaths(companionPath, repositoryId);
167
193
  const commandArgs = deriveGraphifyCommandArgs(args, repo.path, paths);
168
194
 
195
+ /**
196
+ * Every extraction Mate runs leaves secrets out without stopping: the
197
+ * `.graphifyignore` region is rewritten, `--exclude` is passed where graphify
198
+ * takes it, and afterwards any secret that still reached the output is
199
+ * pruned from it. A step that can't run is a warning, never a refusal.
200
+ */
201
+ const extraction = isGraphifyExtraction(commandArgs);
202
+ const secrets = resolveSecretPolicy(companionPath);
203
+ if (extraction) {
204
+ try {
205
+ await ensureGraphifySecretExclusion(repo.path, secrets);
206
+ } catch (error) {
207
+ process.stderr.write(`mate: graphify: warning: ${(error as Error).message}\n`);
208
+ }
209
+ if (acceptsExcludeArgs(commandArgs)) commandArgs.push(...graphifySecretExcludeArgs(secrets));
210
+ }
211
+
169
212
  if (commandArgs[0] === "extract") {
170
213
  await fs.mkdir(paths.graphifyOut, { recursive: true });
171
214
  }
172
215
 
173
- const result = spawnSync("graphify", commandArgs, {
216
+ const result = spawn("graphify", commandArgs, {
174
217
  stdio: "inherit",
175
218
  env: {
176
219
  ...process.env,
@@ -186,4 +229,10 @@ export async function runGraphifyCapCommand(
186
229
  if (result.status !== null) {
187
230
  process.exitCode = result.status;
188
231
  }
232
+
233
+ if (extraction && commandArgs[0] !== "watch") {
234
+ for (const note of await pruneGraphifySecrets(paths.graphifyOut, secrets)) {
235
+ process.stderr.write(`mate: graphify: ${note}\n`);
236
+ }
237
+ }
189
238
  }
@@ -3,6 +3,7 @@ import {
3
3
  hasTokensaveCapability,
4
4
  } from "../../../lib/orchestrator/capabilities";
5
5
  import { resolveForCapability } from "../../../lib/orchestrator/framework-context";
6
+ import { resolveSecretPolicy } from "../../../runtime/policy";
6
7
  import type { CapabilityConfig } from "../../../lib/orchestrator/types";
7
8
  import {
8
9
  ensureTokensaveBranchingPosture,
@@ -29,6 +30,12 @@ export interface SyncCapDeps {
29
30
  onStepDone?: (step: SyncCapStep, ok: boolean) => void;
30
31
  }
31
32
 
33
+ /** The companion's live secret policy decides the secret-file excludes. */
34
+ async function defaultEnsureBranchingPosture(repoPath: string): Promise<void> {
35
+ const { companionPath } = await resolveForCapability(process.cwd());
36
+ await ensureTokensaveBranchingPosture(repoPath, resolveSecretPolicy(companionPath));
37
+ }
38
+
32
39
  async function defaultLoadCapabilities(): Promise<CapabilityConfig[]> {
33
40
  const { configStore } = await resolveForCapability(process.cwd());
34
41
  const config = await configStore.load();
@@ -116,7 +123,7 @@ export async function runIndexCapCommand(args: string[], deps: SyncCapDeps = {})
116
123
  const isTokensaveInitialized = deps.isTokensaveInitialized ?? defaultIsTokensaveInitialized;
117
124
  const ensureStoreExcluded = deps.ensureTokensaveStoreExcluded ?? ensureTokensaveStoreExcluded;
118
125
  const ensureBranchingPosture =
119
- deps.ensureTokensaveBranchingPosture ?? ensureTokensaveBranchingPosture;
126
+ deps.ensureTokensaveBranchingPosture ?? defaultEnsureBranchingPosture;
120
127
  const runGraphify = deps.runGraphify ?? runGraphifyCapCommand;
121
128
  const runTokensave = deps.runTokensave ?? runTokensaveCapCommand;
122
129
 
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { readFileSync } from "node:fs";
3
2
  import path from "node:path";
4
3
 
@@ -6,11 +5,13 @@ import { parse } from "yaml";
6
5
 
7
6
  import { resolveForCapability } from "../../../lib/orchestrator/framework-context";
8
7
  import { getEffectiveCliTools } from "../../../lib/orchestrator/setup-compatibilities";
8
+ import { runOpenSpec, type RunOpenSpec } from "../../../lib/openspec-cli";
9
9
  import type { CapabilityConfig } from "../../../lib/orchestrator/types";
10
10
  import { ensureUnambiguousCompanion } from "../shared/companion-selection";
11
11
 
12
12
  export interface OpenspecCapDeps {
13
13
  ensureUnambiguousCompanion?: (cwd: string) => Promise<boolean>;
14
+ runOpenSpec?: RunOpenSpec;
14
15
  }
15
16
 
16
17
  export function readOpenSpecProjectSchema(companionPath: string): string | undefined {
@@ -79,33 +80,20 @@ export async function runOpenspecCapCommand(
79
80
  readOpenSpecProjectSchema(companionPath),
80
81
  );
81
82
 
83
+ const run = deps.runOpenSpec ?? runOpenSpec;
84
+ const options = { cwd, stdio: "inherit" } as const;
85
+
82
86
  if (forwardedArgs[0] === "init" || forwardedArgs[0] === "update") {
83
- const reset = spawnSync("openspec", ["config", "reset", "--all", "-y"], {
84
- cwd,
85
- stdio: "inherit",
86
- });
87
- if (reset.error) {
88
- process.stderr.write(`Failed to reset openspec config: ${reset.error.message}\n`);
89
- process.exitCode = 1;
90
- return;
91
- }
92
- if (reset.status !== 0) {
93
- process.exitCode = reset.status ?? 1;
87
+ const reset = await run(["config", "reset", "--all", "-y"], options);
88
+ if (reset.code !== 0) {
89
+ // Inherited output already reached the terminal; only Mate's own message is returned.
90
+ if (reset.stderr) process.stderr.write(`${reset.stderr}\n`);
91
+ process.exitCode = reset.code ?? 1;
94
92
  return;
95
93
  }
96
94
  }
97
95
 
98
- const result = spawnSync("openspec", forwardedArgs, {
99
- cwd,
100
- stdio: "inherit",
101
- });
102
- if (result.error) {
103
- process.stderr.write(`Failed to run openspec: ${result.error.message}\n`);
104
- process.exitCode = 1;
105
- return;
106
- }
107
-
108
- if (result.status !== null) {
109
- process.exitCode = result.status;
110
- }
96
+ const result = await run(forwardedArgs, options);
97
+ if (result.stderr) process.stderr.write(`${result.stderr}\n`);
98
+ if (result.code !== null) process.exitCode = result.code;
111
99
  }
@@ -1,55 +1,72 @@
1
- import { CompanionStore } from "../../../lib/orchestrator/repository-link";
2
- import { resolveForLaunch } from "../../../lib/orchestrator/framework-context";
1
+ import path from "node:path";
2
+
3
+ import {
4
+ CompanionResolver,
5
+ type CompanionMatch,
6
+ findRepoLocalRegistryFile,
7
+ } from "../../../lib/orchestrator/repository-link";
8
+ import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
3
9
  import { injectEditorFolder, getPreferredEditorCli } from "../../../lib/orchestrator/editor";
4
- import { ensureUnambiguousCompanion } from "../launch/shared";
10
+ import { WorkingRepoRequiredError } from "../../../lib/orchestrator/types";
11
+ import { resolveProjection } from "../../../runtime/projection";
5
12
 
6
13
  export const workspaceOpenCommandDeps = {
7
- ensureUnambiguousCompanion: () => ensureUnambiguousCompanion(),
8
- resolveLaunchContext: () => resolveForLaunch(process.cwd()),
9
- createStore: (
10
- companionPath: ConstructorParameters<typeof CompanionStore>[0],
11
- workingRepoStore: ConstructorParameters<typeof CompanionStore>[1],
12
- ) => new CompanionStore(companionPath, workingRepoStore),
14
+ cwd: () => process.cwd(),
15
+ /**
16
+ * The repo-local registry root, or the resolved cwd when there is none: the
17
+ * global scan only matches a Working Repository on exact path equality.
18
+ */
19
+ findRepoRoot: async (cwd: string): Promise<string> =>
20
+ (await findRepoLocalRegistryFile(cwd))?.repoRoot ?? path.resolve(cwd),
21
+ /**
22
+ * Every Linked Companion. Not `launchAmbiguityDeps.resolveLinkedCompanions`:
23
+ * `logFailures` warns about each stale pointer the resolver drops, which
24
+ * `wrap` and `session prepare` must not print.
25
+ */
26
+ resolveCompanions: async (cwd: string): Promise<CompanionMatch[]> => {
27
+ const { match, ambiguousMatches } = await new CompanionResolver(
28
+ new GlobalConfigStore(),
29
+ ).resolveWithDiagnostics(cwd, { logFailures: true });
30
+ if (ambiguousMatches.length > 0) return ambiguousMatches;
31
+ return match ? [match] : [];
32
+ },
33
+ resolveProjection,
13
34
  getPreferredEditorCli: () => getPreferredEditorCli(),
14
35
  injectEditorFolder,
15
36
  };
16
37
 
38
+ /**
39
+ * The projected companion first when it still links the repository, then the
40
+ * rest by resolved path, so repeated runs write an identical workspace file.
41
+ */
42
+ function orderCompanionPaths(cwd: string, matches: CompanionMatch[]): string[] {
43
+ const linked = [...new Set(matches.map((match) => path.resolve(match.companionPath)))].toSorted();
44
+ const projectedPath = workspaceOpenCommandDeps.resolveProjection(cwd)?.companionPath;
45
+ const projected = projectedPath ? path.resolve(projectedPath) : null;
46
+ if (!projected || !linked.includes(projected)) return linked;
47
+ return [projected, ...linked.filter((companionPath) => companionPath !== projected)];
48
+ }
49
+
17
50
  /**
18
51
  * @command mate companion open
19
- * @description Updates the managed `.mate/workspace.code-workspace` file for
20
- * the active working repository and companion, then adds both folders to the
21
- * current editor window via `--add`.
22
- * @remarks Throws if the active repository (from `MATE_REPO_ID`) isn't found
23
- * in the registry; sets a non-zero exit code if the editor CLI is unavailable,
24
- * or if the working repo is linked from multiple companions and the ambiguity
25
- * can't be resolved (see {@link ensureUnambiguousCompanion}).
52
+ * @description Updates the managed `.mate/workspace.code-workspace` file with
53
+ * the active working repository and every Linked Companion, then adds those
54
+ * folders to the current editor window via `--add`.
55
+ * @remarks Never selects a companion: no picker, no `MATE_ARTIFACT_PATH`
56
+ * narrowing, and the companion recorded at the Projection Root is left as it
57
+ * was. Stale companion pointers are skipped with a stderr warning. Throws
58
+ * {@link WorkingRepoRequiredError} when no existing Linked Companion remains;
59
+ * sets a non-zero exit code if the editor CLI is unavailable.
26
60
  */
27
61
  export async function runWorkspaceOpenCommand(): Promise<void> {
28
- const unambiguous = await workspaceOpenCommandDeps.ensureUnambiguousCompanion();
29
- if (!unambiguous) {
30
- process.exitCode = 1;
31
- return;
32
- }
33
-
34
- const {
35
- companionPath,
36
- repositoryId,
37
- repository: localRepository,
38
- workingRepoStore,
39
- } = await workspaceOpenCommandDeps.resolveLaunchContext();
40
- const repository =
41
- localRepository ??
42
- (await workspaceOpenCommandDeps
43
- .createStore(companionPath, workingRepoStore)
44
- .getRepository(repositoryId));
45
-
46
- if (!repository) {
47
- throw new Error(`Linked repository not found: ${repositoryId}`);
48
- }
62
+ const cwd = workspaceOpenCommandDeps.cwd();
63
+ const matches = await workspaceOpenCommandDeps.resolveCompanions(cwd);
64
+ if (matches.length === 0) throw new WorkingRepoRequiredError("companion open");
49
65
 
66
+ const repoPath = await workspaceOpenCommandDeps.findRepoRoot(cwd);
50
67
  const injected = await workspaceOpenCommandDeps.injectEditorFolder(
51
- companionPath,
52
- repository.path,
68
+ orderCompanionPaths(cwd, matches),
69
+ repoPath,
53
70
  workspaceOpenCommandDeps.getPreferredEditorCli(),
54
71
  );
55
72
 
@@ -4,6 +4,10 @@ import path from "node:path";
4
4
 
5
5
  import { FRAMEWORK_NAME } from "../../../framework";
6
6
  import { ConfigStore } from "../../../lib/orchestrator/config-store";
7
+ import {
8
+ refreshWorkingClaudeLinks,
9
+ type LinkRefreshResult,
10
+ } from "../../../lib/orchestrator/refresh-working-claude-links";
7
11
  import type { FrameworkConfig } from "../../../lib/orchestrator/types";
8
12
  import { YamlFileStore } from "../../../lib/orchestrator/yaml-file-store";
9
13
  import {
@@ -13,12 +17,13 @@ import {
13
17
  isNewer,
14
18
  } from "../../../lib/update-checker";
15
19
  import { resolveCompanionRuntime } from "../../../runtime/env";
16
- import { syncCompanionFiles } from "../../../tools/setup";
17
- import { MANAGED_MATE_SKILL_NAMES } from "../../../tools/setup/mate";
20
+ import { syncCompanionFiles, type CompanionSyncOptions } from "../../../tools/setup";
21
+ import type { ManagedOutputOwner, ManagedOutputReport } from "../../../tools/setup/plugin";
18
22
  import { runCommand } from "../../../tools/setup/utils";
19
23
  import { confirm } from "../../confirm";
20
24
  import { defaultGitOps, type GitOps } from "../artifact/finish/git";
21
25
  import { resolveCompanionPath } from "./tui";
26
+ import { errorMessage } from "../../../runtime/error-message";
22
27
 
23
28
  /** Snapshot marker for a changed path that no longer exists on disk. */
24
29
  export const DELETED = "<deleted>";
@@ -26,27 +31,17 @@ export const DELETED = "<deleted>";
26
31
  const SKILLS_LOCKFILE = "skills-lock.json";
27
32
 
28
33
  /**
29
- * OpenSpec CLI output (skills and opsx commands): fully regenerated by every update, so
30
- * pre-existing uncommitted edits there are stale output, never local work worth separating.
34
+ * The side the sync reported owning `changedPath`: the path itself or an ancestor
35
+ * was reported. A collapsed `dir/` that only contains a reported entry stays
36
+ * unowned, since porcelain hides what else changed inside it.
31
37
  */
32
- const OPENSPEC_GENERATED = [
33
- /^\.(claude|agents|opencode)\/skills\/openspec-[^/]+(\/|$)/,
34
- /^\.agents\/skills\/\.openspec-target$/,
35
- /^\.claude\/commands\/opsx(\/|$)/,
36
- /^\.opencode\/commands\/opsx-[^/]+$/,
37
- ];
38
-
39
- export function isOpenSpecGenerated(relativePath: string): boolean {
40
- return OPENSPEC_GENERATED.some((pattern) => pattern.test(relativePath));
41
- }
42
-
43
- /** Mate-owned skills: `applyMateSkills` overwrites them wholesale, so a local edit there never survives a sync. */
44
- const MATE_MANAGED_SKILL = new RegExp(
45
- `^\\.(claude|agents|opencode)/skills/(${MANAGED_MATE_SKILL_NAMES.join("|")})(/|$)`,
46
- );
47
-
48
- export function isMateManagedSkill(relativePath: string): boolean {
49
- return MATE_MANAGED_SKILL.test(relativePath);
38
+ export function ownerOf(
39
+ changedPath: string,
40
+ report: ManagedOutputReport,
41
+ ): ManagedOutputOwner | undefined {
42
+ const target = changedPath.replace(/\/+$/, "");
43
+ return report.find((entry) => target === entry.path || target.startsWith(`${entry.path}/`))
44
+ ?.owner;
50
45
  }
51
46
 
52
47
  function frameworkConfigPath(companionPath: string): string {
@@ -96,9 +91,18 @@ export interface CompanionUpdateDeps {
96
91
  getCurrentVersion: () => string;
97
92
  fetchLatestVersion: () => Promise<string>;
98
93
  loadConfig: (companionPath: string) => Promise<FrameworkConfig>;
99
- syncCompanionFiles: (companionPath: string, config: FrameworkConfig) => Promise<void>;
94
+ syncCompanionFiles: (
95
+ companionPath: string,
96
+ config: FrameworkConfig,
97
+ options: CompanionSyncOptions,
98
+ ) => Promise<ManagedOutputReport>;
100
99
  /** Returns true when the file was rewritten. */
101
100
  writeEngineFloor: (companionPath: string, range: string) => Promise<boolean>;
101
+ /** Re-links the companion's Claude skills and commands into its wrapped Working Repositories. */
102
+ refreshWorkingClaudeLinks: (
103
+ companionPath: string,
104
+ config: FrameworkConfig,
105
+ ) => Promise<LinkRefreshResult[]>;
102
106
  hasSkillsLock: (companionPath: string) => Promise<boolean>;
103
107
  runSkillsUpdate: (companionPath: string) => Promise<void>;
104
108
  createGitOps: (companionPath: string) => GitOps;
@@ -114,7 +118,10 @@ export const companionUpdateDeps: CompanionUpdateDeps = {
114
118
  getCurrentVersion,
115
119
  fetchLatestVersion,
116
120
  loadConfig: (companionPath) => new ConfigStore(frameworkConfigPath(companionPath)).load(),
117
- syncCompanionFiles,
121
+ /** The Claude plugin folders it gathers are for a launch; an update ignores them. */
122
+ async syncCompanionFiles(companionPath, config, options) {
123
+ return (await syncCompanionFiles(companionPath, config, undefined, options)).managedOutput;
124
+ },
118
125
  async writeEngineFloor(companionPath, range) {
119
126
  const store = new RawFrameworkConfigStore(frameworkConfigPath(companionPath));
120
127
  const raw = (await store.load()) ?? {};
@@ -123,6 +130,7 @@ export const companionUpdateDeps: CompanionUpdateDeps = {
123
130
  await store.save({ ...raw, engines: { ...engines, [FRAMEWORK_NAME]: range } });
124
131
  return true;
125
132
  },
133
+ refreshWorkingClaudeLinks,
126
134
  hasSkillsLock: (companionPath) =>
127
135
  fs
128
136
  .access(path.join(companionPath, SKILLS_LOCKFILE))
@@ -160,6 +168,7 @@ export async function diffChangedPaths(
160
168
  companionPath: string,
161
169
  git: GitOps,
162
170
  before: Map<string, string>,
171
+ report: ManagedOutputReport,
163
172
  ): Promise<UpdateChanges> {
164
173
  const written: string[] = [];
165
174
  const conflict: string[] = [];
@@ -168,19 +177,17 @@ export async function diffChangedPaths(
168
177
  if (!before.has(changed)) written.push(changed);
169
178
  }
170
179
  const preexisting = await Promise.all(
171
- [...before].map(async ([changed, hash]) => ({
172
- changed,
173
- openspec: isOpenSpecGenerated(changed),
174
- mate: isMateManagedSkill(changed),
175
- unchanged: (await companionUpdateDeps.snapshotPath(companionPath, changed)) === hash,
176
- })),
180
+ [...before].map(async ([changed, hash]) => {
181
+ const after = await companionUpdateDeps.snapshotPath(companionPath, changed);
182
+ return { changed, owner: ownerOf(changed, report), unchanged: after === hash };
183
+ }),
177
184
  );
178
- for (const { changed, openspec, mate, unchanged } of preexisting) {
179
- /** Generated output is committed even when a prior run already wrote identical content. */
180
- if (!openspec && !mate && unchanged) continue;
185
+ for (const { changed, owner, unchanged } of preexisting) {
186
+ // Reported output is committed even when a local edit preceded the sync.
187
+ if (!owner && unchanged) continue;
181
188
  written.push(changed);
182
- if (openspec || (mate && unchanged)) continue;
183
- (mate ? overwritten : conflict).push(changed);
189
+ if (owner === "openspec" || (owner === "mate" && unchanged)) continue;
190
+ (owner === "mate" ? overwritten : conflict).push(changed);
184
191
  }
185
192
  return {
186
193
  written: written.toSorted(),
@@ -200,14 +207,27 @@ function fail(message: string): void {
200
207
  process.exitCode = 1;
201
208
  }
202
209
 
203
- function errorMessage(error: unknown): string {
204
- return error instanceof Error ? error.message : String(error);
205
- }
206
-
207
210
  function listPaths(paths: string[]): string {
208
211
  return paths.map((changed) => ` ${changed}`).join("\n");
209
212
  }
210
213
 
214
+ function reportLinkRefresh(results: LinkRefreshResult[]): void {
215
+ for (const result of results) {
216
+ const where = result.repository?.path ?? "the linked Working Repositories";
217
+ if (result.outcome === "written") {
218
+ process.stdout.write(
219
+ `${FRAMEWORK_NAME}: refreshed Claude skill and command links in ${where}.\n`,
220
+ );
221
+ } else if (result.outcome === "current") {
222
+ process.stdout.write(`${FRAMEWORK_NAME}: Claude links already current in ${where}.\n`);
223
+ } else {
224
+ process.stderr.write(
225
+ `${FRAMEWORK_NAME}: warning: could not refresh Claude links in ${where}: ${result.error}\n`,
226
+ );
227
+ }
228
+ }
229
+ }
230
+
211
231
  /** Version precondition; returns the running version when the update may proceed. */
212
232
  async function checkVersion(): Promise<string | undefined> {
213
233
  const current = companionUpdateDeps.getCurrentVersion();
@@ -313,9 +333,14 @@ export async function runCompanionUpdateCommand(argv: string[]): Promise<void> {
313
333
  return;
314
334
  }
315
335
 
336
+ let config: FrameworkConfig;
337
+ let report: ManagedOutputReport;
316
338
  try {
317
- const config = await companionUpdateDeps.loadConfig(companionPath);
318
- await companionUpdateDeps.syncCompanionFiles(companionPath, config);
339
+ config = await companionUpdateDeps.loadConfig(companionPath);
340
+ // Only this command and setup regenerate OpenSpec output; launches leave it alone.
341
+ report = await companionUpdateDeps.syncCompanionFiles(companionPath, config, {
342
+ regenerateOpenSpec: true,
343
+ });
319
344
  } catch (error) {
320
345
  fail(`companion file synchronization failed: ${errorMessage(error)}`);
321
346
  return;
@@ -331,6 +356,9 @@ export async function runCompanionUpdateCommand(argv: string[]): Promise<void> {
331
356
  }
332
357
  }
333
358
 
359
+ // Working Repositories are outside the companion, so their links never join its commit.
360
+ reportLinkRefresh(await companionUpdateDeps.refreshWorkingClaudeLinks(companionPath, config));
361
+
334
362
  try {
335
363
  await companionUpdateDeps.writeEngineFloor(companionPath, `>=${version}`);
336
364
  } catch (error) {
@@ -338,7 +366,12 @@ export async function runCompanionUpdateCommand(argv: string[]): Promise<void> {
338
366
  return;
339
367
  }
340
368
 
341
- const { written, conflict, overwritten } = await diffChangedPaths(companionPath, git, before);
369
+ const { written, conflict, overwritten } = await diffChangedPaths(
370
+ companionPath,
371
+ git,
372
+ before,
373
+ report,
374
+ );
342
375
  if (written.length === 0) {
343
376
  process.stdout.write(
344
377
  `${FRAMEWORK_NAME}: ${companionPath} is already up to date with ${FRAMEWORK_NAME} ${version}.\n`,