@uniqbit/mate-core 0.15.3 → 0.15.4-canary.10

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 (94) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +4 -0
  2. package/claude-plugin/hooks/artifact-finish-nudge.mjs +8 -0
  3. package/claude-plugin/hooks/hooks.json +37 -0
  4. package/claude-plugin/hooks/session-banner.mjs +8 -0
  5. package/claude-plugin/hooks/ts-loader.mjs +28 -0
  6. package/claude-plugin/hooks/validate-artifact-path.mjs +8 -0
  7. package/package.json +4 -2
  8. package/src/cli/commands/artifact/finish/openspec.ts +1 -5
  9. package/src/cli/commands/companion/hub.ts +99 -0
  10. package/src/cli/commands/companion/link.ts +0 -5
  11. package/src/cli/commands/companion/tui.ts +3 -2
  12. package/src/cli/commands/doctor.ts +254 -170
  13. package/src/cli/commands/install.ts +18 -14
  14. package/src/cli/commands/launch/shared.ts +4 -4
  15. package/src/cli/commands/plugin/install.ts +94 -0
  16. package/src/cli/commands/plugin/plugin.ts +17 -0
  17. package/src/cli/commands/setup.ts +4 -6
  18. package/src/cli/commands/update.ts +9 -11
  19. package/src/cli/companion-link-wizard.tsx +22 -78
  20. package/src/cli/main.ts +57 -13
  21. package/src/cli/plugin-commands.ts +4 -4
  22. package/src/cli/repo-list-table.tsx +2 -7
  23. package/src/cli/usage.ts +7 -3
  24. package/src/distribution.ts +3 -5
  25. package/src/framework.ts +4 -36
  26. package/src/hooks/artifact-finish-nudge.ts +212 -0
  27. package/src/hooks/session-banner.ts +25 -0
  28. package/src/hooks/validate-artifact-path.ts +236 -0
  29. package/src/index.ts +4 -0
  30. package/src/lib/context-mode-package.ts +5 -3
  31. package/src/lib/install.ts +31 -43
  32. package/src/lib/orchestrator/adapters/base.ts +7 -8
  33. package/src/lib/orchestrator/adapters/claude.ts +16 -0
  34. package/src/lib/orchestrator/adapters/opencode.ts +10 -60
  35. package/src/lib/orchestrator/companion-hub.ts +380 -0
  36. package/src/lib/orchestrator/companion-store.ts +1 -35
  37. package/src/lib/orchestrator/config-store.ts +94 -7
  38. package/src/lib/orchestrator/editor.ts +4 -20
  39. package/src/lib/orchestrator/framework-context.ts +119 -24
  40. package/src/lib/orchestrator/global-config-store.ts +1 -5
  41. package/src/lib/orchestrator/launcher.ts +3 -9
  42. package/src/lib/orchestrator/migration.ts +0 -23
  43. package/src/lib/orchestrator/opencode-guidance.ts +1 -2
  44. package/src/lib/orchestrator/repo-local-registry.ts +3 -1
  45. package/src/lib/orchestrator/root-context.ts +117 -0
  46. package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
  47. package/src/lib/orchestrator/setup-preflight.ts +5 -18
  48. package/src/lib/orchestrator/types.ts +27 -19
  49. package/src/lib/orchestrator/working-repo-store.ts +9 -3
  50. package/src/lib/package-paths.ts +37 -0
  51. package/src/lib/update-checker.ts +5 -5
  52. package/src/playbooks/companion-guidance.ts +7 -7
  53. package/src/runtime/env.ts +0 -4
  54. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
  55. package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
  56. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
  57. package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
  58. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +6 -5
  59. package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
  60. package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
  61. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3541 -0
  62. package/src/tools/setup/capabilities/context-mode.ts +57 -83
  63. package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
  64. package/src/tools/setup/capabilities/graphify.ts +86 -295
  65. package/src/tools/setup/capabilities/openspec.ts +27 -30
  66. package/src/tools/setup/capabilities/react-doctor.ts +37 -37
  67. package/src/tools/setup/capabilities/rtk.ts +4 -1
  68. package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
  69. package/src/tools/setup/capabilities/tokensave.ts +50 -71
  70. package/src/tools/setup/context-services.ts +29 -0
  71. package/src/tools/setup/dynamic-plugins/host.ts +2 -1
  72. package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
  73. package/src/tools/setup/dynamic-plugins/install.ts +124 -109
  74. package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
  75. package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
  76. package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
  77. package/src/tools/setup/engine.ts +65 -1
  78. package/src/tools/setup/mate.ts +28 -31
  79. package/src/tools/setup/plugin.ts +81 -0
  80. package/src/tools/setup/plugins/gitignore.ts +19 -9
  81. package/src/tools/setup/plugins/guidance.ts +2 -6
  82. package/src/tools/setup/policy.ts +4 -7
  83. package/src/tools/setup/providers/agent-file-sections.ts +78 -0
  84. package/src/tools/setup/providers/claude-format.ts +159 -0
  85. package/src/tools/setup/providers/claude.ts +283 -292
  86. package/src/tools/setup/providers/opencode-format.ts +146 -0
  87. package/src/tools/setup/providers/opencode.ts +171 -66
  88. package/src/tools/setup/providers/skill-tree.ts +38 -0
  89. package/src/tools/setup.ts +12 -7
  90. package/src/tui.ts +5 -0
  91. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +0 -161
  92. package/src/templates/providers/claude/.claude/hooks/mate-session-banner +0 -28
  93. package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +0 -242
  94. package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
@@ -1,17 +1,13 @@
1
- import fs from "node:fs/promises";
1
+ import { execFileSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
 
4
- import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME } from "../../framework";
5
5
  import { getActiveDistribution } from "../../distribution";
6
- import { CompanionResolver } from "../../lib/orchestrator/companion-resolver";
7
- import { CompanionStore, resolvePolicyFromConfig } from "../../lib/orchestrator/companion-store";
8
- import { ConfigStore } from "../../lib/orchestrator/config-store";
9
6
  import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
10
- import { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
11
- import { migrateConfigDir } from "../../lib/orchestrator/migration";
12
- import { findRepoLocalLinkedRepository } from "../../lib/orchestrator/repo-local-registry";
13
- import type { CapabilityConfig, FrameworkConfig } from "../../lib/orchestrator/types";
14
- import { WorkingRepoStore } from "../../lib/orchestrator/working-repo-store";
7
+ import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
8
+ import { pathIsDirectory } from "../../lib/orchestrator/repo-local-registry";
9
+ import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
10
+ import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
15
11
  import { getRequiredPluginDrift } from "../../tools/setup/policy";
16
12
  import { resolveCommandOnPath } from "../../tools/setup/utils";
17
13
  import { printSection, renderKeyValueTable, renderTable } from "./status";
@@ -20,11 +16,43 @@ interface DoctorDeps {
20
16
  globalConfigStore?: GlobalConfigStore;
21
17
  cwd?: string;
22
18
  pathValue?: string;
19
+ /** Returns a member checkout's HEAD commit, or null when unreadable. */
20
+ gitHead?: (memberPath: string) => string | null;
23
21
  }
24
22
 
25
- interface CompanionStores {
26
- configStore: ConfigStore;
27
- workingRepoStore: WorkingRepoStore;
23
+ export type DoctorKind = "core" | "working" | "companion" | "hub";
24
+
25
+ export interface DoctorToolInstallation {
26
+ tool: string;
27
+ source: string;
28
+ status: "ok" | "missing";
29
+ path: string | null;
30
+ }
31
+
32
+ export interface DoctorHubMember {
33
+ id: string;
34
+ source: "git" | "local";
35
+ path: string;
36
+ exists: boolean;
37
+ commitStatus: "ok" | "drifted" | "missing" | "unknown";
38
+ materializedCommit: string | null;
39
+ actualCommit: string | null;
40
+ }
41
+
42
+ export interface DoctorReport {
43
+ kind: DoctorKind;
44
+ cwd: string;
45
+ companionPath: string | null;
46
+ repositoryId: string | null;
47
+ multipleCompanions: Array<{ companionPath: string; repositoryId: string }>;
48
+ policy?: { allowedAgents: string[] };
49
+ policyError?: string;
50
+ capabilities?: string[];
51
+ toolInstallations?: DoctorToolInstallation[];
52
+ requiredPluginDrift?: Array<{ pluginId: string; kind: string; reason: string }>;
53
+ engineRequirement?: { range: string; ok: boolean; detail: string };
54
+ hub?: { members: DoctorHubMember[] };
55
+ resolutionFailures: Array<{ companionPath: string; message: string }>;
28
56
  }
29
57
 
30
58
  interface ToolCheck {
@@ -33,77 +61,26 @@ interface ToolCheck {
33
61
  source: string;
34
62
  }
35
63
 
36
- function storesForCompanion(companionPath: string): CompanionStores {
37
- const configDir = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config");
38
- return {
39
- configStore: new ConfigStore(path.join(configDir, "framework.yaml")),
40
- workingRepoStore: new WorkingRepoStore(path.join(configDir, "registry.yaml")),
41
- };
64
+ /** Doctor's view of the shared root kind: registry/env resolution means cwd is a working repo. */
65
+ function doctorKind(root: RootContext): DoctorKind {
66
+ if (root.kind === "core") return "core";
67
+ if (root.kind === "hub") return "hub";
68
+ if (root.origin === "env" || root.origin === "registry") return "working";
69
+ return root.kind;
42
70
  }
43
71
 
44
- async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
45
- const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
46
- const localLegacyDirs = frameworkConfig.legacyNames.map((name) => path.join(cwd, `.${name}`));
47
- await migrateConfigDir(localDir, localLegacyDirs);
48
-
72
+ function defaultGitHead(memberPath: string): string | null {
49
73
  try {
50
- await fs.access(path.join(localDir, "config", "framework.yaml"));
51
- return true;
74
+ return execFileSync("git", ["-C", memberPath, "rev-parse", "HEAD"], {
75
+ stdio: ["ignore", "pipe", "ignore"],
76
+ })
77
+ .toString()
78
+ .trim();
52
79
  } catch {
53
- return false;
80
+ return null;
54
81
  }
55
82
  }
56
83
 
57
- function renderList(items: string[], empty: string): string {
58
- return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : empty;
59
- }
60
-
61
- function renderEngineRequirement(
62
- config: FrameworkConfig,
63
- distributionName: string,
64
- currentVersion: string,
65
- ): string {
66
- const range = config.engines?.[distributionName];
67
- const result = checkEngineRequirement(config, distributionName, currentVersion);
68
- return result.ok
69
- ? `engines.${distributionName}: satisfied (requires ${range}, running ${currentVersion})`
70
- : `engines.${distributionName}: ${result.reason}`;
71
- }
72
-
73
- // Only the engines key matching the running distribution produces a
74
- // diagnostic; foreign distribution keys are ignored entirely.
75
- function printEngineRequirement(config: FrameworkConfig): void {
76
- const distribution = getActiveDistribution().config;
77
- if (!config.engines?.[distribution.name]) return;
78
- printSection(
79
- "Version Requirement",
80
- renderEngineRequirement(config, distribution.name, distribution.version),
81
- );
82
- }
83
-
84
- // Companions with no drift produce no section at all: required plugins are
85
- // silent when healthy.
86
- function printRequiredPluginDrift(config: FrameworkConfig): void {
87
- const drift = getRequiredPluginDrift(config);
88
- if (drift.length === 0) return;
89
- printSection(
90
- "Required Plugin Drift",
91
- renderTable(
92
- ["Plugin", "Kind", "Issue"],
93
- drift.map((entry) => [entry.pluginId, entry.kind, entry.reason]),
94
- ),
95
- );
96
- }
97
-
98
- function renderCapabilities(capabilities: CapabilityConfig[]): string {
99
- return capabilities.length > 0
100
- ? renderTable(
101
- ["Capability"],
102
- capabilities.map((capability) => [capability.name]),
103
- )
104
- : "No capabilities configured.";
105
- }
106
-
107
84
  function getSelectedToolChecks(config: {
108
85
  capabilities?: CapabilityConfig[];
109
86
  packageManagers?: string[];
@@ -139,76 +116,183 @@ function getSelectedToolChecks(config: {
139
116
  return checks;
140
117
  }
141
118
 
142
- function renderToolInstallations(
143
- config: {
144
- capabilities?: CapabilityConfig[];
145
- packageManagers?: string[];
146
- },
119
+ function collectToolInstallations(
120
+ config: FrameworkConfig,
147
121
  pathValue: string,
148
- ): string {
149
- const checks = getSelectedToolChecks(config);
150
- if (checks.length === 0) {
151
- return "No selected capabilities or package managers require command checks.";
122
+ ): DoctorToolInstallation[] {
123
+ return getSelectedToolChecks(config).map((check) => {
124
+ const resolved = resolveCommandOnPath(check.command, pathValue);
125
+ return {
126
+ tool: check.name,
127
+ source: check.source,
128
+ status: resolved ? "ok" : "missing",
129
+ path: resolved ?? null,
130
+ };
131
+ });
132
+ }
133
+
134
+ // Only the engines key matching the running distribution produces a
135
+ // diagnostic; foreign distribution keys are ignored entirely.
136
+ function collectEngineRequirement(
137
+ config: FrameworkConfig,
138
+ ): DoctorReport["engineRequirement"] | undefined {
139
+ const range = config.engines?.[FRAMEWORK_NAME];
140
+ if (!range) return undefined;
141
+ const currentVersion = getActiveDistribution().config.version;
142
+ const result = checkEngineRequirement(config, FRAMEWORK_NAME, currentVersion);
143
+ return {
144
+ range,
145
+ ok: result.ok,
146
+ detail: result.ok
147
+ ? `engines.${FRAMEWORK_NAME}: satisfied (requires ${range}, running ${currentVersion})`
148
+ : `engines.${FRAMEWORK_NAME}: ${result.reason}`,
149
+ };
150
+ }
151
+
152
+ async function collectHubMember(
153
+ hubPath: string,
154
+ member: HubMember,
155
+ gitHead: (memberPath: string) => string | null,
156
+ ): Promise<DoctorHubMember> {
157
+ const memberPath = path.resolve(hubPath, member.path);
158
+ const base = {
159
+ id: member.id,
160
+ source: member.source.kind,
161
+ path: memberPath,
162
+ materializedCommit: member.materializedCommit ?? null,
163
+ };
164
+ if (!(await pathIsDirectory(memberPath))) {
165
+ return { ...base, exists: false, commitStatus: "missing", actualCommit: null };
166
+ }
167
+ if (!member.materializedCommit) {
168
+ return { ...base, exists: true, commitStatus: "ok", actualCommit: null };
152
169
  }
170
+ const actualCommit = gitHead(memberPath);
171
+ return {
172
+ ...base,
173
+ exists: true,
174
+ commitStatus:
175
+ actualCommit === null
176
+ ? "unknown"
177
+ : actualCommit === member.materializedCommit
178
+ ? "ok"
179
+ : "drifted",
180
+ actualCommit,
181
+ };
182
+ }
153
183
 
154
- return renderTable(
155
- ["Tool", "Source", "Status", "Path"],
156
- checks.map((check) => {
157
- const resolved = resolveCommandOnPath(check.command, pathValue);
158
- return [check.name, check.source, resolved ? "ok" : "missing", resolved ?? ""];
159
- }),
160
- );
184
+ async function collectWorkingSections(
185
+ report: DoctorReport,
186
+ root: RootContext,
187
+ pathValue: string,
188
+ ): Promise<void> {
189
+ if (!root.rootPath || !root.repositoryId || !root.config) {
190
+ report.policyError = "No repository ID was provided for the resolved companion.";
191
+ return;
192
+ }
193
+
194
+ report.policy = { allowedAgents: root.config.allowedAgents ?? [] };
195
+ collectConfiguredRootSections(report, root.config, pathValue);
161
196
  }
162
197
 
163
- /**
164
- * @command mate doctor
165
- * @description Reports whether the current directory resolves as a linked
166
- * working repository, a companion repository, or neither. For linked working
167
- * repositories it also prints the effective policy and enabled capabilities.
168
- */
169
- export async function runDoctorCommand(_argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
170
- const cwd = deps.cwd ?? process.cwd();
198
+ function collectConfiguredRootSections(
199
+ report: DoctorReport,
200
+ config: FrameworkConfig,
201
+ pathValue: string,
202
+ ): void {
203
+ report.capabilities = (config.capabilities ?? []).map((capability) => capability.name);
204
+ report.toolInstallations = collectToolInstallations(config, pathValue);
205
+ report.requiredPluginDrift = getRequiredPluginDrift(config);
206
+ report.engineRequirement = collectEngineRequirement(config);
207
+ }
208
+
209
+ export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<DoctorReport> {
210
+ const cwd = path.resolve(deps.cwd ?? process.cwd());
171
211
  const pathValue = deps.pathValue ?? process.env.PATH ?? "";
172
- const envCompanionPath = process.env.MATE_ARTIFACT_PATH;
173
- const globalConfigStore = deps.globalConfigStore ?? new GlobalConfigStore();
174
- const resolver = new CompanionResolver(globalConfigStore);
212
+ const gitHead = deps.gitHead ?? defaultGitHead;
213
+ const root = await resolveRootContext(cwd, deps);
214
+ const kind = doctorKind(root);
175
215
 
176
- let state: "linked-working-repository" | "companion-repository" | "not-linked";
177
- let companionPath = "";
178
- let repositoryId = "";
179
- let repository = await findRepoLocalLinkedRepository(cwd);
180
- const resolution = envCompanionPath
181
- ? { match: null, failures: [], ambiguousMatches: [] }
182
- : await resolver.resolveWithDiagnostics(cwd);
216
+ const report: DoctorReport = {
217
+ kind,
218
+ cwd,
219
+ companionPath: root.rootPath ?? null,
220
+ repositoryId: root.repositoryId ?? null,
221
+ multipleCompanions:
222
+ root.resolution.ambiguousMatches.length > 1 ? root.resolution.ambiguousMatches : [],
223
+ resolutionFailures: root.resolution.failures,
224
+ };
183
225
 
184
- if (envCompanionPath) {
185
- state = "linked-working-repository";
186
- companionPath = path.resolve(envCompanionPath);
187
- repositoryId = process.env.MATE_REPO_ID ?? repository?.id ?? "";
188
- } else if (resolution.match) {
189
- state = "linked-working-repository";
190
- companionPath = resolution.match.companionPath;
191
- repositoryId = repository?.id ?? resolution.match.repositoryId;
192
- } else if (await hasLocalCompanionConfig(cwd)) {
193
- state = "companion-repository";
194
- companionPath = cwd;
195
- } else {
196
- state = "not-linked";
226
+ if (kind === "working") {
227
+ await collectWorkingSections(report, root, pathValue);
228
+ } else if (kind === "companion" && root.config) {
229
+ collectConfiguredRootSections(report, root.config, pathValue);
230
+ } else if (kind === "hub" && root.rootPath && root.config) {
231
+ report.hub = {
232
+ members: await Promise.all(
233
+ (root.config.hub?.companions ?? []).map((member) =>
234
+ collectHubMember(root.rootPath!, member, gitHead),
235
+ ),
236
+ ),
237
+ };
238
+ report.requiredPluginDrift = getRequiredPluginDrift(root.config);
239
+ report.engineRequirement = collectEngineRequirement(root.config);
197
240
  }
198
241
 
242
+ return report;
243
+ }
244
+
245
+ function renderList(items: string[], empty: string): string {
246
+ return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : empty;
247
+ }
248
+
249
+ function renderCapabilities(capabilities: string[]): string {
250
+ return capabilities.length > 0
251
+ ? renderTable(
252
+ ["Capability"],
253
+ capabilities.map((capability) => [capability]),
254
+ )
255
+ : "No capabilities configured.";
256
+ }
257
+
258
+ function renderToolInstallations(installations: DoctorToolInstallation[]): string {
259
+ if (installations.length === 0) {
260
+ return "No selected capabilities or package managers require command checks.";
261
+ }
262
+ return renderTable(
263
+ ["Tool", "Source", "Status", "Path"],
264
+ installations.map((item) => [item.tool, item.source, item.status, item.path ?? ""]),
265
+ );
266
+ }
267
+
268
+ function renderHubMembers(members: DoctorHubMember[]): string {
269
+ if (members.length === 0) return "No hub members configured.";
270
+ return renderTable(
271
+ ["Member", "Source", "Path", "Exists", "Commit"],
272
+ members.map((member) => [
273
+ member.id,
274
+ member.source,
275
+ member.path,
276
+ member.exists ? "yes" : "no",
277
+ member.commitStatus,
278
+ ]),
279
+ );
280
+ }
281
+
282
+ function renderHumanReport(report: DoctorReport): void {
199
283
  console.log("Mate Doctor");
200
284
  console.log("");
201
285
  printSection(
202
286
  "Context",
203
287
  renderKeyValueTable([
204
- ["State", state],
205
- ["Current Working Directory", cwd],
206
- ["Companion Path", companionPath || "none"],
207
- ["Repository ID", repositoryId || "none"],
288
+ ["State", report.kind],
289
+ ["Current Working Directory", report.cwd],
290
+ ["Companion Path", report.companionPath ?? "none"],
291
+ ["Repository ID", report.repositoryId ?? "none"],
208
292
  ]),
209
293
  );
210
294
 
211
- if (resolution.ambiguousMatches.length > 1) {
295
+ if (report.multipleCompanions.length > 1) {
212
296
  printSection(
213
297
  "Multiple Companions",
214
298
  [
@@ -218,63 +302,63 @@ export async function runDoctorCommand(_argv: string[] = [], deps: DoctorDeps =
218
302
  "",
219
303
  renderTable(
220
304
  ["Companion Path", "Repository ID"],
221
- resolution.ambiguousMatches.map((match) => [match.companionPath, match.repositoryId]),
305
+ report.multipleCompanions.map((match) => [match.companionPath, match.repositoryId]),
222
306
  ),
223
307
  ].join("\n"),
224
308
  );
225
309
  }
226
310
 
227
- if (state === "linked-working-repository" && companionPath && repositoryId) {
228
- const { configStore, workingRepoStore } = storesForCompanion(companionPath);
229
- const config = await configStore.load();
230
- let resolvedPolicy;
231
-
232
- if (repository) {
233
- resolvedPolicy = resolvePolicyFromConfig(config, [repository], repository.id);
234
- } else {
235
- resolvedPolicy = await new CompanionStore(configStore, workingRepoStore).resolvePolicy(
236
- repositoryId,
237
- );
238
- }
239
-
240
- if (!repository) {
241
- const working = await workingRepoStore.load();
242
- repository = working.repos.find((repo) => repo.id === repositoryId) ?? null;
243
- }
244
-
311
+ if (report.policy) {
245
312
  printSection(
246
- "Policy",
247
- renderKeyValueTable([
248
- ["Profile", repository?.profile ?? "unknown"],
249
- ["Allowed Agents", resolvedPolicy.allowedAgents.join(", ") || "none"],
250
- ]),
313
+ "Allowed Agents",
314
+ renderList(report.policy.allowedAgents, "No allowed agents configured."),
251
315
  );
316
+ }
317
+ if (report.policyError) printSection("Policy", report.policyError);
318
+ if (report.capabilities) printSection("Capabilities", renderCapabilities(report.capabilities));
319
+ if (report.toolInstallations) {
320
+ printSection("Tool Installations", renderToolInstallations(report.toolInstallations));
321
+ }
322
+ if (report.hub) printSection("Hub Members", renderHubMembers(report.hub.members));
323
+ // Companions with no drift produce no section at all: required plugins are
324
+ // silent when healthy.
325
+ if (report.requiredPluginDrift && report.requiredPluginDrift.length > 0) {
252
326
  printSection(
253
- "Allowed Agents",
254
- renderList(resolvedPolicy.allowedAgents, "No allowed agents configured."),
327
+ "Required Plugin Drift",
328
+ renderTable(
329
+ ["Plugin", "Kind", "Issue"],
330
+ report.requiredPluginDrift.map((entry) => [entry.pluginId, entry.kind, entry.reason]),
331
+ ),
255
332
  );
256
- printSection("Capabilities", renderCapabilities(config.capabilities ?? []));
257
- printSection("Tool Installations", renderToolInstallations(config, pathValue));
258
- printRequiredPluginDrift(config);
259
- printEngineRequirement(config);
260
- } else if (state === "linked-working-repository") {
261
- printSection("Policy", "No repository ID was provided for the resolved companion.");
262
- } else if (companionPath) {
263
- const { configStore } = storesForCompanion(companionPath);
264
- const config = await configStore.load();
265
- printSection("Capabilities", renderCapabilities(config.capabilities ?? []));
266
- printSection("Tool Installations", renderToolInstallations(config, pathValue));
267
- printRequiredPluginDrift(config);
268
- printEngineRequirement(config);
333
+ }
334
+ if (report.engineRequirement) {
335
+ printSection("Version Requirement", report.engineRequirement.detail);
269
336
  }
270
337
 
271
338
  printSection(
272
339
  "Resolution Failures",
273
- resolution.failures.length > 0
340
+ report.resolutionFailures.length > 0
274
341
  ? renderTable(
275
342
  ["Companion Path", "Error"],
276
- resolution.failures.map((failure) => [failure.companionPath, failure.message]),
343
+ report.resolutionFailures.map((failure) => [failure.companionPath, failure.message]),
277
344
  )
278
345
  : "No companion resolution failures encountered.",
279
346
  );
280
347
  }
348
+
349
+ /**
350
+ * @command mate doctor
351
+ * @description Reports the current directory's root kind — `working` (linked
352
+ * working repository), `companion`, `hub`, or `core` (not linked) — with
353
+ * kind-specific diagnostics: policy and capabilities for working repos,
354
+ * capabilities and tool checks for companions, member health for hubs.
355
+ * `--json` emits the same report as one JSON document.
356
+ */
357
+ export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
358
+ const report = await collectDoctorReport(deps);
359
+ if (argv.includes("--json")) {
360
+ console.log(JSON.stringify(report, null, 2));
361
+ return;
362
+ }
363
+ renderHumanReport(report);
364
+ }
@@ -1,5 +1,5 @@
1
1
  import { confirm } from "../confirm";
2
- import { frameworkCommandName } from "../../framework";
2
+ import { FRAMEWORK_NAME } from "../../framework";
3
3
  import {
4
4
  buildInstallPlan,
5
5
  inspectInstallPlan,
@@ -14,13 +14,15 @@ import {
14
14
  installDeclaredPlugins,
15
15
  type PluginInstallResult,
16
16
  } from "../../tools/setup/dynamic-plugins/install";
17
+ import { registryConfigHint } from "../../tools/setup/dynamic-plugins/registry-hint";
17
18
 
18
19
  export function reportPluginInstallResults(results: PluginInstallResult[]): void {
19
20
  for (const result of results) {
20
21
  if (result.status === "failed") {
21
22
  process.stderr.write(
22
- `${frameworkCommandName()}: plugin "${result.package}" failed to install: ${result.error ?? "unknown error"}\n`,
23
+ `${FRAMEWORK_NAME}: plugin "${result.package}" failed to install: ${result.error ?? "unknown error"}\n`,
23
24
  );
25
+ process.stderr.write(`${registryConfigHint(result.package)}\n`);
24
26
  } else if (result.status === "installed") {
25
27
  console.log(` installed plugin ${result.package}@${result.resolvedVersion}`);
26
28
  }
@@ -54,10 +56,14 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
54
56
  process.exitCode = 1;
55
57
  return false;
56
58
  }
57
- // Companion-declared plugins install first and hydrate into the registry
58
- // before the plan is built, so one run goes install → load → plan and their
59
- // own install requirements are part of the plan.
60
- if (context.kind === "companion" && context.companionPath && context.config.plugins?.length) {
59
+ // Root-declared plugins (companion or hub) install first and hydrate into
60
+ // the registry before the plan is built, so one run goes install → load →
61
+ // plan and their own install requirements are part of the plan.
62
+ if (
63
+ (context.kind === "companion" || context.kind === "hub") &&
64
+ context.companionPath &&
65
+ context.config.plugins?.length
66
+ ) {
61
67
  reportPluginInstallResults(
62
68
  await installDeclaredPlugins(context.companionPath, context.config.plugins),
63
69
  );
@@ -72,7 +78,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
72
78
  const ok = await confirm("Install the missing requirements? [y/N] ");
73
79
  if (!ok) {
74
80
  process.stderr.write(
75
- `Installation declined. Run \`${frameworkCommandName()} install --yes\` to continue.\n`,
81
+ `Installation declined. Run \`${FRAMEWORK_NAME} install --yes\` to continue.\n`,
76
82
  );
77
83
  process.exitCode = 1;
78
84
  return false;
@@ -83,7 +89,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
83
89
  printPlanText(plan);
84
90
  if (!skipConfirm) {
85
91
  process.stderr.write(
86
- `${frameworkCommandName()}: installation requires confirmation in a TTY. Re-run with \`${frameworkCommandName()} install --yes\`.\n`,
92
+ `${FRAMEWORK_NAME}: installation requires confirmation in a TTY. Re-run with \`${FRAMEWORK_NAME} install --yes\`.\n`,
87
93
  );
88
94
  process.exitCode = 1;
89
95
  return false;
@@ -103,11 +109,11 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
103
109
  if (!execution.ok) {
104
110
  for (const result of execution.results.filter((item) => item.status === "failed")) {
105
111
  process.stderr.write(
106
- `${frameworkCommandName()}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
112
+ `${FRAMEWORK_NAME}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
107
113
  );
108
114
  }
109
115
  process.stderr.write(
110
- `Installation is incomplete. Re-run \`${frameworkCommandName()} install\` after remediation.\n`,
116
+ `Installation is incomplete. Re-run \`${FRAMEWORK_NAME} install\` after remediation.\n`,
111
117
  );
112
118
  process.exitCode = 1;
113
119
  return false;
@@ -118,11 +124,9 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
118
124
  await saveCompleteInstallState(plan, execution.results);
119
125
  } catch (error) {
120
126
  process.stderr.write(
121
- `${frameworkCommandName()}: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
122
- );
123
- process.stderr.write(
124
- `Installation is incomplete. Re-run \`${frameworkCommandName()} install\`.\n`,
127
+ `${FRAMEWORK_NAME}: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
125
128
  );
129
+ process.stderr.write(`Installation is incomplete. Re-run \`${FRAMEWORK_NAME} install\`.\n`);
126
130
  process.exitCode = 1;
127
131
  return false;
128
132
  }
@@ -1,4 +1,4 @@
1
- import { frameworkCommandName } from "../../../framework";
1
+ import { FRAMEWORK_NAME } from "../../../framework";
2
2
  import { FrameworkLauncher } from "../../../lib/orchestrator/launcher";
3
3
  import {
4
4
  LaunchPreflightError,
@@ -43,7 +43,7 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
43
43
  const agentArgs = separatorIndex >= 0 ? argv.slice(separatorIndex + 1) : [];
44
44
 
45
45
  for (const arg of optionArgs) {
46
- process.stderr.write(`${frameworkCommandName()}: unknown launch option: ${arg}\n`);
46
+ process.stderr.write(`${FRAMEWORK_NAME}: unknown launch option: ${arg}\n`);
47
47
  process.exitCode = 1;
48
48
  return null;
49
49
  }
@@ -109,7 +109,7 @@ export async function runLaunchToolCommand(
109
109
 
110
110
  if (error instanceof ToolNotAllowedError) {
111
111
  process.stderr.write(
112
- `${frameworkCommandName()}: \`${tool}\` is disallowed by active repository policy.\n`,
112
+ `${FRAMEWORK_NAME}: \`${tool}\` is disallowed by active repository policy.\n`,
113
113
  );
114
114
  process.exitCode = 1;
115
115
  return;
@@ -117,7 +117,7 @@ export async function runLaunchToolCommand(
117
117
 
118
118
  if (error instanceof RepositoryNotSelectedError) {
119
119
  process.stderr.write(`${error.message}\n`);
120
- process.stderr.write(`Run \`${frameworkCommandName()} companion link\` first.\n`);
120
+ process.stderr.write(`Run \`${FRAMEWORK_NAME} companion link\` first.\n`);
121
121
  process.exitCode = 1;
122
122
  return;
123
123
  }