@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
@@ -3,6 +3,8 @@ import crypto from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
+ import { managedBlockMarkers, managedBlockStartPattern } from "../../runtime/managed-block";
7
+ import { removeEmptiedFile, writeAtomicThroughLink } from "../../runtime/write-atomic";
6
8
  import type {
7
9
  InstructionsService,
8
10
  McpServerDescriptor,
@@ -63,13 +65,6 @@ async function saveManifest(
63
65
  // Managed instruction blocks
64
66
  // ---------------------------------------------------------------------------
65
67
 
66
- function blockMarkers(frameworkName: string, blockKey: string): { start: string; end: string } {
67
- return {
68
- start: `<!-- ${frameworkName}:managed:${blockKey} start -->`,
69
- end: `<!-- ${frameworkName}:managed:${blockKey} end -->`,
70
- };
71
- }
72
-
73
68
  export function instructionBlockKey(pluginId: string, content: string): string {
74
69
  const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 12);
75
70
  return `${pluginId}:${hash}`;
@@ -82,7 +77,7 @@ export async function upsertManagedBlock(
82
77
  blockKey: string,
83
78
  content: string,
84
79
  ): Promise<void> {
85
- const { start, end } = blockMarkers(frameworkName, blockKey);
80
+ const { start, end } = managedBlockMarkers(frameworkName, blockKey);
86
81
  let existing = "";
87
82
  try {
88
83
  existing = await fs.readFile(filePath, "utf8");
@@ -93,7 +88,8 @@ export async function upsertManagedBlock(
93
88
  if (existing.includes(start)) return;
94
89
  const separator = existing.length === 0 || existing.endsWith("\n\n") ? "" : "\n";
95
90
  await fs.mkdir(path.dirname(filePath), { recursive: true });
96
- await fs.writeFile(filePath, existing + (existing ? separator : "") + block, "utf8");
91
+ /** A session-start hook may read the file while another session's prepare rewrites it. */
92
+ await writeAtomicThroughLink(filePath, existing + (existing ? separator : "") + block);
97
93
  }
98
94
 
99
95
  /**
@@ -113,11 +109,7 @@ export async function removeManagedBlocksForPlugin(
113
109
  } catch {
114
110
  return;
115
111
  }
116
- const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
117
- const keyPattern = new RegExp(
118
- `<!-- ${escape(frameworkName)}:managed:(${escape(pluginId)}:[0-9a-f]+) start -->`,
119
- "g",
120
- );
112
+ const keyPattern = managedBlockStartPattern(frameworkName, pluginId);
121
113
  const keys = [...existing.matchAll(keyPattern)].map((match) => match[1]);
122
114
  for (const key of keys) {
123
115
  if (keepKeys.has(key)) continue;
@@ -131,7 +123,7 @@ async function removeManagedBlock(
131
123
  frameworkName: string,
132
124
  blockKey: string,
133
125
  ): Promise<void> {
134
- const { start, end } = blockMarkers(frameworkName, blockKey);
126
+ const { start, end } = managedBlockMarkers(frameworkName, blockKey);
135
127
  let existing: string;
136
128
  try {
137
129
  existing = await fs.readFile(filePath, "utf8");
@@ -146,10 +138,10 @@ async function removeManagedBlock(
146
138
  const after = existing.slice(endIdx + end.length).replace(/^\n+/, "\n");
147
139
  const next = (before + after).replace(/^\n+/, "");
148
140
  if (!next.trim()) {
149
- await fs.unlink(filePath).catch(() => {});
141
+ await removeEmptiedFile(filePath).catch(() => {});
150
142
  return;
151
143
  }
152
- await fs.writeFile(filePath, next, "utf8");
144
+ await writeAtomicThroughLink(filePath, next);
153
145
  }
154
146
 
155
147
  // ---------------------------------------------------------------------------
@@ -26,45 +26,40 @@ export interface PluginInstallResult {
26
26
  error?: string;
27
27
  }
28
28
 
29
- /** Runs `npm install` for the shared plugin workspace, honoring the user's ambient registry/auth config. */
30
- function defaultNpmInstall(workspaceRoot: string): { ok: boolean; detail?: string } {
31
- const result = spawnSync("npm", ["install", "--no-audit", "--no-fund", "--silent"], {
32
- cwd: workspaceRoot,
33
- encoding: "utf8",
34
- });
29
+ /**
30
+ * Runs `npm <command>` in the shared plugin workspace, honoring the user's ambient
31
+ * registry/auth config. `--loglevel=error` keeps npm's own error in stderr for the report.
32
+ */
33
+ function runNpm(
34
+ workspaceRoot: string,
35
+ command: string,
36
+ args: string[] = [],
37
+ ): { ok: boolean; detail?: string } {
38
+ const result = spawnSync(
39
+ "npm",
40
+ [command, "--no-audit", "--no-fund", "--loglevel=error", ...args],
41
+ { cwd: workspaceRoot, encoding: "utf8" },
42
+ );
35
43
  if (result.error || result.status !== 0) {
36
44
  return {
37
45
  ok: false,
38
46
  detail:
39
47
  result.error?.message ??
40
- result.stderr?.trim() ??
41
- `npm install exited with ${result.status}`,
48
+ (result.stderr?.trim() || `npm ${command} exited with ${result.status}`),
42
49
  };
43
50
  }
44
51
  return { ok: true };
45
52
  }
46
53
 
54
+ /** Runs `npm install` for the shared plugin workspace. */
55
+ const defaultNpmInstall: NpmInstallRunner = (workspaceRoot) => runNpm(workspaceRoot, "install");
56
+
47
57
  /** Version literals treated as moving targets: re-resolved via `npm update` on every run. */
48
58
  const MOVING_VERSION_TAGS = new Set(["latest", "canary"]);
49
59
 
50
60
  /** Runs `npm update <pkg...>` to force re-resolution of moving-tag-declared plugins every run. */
51
- function defaultNpmUpdate(
52
- workspaceRoot: string,
53
- packages: string[],
54
- ): { ok: boolean; detail?: string } {
55
- const result = spawnSync("npm", ["update", "--no-audit", "--no-fund", "--silent", ...packages], {
56
- cwd: workspaceRoot,
57
- encoding: "utf8",
58
- });
59
- if (result.error || result.status !== 0) {
60
- return {
61
- ok: false,
62
- detail:
63
- result.error?.message ?? result.stderr?.trim() ?? `npm update exited with ${result.status}`,
64
- };
65
- }
66
- return { ok: true };
67
- }
61
+ const defaultNpmUpdate: NpmUpdateRunner = (workspaceRoot, packages) =>
62
+ runNpm(workspaceRoot, "update", packages);
68
63
 
69
64
  async function readInstalledVersion(packageRoot: string): Promise<string | null> {
70
65
  try {
@@ -155,14 +150,15 @@ export async function installDeclaredPlugins(
155
150
 
156
151
  await writeWorkspaceManifest(workspaceRoot, desired);
157
152
  const installOutcome = await runNpmInstall(workspaceRoot);
153
+ // Failure is decided from `ok`, so an empty detail can never read as success.
158
154
  const installFailure = installOutcome.ok
159
155
  ? undefined
160
- : (installOutcome.detail ?? "npm install failed");
156
+ : installOutcome.detail || "npm install failed";
161
157
 
162
158
  let updateFailure: string | undefined;
163
159
  if (movingPackages.length > 0) {
164
160
  const updateOutcome = await runNpmUpdate(workspaceRoot, movingPackages);
165
- updateFailure = updateOutcome.ok ? undefined : (updateOutcome.detail ?? "npm update failed");
161
+ updateFailure = updateOutcome.ok ? undefined : updateOutcome.detail || "npm update failed";
166
162
  }
167
163
 
168
164
  const resolvedVersions = await Promise.all(
@@ -174,7 +170,7 @@ export async function installDeclaredPlugins(
174
170
  return sorted.map((declaration, index) => {
175
171
  const isMoving = MOVING_VERSION_TAGS.has(declaration.version);
176
172
  const failure = installFailure ?? (isMoving ? updateFailure : undefined);
177
- if (failure) {
173
+ if (failure !== undefined) {
178
174
  return { package: declaration.package, status: "failed", error: failure };
179
175
  }
180
176
  const resolvedVersion = resolvedVersions[index];
@@ -2,19 +2,29 @@
2
2
  import { getActiveDistribution } from "../../distribution";
3
3
  import { FRAMEWORK_NAME } from "../../framework";
4
4
  import type { FrameworkConfig } from "../../lib/orchestrator/types";
5
- import type { RenderedRuntimeDocument } from "../../runtime/runtime-document-manifest";
5
+ import { getClaudePluginRoot } from "../../lib/package-paths";
6
+ import type { RenderedRuntimeDocuments } from "../../runtime/runtime-document-manifest";
6
7
  import { collectHostingProviders, ContextServiceMediator } from "./context-services";
7
8
  import type {
8
9
  CapabilityContributionInput,
9
10
  CapabilityPlugin,
10
11
  Plugin,
11
12
  PluginRegistration,
13
+ RuntimeContributionsByRuntime,
12
14
  SetupContext,
13
15
  } from "./plugin";
14
- import { reconcileClaudeContributions } from "./providers/claude";
15
- import { reconcileOpenCodeContributions } from "./providers/opencode";
16
- import { mergeCompanionOpenCodeConfig, renderDocumentsForTarget } from "./runtime-documents";
16
+ import { reconcileClaudeContributions, renderClaudeWorkingDocuments } from "./providers/claude";
17
+ import {
18
+ inspectClaudePluginFolders,
19
+ publishClaudePluginFolders,
20
+ type ClaudePluginFolders,
21
+ } from "./providers/claude-plugin-folders";
22
+ import {
23
+ reconcileOpenCodeContributions,
24
+ renderOpenCodeWorkingDocuments,
25
+ } from "./providers/opencode";
17
26
  import { normalizeRegistration, type NormalizedRegistration } from "./registry";
27
+ import { projectionRepoRoot } from "./surface-target";
18
28
 
19
29
  export interface SetupInstallationPlanAction {
20
30
  phase: Plugin["kind"];
@@ -33,6 +43,11 @@ export interface SetupInstallationOutcome {
33
43
  executedActions: SetupInstallationPlanAction[];
34
44
  skippedActions: SetupInstallationPlanAction[];
35
45
  warnings: string[];
46
+ /**
47
+ * Enabled Capabilities' Claude plugin folders, published outside a hub (a hub
48
+ * has no wrap for plain sessions to load them through); never printed here.
49
+ */
50
+ claudePluginFolders: ClaudePluginFolders;
36
51
  }
37
52
 
38
53
  export function buildSetupInstallationPlan(
@@ -176,9 +191,15 @@ export async function executeSetupInstallationPlan(
176
191
 
177
192
  await mediator.finalize();
178
193
 
179
- await reconcileCapabilityContributions(ctx, plugins, plan);
194
+ const inputsByRuntime = await reconcileCapabilityContributions(ctx, plugins, plan);
195
+ const claudeInputs = inputsByRuntime.get("claude") ?? [];
196
+ const folderOptions = { bundledPluginRoot: getClaudePluginRoot() };
197
+ const claudePluginFolders =
198
+ ctx.scope === "hub"
199
+ ? await inspectClaudePluginFolders(claudeInputs, folderOptions)
200
+ : await publishClaudePluginFolders(claudeInputs, folderOptions);
180
201
 
181
- return { executedActions, skippedActions, warnings };
202
+ return { executedActions, skippedActions, warnings, claudePluginFolders };
182
203
  }
183
204
 
184
205
  // The Runtime Surface of each active Agent Runtime, by runtime id.
@@ -190,6 +211,18 @@ const RUNTIME_SURFACE_RECONCILERS: Record<
190
211
  opencode: reconcileOpenCodeContributions,
191
212
  };
192
213
 
214
+ /**
215
+ * The working-target render of each Runtime Surface, by runtime id, in the
216
+ * order its documents are rendered.
217
+ */
218
+ const RUNTIME_SURFACE_WORKING_RENDERERS: Record<
219
+ string,
220
+ (ctx: SetupContext, inputs: CapabilityContributionInput[]) => Promise<RenderedRuntimeDocuments>
221
+ > = {
222
+ claude: renderClaudeWorkingDocuments,
223
+ opencode: renderOpenCodeWorkingDocuments,
224
+ };
225
+
193
226
  /**
194
227
  * The declared Capability contributions of one pass, keyed by runtime. Shared
195
228
  * by both targets: what a companion pass writes is what a working pass renders.
@@ -198,6 +231,7 @@ async function collectContributionInputs(
198
231
  ctx: SetupContext,
199
232
  plugins: Plugin[],
200
233
  plan: SetupInstallationPlan,
234
+ onContributionError?: ContributionErrorHandler,
201
235
  ): Promise<Map<string, CapabilityContributionInput[]>> {
202
236
  // The plan already resolved enablement (policy, saved selection, package
203
237
  // manager requirements); mirror it instead of recomputing.
@@ -213,7 +247,14 @@ async function collectContributionInputs(
213
247
  const capability = plugin as CapabilityPlugin;
214
248
  if (!capability.getRuntimeContributions) continue;
215
249
  const enabled = enabledByPluginId.get(capability.id) ?? false;
216
- const byRuntime = await capability.getRuntimeContributions(ctx);
250
+ let byRuntime: RuntimeContributionsByRuntime;
251
+ try {
252
+ byRuntime = await capability.getRuntimeContributions(ctx);
253
+ } catch (error) {
254
+ if (!onContributionError) throw error;
255
+ onContributionError(capability.id, error);
256
+ continue;
257
+ }
217
258
  for (const [runtimeId, contributions] of Object.entries(byRuntime)) {
218
259
  if (!contributions) continue;
219
260
  // Inactive runtimes still reconcile — with everything disabled — so a
@@ -237,42 +278,77 @@ async function collectContributionInputs(
237
278
  * Reconcile declared Capability contributions through every active runtime's
238
279
  * Runtime Surface (spec: plugin-engine). Runs after all plugin phases, so
239
280
  * providers are always done first. Disabled capabilities participate with
240
- * `enabled: false` and drive teardown of their managed entries.
281
+ * `enabled: false` and drive teardown of their managed entries. Returns the
282
+ * inputs it reconciled, so the pass derives launch-only data from the same ones.
241
283
  */
242
284
  async function reconcileCapabilityContributions(
243
285
  ctx: SetupContext,
244
286
  plugins: Plugin[],
245
287
  plan: SetupInstallationPlan,
246
- ): Promise<void> {
288
+ ): Promise<Map<string, CapabilityContributionInput[]>> {
247
289
  const inputsByRuntime = await collectContributionInputs(ctx, plugins, plan);
248
290
  for (const [runtimeId, inputs] of inputsByRuntime) {
249
291
  const reconcile = RUNTIME_SURFACE_RECONCILERS[runtimeId];
250
292
  if (!reconcile || inputs.length === 0) continue;
251
293
  await reconcile(ctx, inputs);
252
294
  }
295
+ return inputsByRuntime;
253
296
  }
254
297
 
255
- /** What a render returns: its warnings are data for the caller, never printed here. */
256
- export interface RenderedRuntimeDocuments {
257
- documents: RenderedRuntimeDocument[];
258
- warnings: string[];
298
+ /** Receives one Capability's rejected contributions; that Capability then contributes nothing. */
299
+ export type ContributionErrorHandler = (capabilityId: string, error: unknown) => void;
300
+
301
+ /**
302
+ * One pass's declared Capability contributions by runtime, collected without a
303
+ * reconcile. A rejecting Capability fails the collection unless
304
+ * `onContributionError` takes its error; only diagnostics should pass one.
305
+ */
306
+ export async function collectRuntimeContributionInputs(
307
+ ctx: SetupContext,
308
+ registrations: PluginRegistration[],
309
+ plan: SetupInstallationPlan,
310
+ onContributionError?: ContributionErrorHandler,
311
+ ): Promise<Map<string, CapabilityContributionInput[]>> {
312
+ const plugins = registrations.map((entry) => normalizeRegistration(entry).plugin);
313
+ return collectContributionInputs(ctx, plugins, plan, onContributionError);
259
314
  }
260
315
 
261
316
  /**
262
317
  * The same declarations, rendered rather than written: a working-target pass
263
318
  * produces the documents and the Projection Root places them, so no Runtime
264
- * Surface reaches inside a Working Repository.
319
+ * Surface reaches inside a Working Repository. Refuses before anything renders
320
+ * when the context has no Working Repository: every document is addressed by it.
321
+ * Its warnings are data for the caller, never printed here.
265
322
  */
266
323
  export async function renderRuntimeDocuments(
267
324
  ctx: SetupContext,
268
325
  registrations: PluginRegistration[],
269
326
  plan: SetupInstallationPlan,
270
327
  ): Promise<RenderedRuntimeDocuments> {
271
- const plugins = registrations.map((entry) => normalizeRegistration(entry).plugin);
272
- const documents = renderDocumentsForTarget(
273
- ctx,
274
- await collectContributionInputs(ctx, plugins, plan),
275
- );
276
- if (ctx.target !== "working") return { documents, warnings: [] };
277
- return mergeCompanionOpenCodeConfig(ctx.companionPath, documents);
328
+ projectionRepoRoot(ctx);
329
+ const inputsByRuntime = await collectRuntimeContributionInputs(ctx, registrations, plan);
330
+ const rendered: RenderedRuntimeDocuments = { documents: [], warnings: [] };
331
+ for (const [runtimeId, render] of Object.entries(RUNTIME_SURFACE_WORKING_RENDERERS)) {
332
+ if (!ctx.activeProviders.includes(runtimeId)) continue;
333
+ const { documents, warnings } = await render(ctx, inputsByRuntime.get(runtimeId) ?? []);
334
+ rendered.documents.push(...documents);
335
+ rendered.warnings.push(...warnings);
336
+ }
337
+ return rendered;
338
+ }
339
+
340
+ /**
341
+ * Publishes the enabled Claude plugin folders for a wrap pass, before it
342
+ * renders: writes Mate's marketplace files and removes those of disabled
343
+ * Capabilities. Runs no plugin phase.
344
+ */
345
+ export async function publishRuntimeClaudePluginMarketplaces(
346
+ ctx: SetupContext,
347
+ registrations: PluginRegistration[],
348
+ plan: SetupInstallationPlan,
349
+ ): Promise<ClaudePluginFolders> {
350
+ const inputsByRuntime = await collectRuntimeContributionInputs(ctx, registrations, plan);
351
+ return publishClaudePluginFolders(inputsByRuntime.get("claude") ?? [], {
352
+ bundledPluginRoot: getClaudePluginRoot(),
353
+ });
278
354
  }
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import type {
5
+ ManagedOutputEntry,
6
+ ManagedOutputOwner,
7
+ ManagedOutputRecorder,
8
+ ManagedOutputReport,
9
+ } from "./plugin";
10
+
11
+ export interface ManagedOutputCollector {
12
+ recorder: ManagedOutputRecorder;
13
+ /** Entries inside the companion, deduplicated, as companion-relative POSIX paths. */
14
+ report(): ManagedOutputReport;
15
+ }
16
+
17
+ export function createManagedOutputCollector(companionPath: string): ManagedOutputCollector {
18
+ const entries: ManagedOutputEntry[] = [];
19
+ return {
20
+ recorder: { record: (entry) => entries.push(entry) },
21
+ report() {
22
+ const seen = new Set<string>();
23
+ const report: ManagedOutputEntry[] = [];
24
+ for (const entry of entries) {
25
+ const relative = path.relative(companionPath, entry.path);
26
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) continue;
27
+ const posix = relative.split(path.sep).join("/");
28
+ const key = `${entry.change}\0${entry.owner}\0${posix}`;
29
+ if (seen.has(key)) continue;
30
+ seen.add(key);
31
+ report.push({ ...entry, path: posix });
32
+ }
33
+ return report;
34
+ },
35
+ };
36
+ }
37
+
38
+ /** Removes `target` and records it, only when it existed: a no-op removal claims nothing. */
39
+ export async function removeRecorded(
40
+ target: string,
41
+ owner: ManagedOutputOwner,
42
+ recorder?: ManagedOutputRecorder,
43
+ ): Promise<void> {
44
+ const existed = await fs.lstat(target).then(
45
+ () => true,
46
+ () => false,
47
+ );
48
+ if (!existed) return;
49
+ await fs.rm(target, { recursive: true, force: true });
50
+ recorder?.record({ path: target, change: "removed", owner });
51
+ }
@@ -1,13 +1,20 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { removeRecorded } from "./managed-output";
5
+ import type { ManagedOutputRecorder } from "./plugin";
4
6
  import { pruneEmptyAncestors } from "./utils";
7
+ import { readFrontmatter } from "../../lib/frontmatter";
5
8
 
6
- export const MATE_ARTIFACT_SKILLS = ["mate-artifact-publish"] as const;
7
9
  export const MATE_SKILLS = [
8
- "mate-artifact-publish",
10
+ "mate-clarify-intent",
11
+ "mate-explore-change",
12
+ "mate-propose-change",
13
+ "mate-apply-change",
14
+ "mate-archive-change",
15
+ "mate-publish-artifact",
16
+ "mate-backfill-spec",
9
17
  "mate-create-report",
10
- "mate-openspec-backfill",
11
18
  "mate-interview-me",
12
19
  "mate-grill-me",
13
20
  "mate-grilling",
@@ -17,8 +24,11 @@ export const MATE_SKILLS = [
17
24
  "mate-simplify-code",
18
25
  ] as const;
19
26
  const LEGACY_MATE_SKILLS = ["mate-artifact-finish", "mate-openspec-artifact-finish"] as const;
20
- /** Every Mate-owned skill name a skill root may hold, current or retired. */
21
- export const MANAGED_MATE_SKILL_NAMES = [...MATE_SKILLS, ...LEGACY_MATE_SKILLS] as const;
27
+ /**
28
+ * Renamed without aliases: removed only when Mate wrote them (`metadata.author: mate`),
29
+ * so a user-authored skill that happens to share a retired name is left alone.
30
+ */
31
+ const RETIRED_MATE_SKILLS = ["mate-artifact-publish", "mate-openspec-backfill"] as const;
22
32
 
23
33
  const MATE_SKILLS_SOURCE = path.join(import.meta.dirname, "../../templates/mate-skills");
24
34
 
@@ -83,13 +93,41 @@ A \`diff\` section carries a unified diff patch as produced by \`git diff\`. The
83
93
 
84
94
  A report carrying a mermaid payload loads the pinned, integrity-checked diagram runtime from a CDN; a report without one does not. Offline, each diagram stays readable as its source. Otherwise the page stays a single self-contained file.
85
95
 
96
+ ## Presentation Fields
97
+
98
+ These fields are optional. A document without them stays valid and renders with the defaults.
99
+
100
+ - \`tone\` on a status item: \`ok\`, \`warn\`, \`error\`, \`info\`, or \`neutral\`. It sets the marker's colour and shape. When absent, the tone is inferred from well-known status words such as \`passed\`, \`stale\`, or \`failed\`, and any other text is \`neutral\`.
101
+
102
+ \`\`\`json
103
+ { "label": "Build", "status": "flaky", "tone": "warn" }
104
+ \`\`\`
105
+
106
+ - \`format\` on a \`text\` section: \`plain\` (default, shown verbatim with its line breaks) or \`markdown\`.
107
+
108
+ \`\`\`json
109
+ { "id": "notes", "title": "Notes", "type": "text", "format": "markdown", "content": "- **Renderer** owns layout\\n- See \`highlight.ts\`" }
110
+ \`\`\`
111
+
112
+ - \`caption\` on a \`diagram\` or \`diff\` section: a non-empty Markdown note shown beneath the visual, in the same section and without a heading of its own.
113
+
114
+ \`\`\`json
115
+ { "id": "changes", "title": "Changes", "type": "diff", "patch": "...", "caption": "Moves validation into \`contract.ts\`." }
116
+ \`\`\`
117
+
118
+ Markdown supports paragraphs, \`#\` headings, one-level \`-\`, \`*\`, or \`1.\` lists, \`**strong**\`, \`*emphasis*\`, \`\` \`code\` \`\`, fenced code blocks with syntax highlighting, and \`[text](https://…)\` links. Raw HTML is shown as text, links other than \`http\` or \`https\` are shown as plain text, and any other syntax stays literal.
119
+
120
+ The page follows the reader's colour scheme: Mate brand colours on white in light mode, and on a dark background in dark mode, with highlighted code and mermaid diagrams following the same scheme. It always prints as dark text on white.
121
+
122
+ To mark mermaid nodes, apply the theme classes \`added\`, \`changed\`, and \`removed\` with \`class\` or \`:::\`; they follow the colour scheme. Lines that fix a colour with \`classDef\`, \`style\`, or \`linkStyle\` are dropped from the dark copy, because a fill chosen for a white page hides the labels there.
123
+
86
124
  ## Invocation
87
125
 
88
126
  1. Serialize the complete document to a temporary file or pipe it to stdin.
89
127
  2. Run \`mate report --input <file-or->\`.
90
128
  3. Add \`--json\` when the caller needs normalized JSON instead of browser delivery.
91
129
 
92
- The CLI chooses the delivery surface; deliver only through \`mate report\`, never a hand-written file, server, or browser launch. Inside a Studio terminal it hosts the report in Studio and prints a full link only when Studio has a public address, otherwise it tells the operator to open Studio → Reports; relay that output. Elsewhere, or when Studio is unreachable, it writes self-contained HTML to a unique OS temporary directory and opens it in the default browser. The report includes a visible "Print / Save as PDF" control that calls the browser's native print dialog. If HTML delivery fails, the CLI warns on stderr and emits the complete report document as JSON on stdout.
130
+ The CLI chooses the delivery surface; deliver only through \`mate report\`, never a hand-written file, server, or browser launch. Inside a Studio terminal it hosts the report in Studio and prints a full link only when Studio has a public address, otherwise it tells the operator to open Studio → Reports; relay that output. Elsewhere, or when Studio is unreachable, it writes self-contained HTML to a unique OS temporary directory and opens it in the default browser. The report header carries an icon-only print control, named "Print / Save as PDF", that calls the browser's native print dialog. If HTML delivery fails, the CLI warns on stderr and emits the complete report document as JSON on stdout.
93
131
 
94
132
  The built-in \`mate report\` path collects Mate usage data and adapts it to the same contract and renderer.
95
133
  `;
@@ -133,10 +171,41 @@ async function resolveMateSkillSource(skill: string, tool: string): Promise<stri
133
171
  throw new Error(`Mate skill source not found: ${skill}`);
134
172
  }
135
173
 
136
- export async function applyMateSkills(skillsDir: string, tool: string): Promise<void> {
174
+ /** The `metadata.author` a skill directory's SKILL.md declares, if any. */
175
+ export async function readSkillAuthor(skillDir: string): Promise<unknown> {
176
+ let source: string;
177
+ try {
178
+ source = await fs.readFile(path.join(skillDir, "SKILL.md"), "utf8");
179
+ } catch {
180
+ return undefined;
181
+ }
182
+ const metadata = readFrontmatter(source)?.metadata as { author?: unknown } | null | undefined;
183
+ return metadata?.author;
184
+ }
185
+
186
+ async function isMateAuthored(skillDir: string): Promise<boolean> {
187
+ return (await readSkillAuthor(skillDir)) === "mate";
188
+ }
189
+
190
+ export async function removeRetiredMateSkills(
191
+ skillsDir: string,
192
+ recorder?: ManagedOutputRecorder,
193
+ ): Promise<void> {
194
+ for (const skill of RETIRED_MATE_SKILLS) {
195
+ const dir = path.join(skillsDir, skill);
196
+ if (await isMateAuthored(dir)) await removeRecorded(dir, "mate", recorder);
197
+ }
198
+ }
199
+
200
+ export async function applyMateSkills(
201
+ skillsDir: string,
202
+ tool: string,
203
+ recorder?: ManagedOutputRecorder,
204
+ ): Promise<void> {
137
205
  for (const skill of LEGACY_MATE_SKILLS) {
138
- await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
206
+ await removeRecorded(path.join(skillsDir, skill), "mate", recorder);
139
207
  }
208
+ await removeRetiredMateSkills(skillsDir, recorder);
140
209
  for (const skill of MATE_SKILLS) {
141
210
  const destination = path.join(skillsDir, skill);
142
211
  if (skill === "mate-create-report") {
@@ -145,13 +214,19 @@ export async function applyMateSkills(skillsDir: string, tool: string): Promise<
145
214
  } else {
146
215
  await deployMateSkillDir(await resolveMateSkillSource(skill, tool), destination);
147
216
  }
217
+ recorder?.record({ path: destination, change: "written", owner: "mate" });
148
218
  }
149
219
  }
150
220
 
151
- export async function teardownMateSkills(skillsDir: string, companionPath: string): Promise<void> {
152
- for (const skill of MANAGED_MATE_SKILL_NAMES) {
221
+ export async function teardownMateSkills(
222
+ skillsDir: string,
223
+ companionPath: string,
224
+ recorder?: ManagedOutputRecorder,
225
+ ): Promise<void> {
226
+ await removeRetiredMateSkills(skillsDir, recorder);
227
+ for (const skill of [...MATE_SKILLS, ...LEGACY_MATE_SKILLS]) {
153
228
  try {
154
- await fs.rm(path.join(skillsDir, skill), { recursive: true, force: true });
229
+ await removeRecorded(path.join(skillsDir, skill), "mate", recorder);
155
230
  } catch {
156
231
  /* not present */
157
232
  }
@@ -1,4 +1,5 @@
1
1
  import type { FrameworkConfig, LinkedRepository } from "../../lib/orchestrator/types";
2
+ import type { SessionKind } from "../../runtime/session-kind";
2
3
  import type { InstallRequirement, InstallRequirementContext } from "./install-contract";
3
4
  import type { ClaudeHookGroup } from "./providers/claude-format";
4
5
 
@@ -22,6 +23,11 @@ export interface SetupContext {
22
23
  repoPath?: string;
23
24
  /** Defaults to `companion`, so a caller that names no target is unchanged. */
24
25
  target?: ProjectionTarget;
26
+ /**
27
+ * Asks the OpenSpec capability to regenerate its CLI output in `sync` mode.
28
+ * Only `mate companion update` sets it; launches leave committed output alone.
29
+ */
30
+ regenerateOpenSpec?: boolean;
25
31
  /**
26
32
  * Provider-mediated context services. Always populated when the engine
27
33
  * executes a plugin; optional so hand-built contexts (tests, direct calls)
@@ -30,13 +36,34 @@ export interface SetupContext {
30
36
  mcp?: McpService;
31
37
  instructions?: InstructionsService;
32
38
  templates?: TemplatesService;
39
+ /** Collects the managed output a sync pass touched; absent outside one. */
40
+ managedOutput?: ManagedOutputRecorder;
41
+ }
42
+
43
+ export type ManagedOutputOwner = "mate" | "openspec";
44
+
45
+ /** One path a pass wrote or removed, with the side that owns it. */
46
+ export interface ManagedOutputEntry {
47
+ /** Absolute when recorded; companion-relative POSIX in a {@link ManagedOutputReport}. */
48
+ path: string;
49
+ change: "written" | "removed";
50
+ owner: ManagedOutputOwner;
33
51
  }
34
52
 
53
+ export interface ManagedOutputRecorder {
54
+ record(entry: ManagedOutputEntry): void;
55
+ }
56
+
57
+ /** What a sync pass reports it touched, as companion-relative POSIX paths. */
58
+ export type ManagedOutputReport = readonly ManagedOutputEntry[];
59
+
35
60
  export interface LaunchPreflightContext {
36
61
  companionPath: string;
37
62
  config: FrameworkConfig;
38
63
  repository?: LinkedRepository;
39
64
  providerId: string;
65
+ /** The session the prepare answers for, so a check can skip what that session does not load. */
66
+ sessionKind: SessionKind;
40
67
  }
41
68
 
42
69
  /** Provider-agnostic description of an MCP server to register. */
@@ -188,10 +215,17 @@ export interface AgentDefinitionContribution {
188
215
  content: string;
189
216
  }
190
217
 
218
+ /** A Claude Code plugin folder, declared under the `claude` key. Launch-only: never written to a file. */
219
+ export interface PluginDirectoryContribution {
220
+ /** Absolute path of the folder holding `.claude-plugin/plugin.json`. */
221
+ path: string;
222
+ }
223
+
191
224
  /**
192
225
  * Declarative Agent Runtime contributions of one Capability for one runtime.
193
226
  * The runtime's Runtime Surface reconciles these symmetrically: applied while
194
227
  * the Capability is enabled, removed when it is not, idempotent across runs.
228
+ * `pluginDirectories` is the exception: gathered for the launch, written nowhere.
195
229
  */
196
230
  export interface RuntimeContributions {
197
231
  mcpServers?: McpServerDescriptor[];
@@ -201,6 +235,7 @@ export interface RuntimeContributions {
201
235
  skillTrees?: SkillTreeContribution[];
202
236
  pluginReferences?: PluginReferenceContribution[];
203
237
  agentDefinitions?: AgentDefinitionContribution[];
238
+ pluginDirectories?: PluginDirectoryContribution[];
204
239
  }
205
240
 
206
241
  /**