@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
@@ -6,6 +6,7 @@ import { parse as parseYaml } from "yaml";
6
6
 
7
7
  import { FRAMEWORK_NAME } from "../../framework";
8
8
  import { gitEnvironment } from "../../runtime/companion-git";
9
+ import { companionClaudeMdHasOwnText } from "../../runtime/companion-instructions";
9
10
  import { getActiveDistribution } from "../../distribution";
10
11
  import { hasOpenspecCapability } from "../../lib/orchestrator/capabilities";
11
12
  import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
@@ -13,6 +14,8 @@ import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-sto
13
14
  import { pathIsDirectory } from "../../lib/fs-utils";
14
15
  import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
15
16
  import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
17
+ import { locateOpenSpec, type OpenSpecLocation } from "../../lib/openspec-cli";
18
+ import { MATE_CLAUDE_PLUGIN_NAME } from "../../lib/package-paths";
16
19
  import {
17
20
  describeUnknownAudience,
18
21
  readAudienceSelection,
@@ -23,7 +26,20 @@ import {
23
26
  type PluginInspection,
24
27
  type PluginVerificationFailure,
25
28
  } from "../../tools/setup/dynamic-plugins/verify";
29
+ import { hydrateDynamicPlugins } from "../../tools/setup/dynamic-plugins/hydrate";
30
+ import {
31
+ inspectClaudePluginDirectories,
32
+ type ClaudePluginDirectoryInspection,
33
+ } from "../../tools/setup";
26
34
  import { getRequiredPluginDrift } from "../../tools/setup/policy";
35
+ import {
36
+ CLAUDE_PLUGIN_MANIFEST,
37
+ CLAUDE_PLUGIN_MARKETPLACE,
38
+ type ClaudePluginContributionFailure,
39
+ type ClaudePluginDirectoryProblem,
40
+ type ClaudePluginDirectoryReport,
41
+ type ClaudePluginUnpublishedReason,
42
+ } from "../../tools/setup/providers/claude-plugin-folders";
27
43
  import { resolveCommandOnPath } from "../../tools/setup/utils";
28
44
  import { writeJsonStdout } from "../write-json-stdout";
29
45
  import { printSection, renderKeyValueTable, renderTable } from "./status";
@@ -35,6 +51,12 @@ interface DoctorDeps {
35
51
  /** Returns a member checkout's HEAD commit, or null when unreadable. */
36
52
  gitHead?: (memberPath: string) => string | null;
37
53
  inspectPlugins?: (companionPath: string) => Promise<PluginInspection>;
54
+ /** Loads the companion's declared plugins so their Capabilities contribute. */
55
+ hydratePlugins?: (companionPath: string) => Promise<void>;
56
+ inspectClaudePlugins?: (
57
+ companionPath: string,
58
+ config: FrameworkConfig,
59
+ ) => Promise<ClaudePluginDirectoryInspection>;
38
60
  /** Environment `MATE_AUDIENCE` is read from; defaults to the process environment. */
39
61
  env?: Record<string, string | undefined>;
40
62
  }
@@ -46,6 +68,8 @@ export interface DoctorToolInstallation {
46
68
  source: string;
47
69
  status: "ok" | "missing";
48
70
  path: string | null;
71
+ /** Set for a tool Mate ships itself, such as the OpenSpec CLI. */
72
+ version?: string;
49
73
  }
50
74
 
51
75
  export interface DoctorHubMember {
@@ -79,6 +103,17 @@ export interface DoctorReport {
79
103
  engineRequirement?: { range: string; ok: boolean; detail: string };
80
104
  /** Present when the companion's `openspec/config.yaml` has `rules` keys OpenSpec ignores. */
81
105
  openspecRuleWarnings?: Array<{ key: string; message: string }>;
106
+ /** Present when the companion `CLAUDE.md` holds text no Working Repository session receives. */
107
+ companionInstructionWarnings?: Array<{ file: string; message: string }>;
108
+ /**
109
+ * Enabled Capabilities' Claude plugin folders, which load only with `mate claude`; absent when
110
+ * none. `problem` names why `mate claude` leaves a folder out: `not-absolute`,
111
+ * `missing-manifest`, `bundled-mate-plugin` (Mate's own plugin, which every managed launch
112
+ * loads already) or `mate-plugin-name` (its manifest uses Mate's plugin name).
113
+ */
114
+ claudePlugins?: ClaudePluginDirectoryReport[];
115
+ /** Capabilities whose contributions failed, so their Claude plugin folders are unknown; absent when none. */
116
+ claudePluginFailures?: ClaudePluginContributionFailure[];
82
117
  hub?: { members: DoctorHubMember[] };
83
118
  resolutionFailures: Array<{ companionPath: string; message: string }>;
84
119
  }
@@ -87,6 +122,8 @@ interface ToolCheck {
87
122
  name: string;
88
123
  command: string;
89
124
  source: string;
125
+ /** Locates a tool Mate ships instead of looking it up on PATH. */
126
+ bundled?: () => OpenSpecLocation;
90
127
  }
91
128
 
92
129
  /** Doctor's view of the shared root kind: registry/env resolution means cwd is a working repo. */
@@ -127,7 +164,12 @@ function getSelectedToolChecks(config: {
127
164
  checks.push({ name: "uv", command: "uv", source: "package manager" });
128
165
  }
129
166
  if (selectedCapabilities.has("openspec")) {
130
- checks.push({ name: "openspec", command: "openspec", source: "capability" });
167
+ checks.push({
168
+ name: "openspec",
169
+ command: "openspec",
170
+ source: "capability",
171
+ bundled: locateOpenSpec,
172
+ });
131
173
  }
132
174
  if (selectedCapabilities.has("tokensave")) {
133
175
  checks.push({ name: "tokensave", command: "tokensave", source: "capability" });
@@ -147,6 +189,21 @@ function collectToolInstallations(
147
189
  pathValue: string,
148
190
  ): DoctorToolInstallation[] {
149
191
  return getSelectedToolChecks(config).map((check) => {
192
+ if (check.bundled) {
193
+ const source = `${check.source}, bundled with ${FRAMEWORK_NAME}`;
194
+ const location = check.bundled();
195
+ if ("missing" in location) {
196
+ return { tool: check.name, source, status: "missing", path: null };
197
+ }
198
+ // An unreadable manifest only loses the version; the CLI itself resolved.
199
+ return {
200
+ tool: check.name,
201
+ source,
202
+ status: "ok",
203
+ path: location.bin,
204
+ ...(location.version ? { version: location.version } : {}),
205
+ };
206
+ }
150
207
  const resolved = resolveCommandOnPath(check.command, pathValue);
151
208
  return {
152
209
  tool: check.name,
@@ -309,6 +366,19 @@ async function collectOpenSpecRuleWarnings(
309
366
  }));
310
367
  }
311
368
 
369
+ function collectCompanionInstructionWarnings(
370
+ companionPath: string,
371
+ ): NonNullable<DoctorReport["companionInstructionWarnings"]> {
372
+ if (!companionClaudeMdHasOwnText(companionPath)) return [];
373
+ const file = path.join(companionPath, "CLAUDE.md");
374
+ return [
375
+ {
376
+ file,
377
+ message: `${file} holds text besides @AGENTS.md and Mate-managed blocks. That text does not reach Working Repository sessions, only sessions started in the companion. Move rules meant for every session into ${path.join(companionPath, "AGENTS.md")}.`,
378
+ },
379
+ ];
380
+ }
381
+
312
382
  export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<DoctorReport> {
313
383
  const cwd = path.resolve(deps.cwd ?? process.cwd());
314
384
  const pathValue = deps.pathValue ?? process.env.PATH ?? "";
@@ -356,6 +426,23 @@ export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<Doctor
356
426
  if (warnings.length > 0) report.openspecRuleWarnings = warnings;
357
427
  }
358
428
 
429
+ if (root.rootPath && (kind === "working" || kind === "companion")) {
430
+ const warnings = collectCompanionInstructionWarnings(root.rootPath);
431
+ if (warnings.length > 0) report.companionInstructionWarnings = warnings;
432
+ }
433
+
434
+ if (root.rootPath && root.config && (kind === "working" || kind === "companion")) {
435
+ /** Doctor reports plugin failures below; hydration supplies their contributions. */
436
+ const hydrate =
437
+ deps.hydratePlugins ??
438
+ ((companionPath) => hydrateDynamicPlugins({ companionPath, env, warn: () => {} }));
439
+ await hydrate(root.rootPath);
440
+ const inspectClaudePlugins = deps.inspectClaudePlugins ?? inspectClaudePluginDirectories;
441
+ const inspection = await inspectClaudePlugins(root.rootPath, root.config);
442
+ if (inspection.plugins.length > 0) report.claudePlugins = inspection.plugins;
443
+ if (inspection.failures.length > 0) report.claudePluginFailures = inspection.failures;
444
+ }
445
+
359
446
  if (root.rootPath && root.config) {
360
447
  const inspect =
361
448
  deps.inspectPlugins ?? ((companionPath) => inspectDeclaredPlugins(companionPath, { env }));
@@ -386,7 +473,12 @@ function renderToolInstallations(installations: DoctorToolInstallation[]): strin
386
473
  }
387
474
  return renderTable(
388
475
  ["Tool", "Source", "Status", "Path"],
389
- installations.map((item) => [item.tool, item.source, item.status, item.path ?? ""]),
476
+ installations.map((item) => [
477
+ item.version ? `${item.tool} ${item.version}` : item.tool,
478
+ item.source,
479
+ item.status,
480
+ item.path ?? "",
481
+ ]),
390
482
  );
391
483
  }
392
484
 
@@ -404,6 +496,65 @@ function renderHubMembers(members: DoctorHubMember[]): string {
404
496
  );
405
497
  }
406
498
 
499
+ /** The Status column's text for each reason a folder is left out. */
500
+ const CLAUDE_PLUGIN_PROBLEM_STATUS: Record<ClaudePluginDirectoryProblem, string> = {
501
+ "not-absolute": "not an absolute path",
502
+ "bundled-mate-plugin": "Mate's bundled plugin, already loaded",
503
+ "missing-manifest": `missing ${CLAUDE_PLUGIN_MANIFEST}`,
504
+ "mate-plugin-name": `uses Mate's plugin name ${MATE_CLAUDE_PLUGIN_NAME}`,
505
+ };
506
+
507
+ function claudePluginStatus(plugin: ClaudePluginDirectoryReport): string {
508
+ return plugin.problem ? CLAUDE_PLUGIN_PROBLEM_STATUS[plugin.problem] : "present";
509
+ }
510
+
511
+ /** The Plain Sessions column's text for each reason a loadable folder is not published. */
512
+ const CLAUDE_PLUGIN_UNPUBLISHED_STATUS: Record<ClaudePluginUnpublishedReason, string> = {
513
+ "foreign-marketplace": `no: ${CLAUDE_PLUGIN_MARKETPLACE} is not Mate's and does not list it`,
514
+ "invalid-marketplace-name": "no: its name cannot form a marketplace name",
515
+ "name-conflict": "no: its marketplace name is already in use",
516
+ "missing-marketplace": `no: ${CLAUDE_PLUGIN_MARKETPLACE} could not be written`,
517
+ };
518
+
519
+ function claudePluginPlainSessions(plugin: ClaudePluginDirectoryReport): string {
520
+ if (plugin.marketplace) return `marketplace ${plugin.marketplace}`;
521
+ if (plugin.unpublishedReason) return CLAUDE_PLUGIN_UNPUBLISHED_STATUS[plugin.unpublishedReason];
522
+ return "no";
523
+ }
524
+
525
+ /** A table for each list present, then a note on where plain sessions load them. */
526
+ function renderClaudePlugins(
527
+ plugins: ClaudePluginDirectoryReport[] | undefined,
528
+ failures: ClaudePluginContributionFailure[] | undefined,
529
+ ): string {
530
+ const tables: string[] = [];
531
+ if (plugins) {
532
+ tables.push(
533
+ renderTable(
534
+ ["Capability", "Plugin Folder", "Status", "Plain Sessions"],
535
+ plugins.map((plugin) => [
536
+ plugin.capability,
537
+ plugin.path,
538
+ claudePluginStatus(plugin),
539
+ claudePluginPlainSessions(plugin),
540
+ ]),
541
+ ),
542
+ );
543
+ }
544
+ if (failures) {
545
+ tables.push(
546
+ renderTable(
547
+ ["Capability", "Error"],
548
+ failures.map((failure) => [failure.capability, failure.reason]),
549
+ ),
550
+ );
551
+ }
552
+ return [
553
+ ...tables.flatMap((table) => [table, ""]),
554
+ `\`${FRAMEWORK_NAME} claude\` loads every present plugin. Plain \`claude\` sessions load one with a marketplace, and only in a wrapped repository that Claude Code trusts.`,
555
+ ].join("\n");
556
+ }
557
+
407
558
  function renderHumanReport(report: DoctorReport): void {
408
559
  console.log("Mate Doctor");
409
560
  console.log("");
@@ -477,6 +628,12 @@ function renderHumanReport(report: DoctorReport): void {
477
628
  if (report.engineRequirement) {
478
629
  printSection("Version Requirement", report.engineRequirement.detail);
479
630
  }
631
+ if (report.claudePlugins || report.claudePluginFailures) {
632
+ printSection(
633
+ "Claude Plugins",
634
+ renderClaudePlugins(report.claudePlugins, report.claudePluginFailures),
635
+ );
636
+ }
480
637
  if (report.openspecRuleWarnings) {
481
638
  printSection(
482
639
  "OpenSpec Config",
@@ -486,6 +643,15 @@ function renderHumanReport(report: DoctorReport): void {
486
643
  ),
487
644
  );
488
645
  }
646
+ if (report.companionInstructionWarnings) {
647
+ printSection(
648
+ "Companion Instructions",
649
+ renderList(
650
+ report.companionInstructionWarnings.map((warning) => warning.message),
651
+ "",
652
+ ),
653
+ );
654
+ }
489
655
 
490
656
  printSection(
491
657
  "Resolution Failures",
@@ -506,7 +672,8 @@ function renderHumanReport(report: DoctorReport): void {
506
672
  * capabilities and tool checks for companions, member health for hubs.
507
673
  * Declared plugins of the effective set (base plus the `MATE_AUDIENCE`
508
674
  * audience) are verified strictly (installed, loadable); any failure is named
509
- * on stderr and exits non-zero, as is an undeclared `MATE_AUDIENCE`.
675
+ * on stderr and exits non-zero, as is an undeclared `MATE_AUDIENCE` or a
676
+ * Capability whose runtime contributions fail to load.
510
677
  * `--json` emits the same report as one JSON document.
511
678
  */
512
679
  export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
@@ -518,6 +685,12 @@ export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {
518
685
  for (const failure of failures) {
519
686
  process.stderr.write(`${FRAMEWORK_NAME}: plugin ${failure.package}: ${failure.reason}\n`);
520
687
  }
688
+ for (const failure of report.claudePluginFailures ?? []) {
689
+ process.stderr.write(
690
+ `${FRAMEWORK_NAME}: capability ${failure.capability}: ${failure.reason}\n`,
691
+ );
692
+ process.exitCode = 1;
693
+ }
521
694
  if (report.audienceError) {
522
695
  process.stderr.write(`${FRAMEWORK_NAME}: ${report.audienceError}\n`);
523
696
  process.exitCode = 1;
@@ -32,6 +32,8 @@ function withDeclaration(
32
32
  );
33
33
  }
34
34
 
35
+ const INSTALL_USAGE = `usage: ${FRAMEWORK_NAME} plugin install <package>[@version]`;
36
+
35
37
  export interface PluginInstallCommandDeps {
36
38
  cwd?: string;
37
39
  installDeps?: PluginInstallDeps;
@@ -50,10 +52,13 @@ export async function runPluginInstallCommand(
50
52
  deps: PluginInstallCommandDeps = {},
51
53
  ): Promise<boolean> {
52
54
  const spec = argv[0];
53
- if (!spec) {
54
- process.stderr.write(
55
- `${FRAMEWORK_NAME}: usage: ${FRAMEWORK_NAME} plugin install <package>[@version]\n`,
56
- );
55
+ if (spec === "-h" || spec === "--help") {
56
+ process.stdout.write(`${INSTALL_USAGE}\n`);
57
+ return true;
58
+ }
59
+ if (!spec || spec.startsWith("-")) {
60
+ const problem = spec ? `unknown option ${spec}; ` : "";
61
+ process.stderr.write(`${FRAMEWORK_NAME}: ${problem}${INSTALL_USAGE}\n`);
57
62
  process.exitCode = 1;
58
63
  return false;
59
64
  }
@@ -1,13 +1,14 @@
1
1
  import { usage } from "../../usage";
2
- import { runPluginInstallCommand } from "./install";
2
+ import { type PluginInstallCommandDeps, runPluginInstallCommand } from "./install";
3
3
 
4
4
  export async function runPluginCommand(
5
5
  subcommand: string | undefined,
6
6
  argv: string[],
7
+ deps: PluginInstallCommandDeps = {},
7
8
  ): Promise<void> {
8
9
  switch (subcommand) {
9
10
  case "install":
10
- await runPluginInstallCommand(argv);
11
+ if (!(await runPluginInstallCommand(argv, deps))) process.exitCode = 1;
11
12
  return;
12
13
  default:
13
14
  console.error(`Unknown plugin command: ${subcommand ?? ""}`);
@@ -5,13 +5,21 @@ import type {
5
5
  ReportKeyValue,
6
6
  ReportMetric,
7
7
  ReportSection,
8
+ ReportStatusTone,
8
9
  } from "./types";
9
10
  import { REPORT_DOCUMENT_VERSION } from "./types";
10
11
 
12
+ /** Collector status strings mapped to tones, so the built-in report never relies on inference. */
13
+ const TOOL_STATUS_TONES: Record<string, ReportStatusTone> = {
14
+ ok: "ok",
15
+ "no data": "warn",
16
+ "no window data": "warn",
17
+ "not installed": "neutral",
18
+ "not enabled": "neutral",
19
+ };
20
+
11
21
  export function reportDataToDocument(data: ReportData): ReportDocument {
12
22
  const metadata: ReportKeyValue[] = [
13
- { label: "Window", value: `Last ${data.days} days` },
14
- { label: "Generated", value: data.generatedAt },
15
23
  { label: "Working repository", value: data.workingRepoPath },
16
24
  { label: "Companion repository", value: data.companionRepoPath },
17
25
  { label: "Active agents", value: data.activeAgents.join(", ") || "None" },
@@ -57,6 +65,7 @@ export function reportDataToDocument(data: ReportData): ReportDocument {
57
65
  items: data.toolStatus.map((entry) => ({
58
66
  label: entry.name,
59
67
  status: entry.status,
68
+ tone: TOOL_STATUS_TONES[entry.status] ?? "neutral",
60
69
  })),
61
70
  },
62
71
  ];
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  REPORT_DOCUMENT_VERSION,
3
+ REPORT_STATUS_TONES,
4
+ REPORT_TEXT_FORMATS,
3
5
  type ReportDiagramSection,
4
6
  type ReportDiffSection,
5
7
  type ReportDocument,
@@ -46,6 +48,21 @@ function requiredString(
46
48
  return true;
47
49
  }
48
50
 
51
+ function optionalEnum(
52
+ value: unknown,
53
+ allowed: readonly string[],
54
+ path: string,
55
+ issues: ReportValidationIssue[],
56
+ ): void {
57
+ if (value === undefined || allowed.includes(value as string)) return;
58
+ const list = `${allowed.slice(0, -1).join(", ")}${allowed.length > 2 ? "," : ""} or ${allowed.at(-1)}`;
59
+ issues.push({ path, message: `must be one of ${list}` });
60
+ }
61
+
62
+ function optionalCaption(value: unknown, path: string, issues: ReportValidationIssue[]): void {
63
+ if (value !== undefined) requiredString(value, path, issues);
64
+ }
65
+
49
66
  function validateKeyValues(value: unknown, path: string, issues: ReportValidationIssue[]): boolean {
50
67
  if (!Array.isArray(value)) {
51
68
  issues.push({ path, message: "must be an array" });
@@ -112,6 +129,7 @@ function validateStatuses(value: unknown, path: string, issues: ReportValidation
112
129
  if (item.detail !== undefined && typeof item.detail !== "string") {
113
130
  issues.push({ path: `${itemPath}.detail`, message: "must be a string" });
114
131
  }
132
+ optionalEnum(item.tone, REPORT_STATUS_TONES, `${itemPath}.tone`, issues);
115
133
  });
116
134
  return true;
117
135
  }
@@ -209,14 +227,17 @@ function validateSection(value: unknown, path: string, issues: ReportValidationI
209
227
  if (typeof value.content !== "string") {
210
228
  issues.push({ path: `${path}.content`, message: "must be a string" });
211
229
  }
230
+ optionalEnum(value.format, REPORT_TEXT_FORMATS, `${path}.format`, issues);
212
231
  return;
213
232
  case "diagram":
214
233
  validateDiagram(value, path, issues);
234
+ optionalCaption(value.caption, `${path}.caption`, issues);
215
235
  return;
216
236
  case "diff":
217
237
  if (typeof value.patch !== "string") {
218
238
  issues.push({ path: `${path}.patch`, message: "must be a string" });
219
239
  }
240
+ optionalCaption(value.caption, `${path}.caption`, issues);
220
241
  return;
221
242
  default:
222
243
  issues.push({
@@ -1,5 +1,12 @@
1
- import { normalizeLanguage, renderNodesToHtml, renderTokens, tokenize } from "@tanstack/highlight";
2
- import { createThemeCss } from "@tanstack/highlight/theme";
1
+ import {
2
+ type HighlightRenderNode,
3
+ normalizeLanguage,
4
+ renderNodesToHtml,
5
+ renderTokens,
6
+ tokenize,
7
+ } from "@tanstack/highlight";
8
+ import { createThemeCss, createThemeRule } from "@tanstack/highlight/theme";
9
+ import githubDark from "@tanstack/highlight/themes/github-dark";
3
10
  import githubLight from "@tanstack/highlight/themes/github-light";
4
11
 
5
12
  /** Sole boundary against `@tanstack/highlight`, so an upstream API break stays a single-file fix. */
@@ -18,7 +25,30 @@ export function highlightInline(code: string, language: string | undefined): str
18
25
  return renderNodesToHtml(renderTokens(tokens));
19
26
  }
20
27
 
21
- /** Token colours as an inlinable stylesheet; carries no script, font, or external reference. */
28
+ /**
29
+ * The same tokens as structure rather than markup, for a renderer that builds elements
30
+ * and so never holds a markup string. An unknown language yields plain text nodes.
31
+ */
32
+ export function highlightNodes(
33
+ code: string,
34
+ language: string | undefined,
35
+ ): ReadonlyArray<HighlightRenderNode> {
36
+ const { tokens } = tokenize(code, { lang: resolveHighlightLanguage(language) });
37
+ return renderTokens(tokens);
38
+ }
39
+
40
+ /**
41
+ * Token colours as an inlinable stylesheet; carries no script, font, or external reference.
42
+ * Light by default, dark on screen when the reader prefers it or the page sets
43
+ * `data-scheme="dark"`, and light in print because both dark rules are screen-only.
44
+ */
22
45
  export function highlightStyleBlock(codeBlockSelector: string): string {
23
- return createThemeCss({ light: githubLight, codeBlockSelector });
46
+ const light = createThemeCss({ light: githubLight, codeBlockSelector });
47
+ return `${light}
48
+ @media screen and (prefers-color-scheme: dark) {
49
+ ${createThemeRule(':root:not([data-scheme="light"])', githubDark)}
50
+ }
51
+ @media screen {
52
+ ${createThemeRule(':root[data-scheme="dark"]', githubDark)}
53
+ }`;
24
54
  }
@@ -0,0 +1,110 @@
1
+ import { highlightInline } from "./highlight";
2
+
3
+ /**
4
+ * Safe Markdown subset for report text and captions: paragraphs, headings shifted
5
+ * below the section heading, one-level lists, code spans, strong, emphasis,
6
+ * `http(s)` links, and highlighted fenced code. Every literal is escaped before
7
+ * markup is added, so raw HTML and any other syntax degrade to visible text.
8
+ */
9
+
10
+ export function escapeHTML(value: string): string {
11
+ return value.replace(/[&<>"']/g, (character) => {
12
+ switch (character) {
13
+ case "&":
14
+ return "&amp;";
15
+ case "<":
16
+ return "&lt;";
17
+ case ">":
18
+ return "&gt;";
19
+ case '"':
20
+ return "&quot;";
21
+ default:
22
+ return "&#39;";
23
+ }
24
+ });
25
+ }
26
+
27
+ const FENCE = /^\s{0,3}```\s*([\w+#.-]*)\s*$/;
28
+ const HEADING = /^\s{0,3}(#{1,6})\s+(.*)$/;
29
+ const LIST_ITEM = /^\s{0,3}([-*]|\d+[.)])\s+(.*)$/;
30
+ const INLINE_TOKEN = /`([^`\n]+)`|\[([^\]\n]+)\]\(([^()\s]+)\)/g;
31
+ const SAFE_URL = /^https?:\/\/\S+$/i;
32
+
33
+ /** Emphasis runs over already-escaped text, which carries no `*` or `_` of its own. */
34
+ function emphasize(escaped: string): string {
35
+ return escaped
36
+ .replace(/\*\*(?=\S)([^*]*?\S)\*\*/g, "<strong>$1</strong>")
37
+ .replace(/\*(?=\S)([^*]*?\S)\*/g, "<em>$1</em>")
38
+ .replace(/(^|\W)_(?=\S)([^_]*?\S)_(?!\w)/g, "$1<em>$2</em>");
39
+ }
40
+
41
+ function renderInline(source: string): string {
42
+ let html = "";
43
+ let last = 0;
44
+ for (const match of source.matchAll(INLINE_TOKEN)) {
45
+ html += emphasize(escapeHTML(source.slice(last, match.index)));
46
+ const [whole, code, label, url] = match;
47
+ if (code !== undefined) {
48
+ html += `<code>${escapeHTML(code)}</code>`;
49
+ } else if (SAFE_URL.test(url)) {
50
+ html += `<a href="${escapeHTML(url)}" rel="noopener noreferrer">${emphasize(escapeHTML(label))}</a>`;
51
+ } else {
52
+ html += emphasize(escapeHTML(whole));
53
+ }
54
+ last = match.index + whole.length;
55
+ }
56
+ return html + emphasize(escapeHTML(source.slice(last)));
57
+ }
58
+
59
+ function renderFence(code: string[], language: string): string {
60
+ return `<pre class="report-code">${highlightInline(code.join("\n"), language || undefined)}</pre>`;
61
+ }
62
+
63
+ export function renderMarkdownSubset(source: string): string {
64
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
65
+ const blocks: string[] = [];
66
+ let paragraph: string[] = [];
67
+ let list: { tag: "ul" | "ol"; items: string[] } | undefined;
68
+
69
+ const flush = (): void => {
70
+ if (paragraph.length) blocks.push(`<p>${paragraph.map(renderInline).join("\n")}</p>`);
71
+ if (list) blocks.push(`<${list.tag}>${list.items.join("")}</${list.tag}>`);
72
+ paragraph = [];
73
+ list = undefined;
74
+ };
75
+
76
+ for (let index = 0; index < lines.length; index++) {
77
+ const line = lines[index];
78
+ const fence = FENCE.exec(line);
79
+ if (fence) {
80
+ flush();
81
+ const code: string[] = [];
82
+ while (++index < lines.length && !FENCE.test(lines[index])) code.push(lines[index]);
83
+ blocks.push(renderFence(code, fence[1]));
84
+ continue;
85
+ }
86
+ if (line.trim() === "") {
87
+ flush();
88
+ continue;
89
+ }
90
+ const heading = HEADING.exec(line);
91
+ if (heading) {
92
+ flush();
93
+ const level = Math.min(heading[1].length + 2, 6);
94
+ blocks.push(`<h${level}>${renderInline(heading[2].trim())}</h${level}>`);
95
+ continue;
96
+ }
97
+ const item = LIST_ITEM.exec(line);
98
+ if (item) {
99
+ const tag = /\d/.test(item[1]) ? "ol" : "ul";
100
+ if (paragraph.length || (list && list.tag !== tag)) flush();
101
+ list ??= { tag, items: [] };
102
+ list.items.push(`<li>${renderInline(item[2])}</li>`);
103
+ continue;
104
+ }
105
+ if (list) flush();
106
+ paragraph.push(line);
107
+ }
108
+ flush();
109
+ return blocks.join("");
110
+ }