@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
@@ -3,10 +3,10 @@ import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
 
6
- import { defaultConfig, ConfigStore, mergeWithDefaults } from "./orchestrator/config-store";
7
- import { CompanionResolver } from "./orchestrator/companion-resolver";
6
+ import { defaultConfig } from "./orchestrator/config-store";
8
7
  import { checkEngineRequirement } from "./orchestrator/engine-guard";
9
- import { GlobalConfigStore } from "./orchestrator/global-config-store";
8
+ import type { GlobalConfigStore } from "./orchestrator/global-config-store";
9
+ import { resolveRootContext } from "./orchestrator/root-context";
10
10
  import { YamlFileStore } from "./orchestrator/yaml-file-store";
11
11
  import { FRAMEWORK_NAME } from "../framework";
12
12
  import type { FrameworkConfig } from "./orchestrator/types";
@@ -18,7 +18,7 @@ import type { Plugin } from "../tools/setup/plugin";
18
18
  export const INSTALL_CONTRACT_REVISION = 1;
19
19
 
20
20
  export interface InstallContext {
21
- kind: "core" | "companion" | "ambiguous";
21
+ kind: "core" | "working" | "companion" | "hub" | "ambiguous";
22
22
  companionPath?: string;
23
23
  config: FrameworkConfig;
24
24
  fingerprint: string;
@@ -66,8 +66,8 @@ export function getInstallStatePath(
66
66
  throw new Error("cannot resolve install state for an ambiguous companion context");
67
67
  }
68
68
  const key =
69
- scope.kind === "companion" && scope.companionPath
70
- ? fingerprint({ kind: "companion", companionPath: path.resolve(scope.companionPath) })
69
+ (scope.kind === "companion" || scope.kind === "hub") && scope.companionPath
70
+ ? fingerprint({ kind: scope.kind, companionPath: path.resolve(scope.companionPath) })
71
71
  : "core";
72
72
  return path.join(stateRoot, `${key}.yaml`);
73
73
  }
@@ -106,50 +106,40 @@ function coreContext(): InstallContext {
106
106
  };
107
107
  }
108
108
 
109
- async function contextForCompanion(companionPath: string): Promise<InstallContext> {
110
- const configPath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
111
- const config = mergeWithDefaults(await new ConfigStore(configPath).load());
112
- return {
113
- kind: "companion",
114
- companionPath,
115
- config,
116
- fingerprint: fingerprint({ kind: "companion", companionPath, config }),
117
- };
118
- }
119
-
120
- async function hasLocalConfig(cwd: string): Promise<boolean> {
121
- try {
122
- await fs.access(path.join(cwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
123
- return true;
124
- } catch {
125
- return false;
126
- }
109
+ function workingContext(): InstallContext {
110
+ const context = coreContext();
111
+ return { ...context, kind: "working" };
127
112
  }
128
113
 
129
114
  export async function resolveInstallContext(
130
115
  cwd = process.cwd(),
131
116
  deps: InstallContextDeps = {},
132
117
  ): Promise<InstallContext> {
133
- const resolvedCwd = path.resolve(cwd);
134
- const envCompanionPath = process.env.MATE_ARTIFACT_PATH;
135
- if (envCompanionPath) return contextForCompanion(path.resolve(envCompanionPath));
136
-
137
- const resolver = new CompanionResolver(deps.globalConfigStore ?? new GlobalConfigStore());
138
- const resolution = await resolver.resolveWithDiagnostics(resolvedCwd);
139
- if (resolution.ambiguousMatches.length > 1) {
118
+ const root = await resolveRootContext(cwd, deps);
119
+ if (root.resolution.ambiguousMatches.length > 1) {
140
120
  return {
141
121
  ...coreContext(),
142
122
  kind: "ambiguous",
143
123
  message: [
144
124
  "multiple companions match the current working repository.",
145
- ...resolution.ambiguousMatches.map((match) => ` - ${match.companionPath}`),
125
+ ...root.resolution.ambiguousMatches.map((match) => ` - ${match.companionPath}`),
146
126
  "Run `mate install` from an explicit companion context or set MATE_ARTIFACT_PATH.",
147
127
  ].join("\n"),
148
128
  };
149
129
  }
150
- if (resolution.match) return contextForCompanion(resolution.match.companionPath);
151
- if (await hasLocalConfig(resolvedCwd)) return contextForCompanion(resolvedCwd);
152
- return coreContext();
130
+ if (root.kind === "working") return workingContext();
131
+ if (root.kind === "core" || !root.rootPath || !root.config) return coreContext();
132
+
133
+ return {
134
+ kind: root.kind,
135
+ companionPath: root.rootPath,
136
+ config: root.config,
137
+ fingerprint: fingerprint({
138
+ kind: root.kind,
139
+ companionPath: root.rootPath,
140
+ config: root.config,
141
+ }),
142
+ };
153
143
  }
154
144
 
155
145
  export function buildInstallPlan(
@@ -292,11 +282,7 @@ export async function inspectInstallPreflight(
292
282
  ): Promise<InstallPreflightResult> {
293
283
  const context = await resolveInstallContext(cwd, deps);
294
284
  const distribution = getActiveDistribution().config;
295
- const engineCheck = checkEngineRequirement(
296
- context.config,
297
- distribution.name,
298
- distribution.version,
299
- );
285
+ const engineCheck = checkEngineRequirement(context.config, FRAMEWORK_NAME, distribution.version);
300
286
  if (!engineCheck.ok) return { ok: false, reason: engineCheck.reason };
301
287
  const plan = await inspectInstallPlan(buildInstallPlan(context));
302
288
  if (context.kind === "ambiguous") return { ok: false, reason: context.message, plan };
@@ -333,8 +319,10 @@ export async function inspectInstallPreflight(
333
319
  }
334
320
 
335
321
  export async function reconcileInstalledCompanion(plan: InstallPlan): Promise<void> {
336
- if (plan.context.kind === "companion" && plan.context.companionPath) {
337
- const { syncCompanionFiles } = await import("../tools/setup");
338
- await syncCompanionFiles(plan.context.companionPath, plan.context.config);
322
+ if (plan.context.kind !== "companion" || !plan.context.companionPath) {
323
+ return;
339
324
  }
325
+
326
+ const { syncCompanionFiles } = await import("../tools/setup");
327
+ await syncCompanionFiles(plan.context.companionPath, plan.context.config);
340
328
  }
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import { version } from "../../../../package.json";
5
5
  import { isCommandOnPath } from "../../fs-utils";
6
- import { FRAMEWORK_NAME, frameworkCommandName } from "../../../framework";
6
+ import { FRAMEWORK_NAME } from "../../../framework";
7
7
  import { getReactDoctorBinPath, getWrapperBinPath } from "../../package-paths";
8
8
  import {
9
9
  GRAPHIFY_OUTPUT_SUBDIR,
@@ -17,11 +17,11 @@ import {
17
17
  spawnProxyBackground as defaultSpawnProxyBackground,
18
18
  waitForProxyReachable as defaultWaitForProxyReachable,
19
19
  } from "../headroom/proxy";
20
- import type { CapabilityConfig, GitModeProfile, LinkedRepository, PolicySettings } from "../types";
20
+ import type { CapabilityConfig, GitModeProfile, LinkedRepository } from "../types";
21
21
 
22
22
  export interface AdapterContext {
23
23
  repository: LinkedRepository;
24
- policy: PolicySettings;
24
+ allowedAgents: string[];
25
25
  companionPath: string;
26
26
  capabilities: CapabilityConfig[];
27
27
  git?: GitModeProfile;
@@ -78,18 +78,17 @@ export abstract class LaunchAdapter {
78
78
  const env: NodeJS.ProcessEnv = {
79
79
  ...process.env,
80
80
  MATE_NAME: FRAMEWORK_NAME,
81
- MATE_COMMAND: frameworkCommandName(),
82
81
  MATE_VERSION: version,
83
82
  MATE_ARTIFACT_PATH: context.companionPath,
84
83
  MATE_WRAPPER_BIN_PATH: wrapperBinPath,
85
84
  PATH: prependPathEntry(process.env.PATH, wrapperBinPath),
86
85
  MATE_GRAPHIFY_ENABLED: context.capabilities.some((c) => c.name === "graphify") ? "1" : "0",
86
+ MATE_OPENSPEC_ENABLED: context.capabilities.some((c) => c.name === "openspec") ? "1" : "0",
87
87
  MATE_REACT_DOCTOR_ENABLED: reactDoctorEnabled ? "1" : "0",
88
88
  MATE_GIT_AUTO_MODE: context.git === "auto" ? "1" : "0",
89
89
  MATE_REPO_ID: context.repository.id,
90
90
  MATE_REPO_PATH: context.repository.path,
91
- MATE_REPO_PROFILE: context.repository.profile,
92
- MATE_POLICY_JSON: JSON.stringify(context.policy),
91
+ MATE_POLICY_JSON: JSON.stringify({ allowedAgents: context.allowedAgents }),
93
92
  };
94
93
 
95
94
  if (reactDoctorEnabled) {
@@ -149,7 +148,7 @@ export abstract class LaunchAdapter {
149
148
  command: this.toolName,
150
149
  args: builtArgs,
151
150
  env,
152
- warning: `${frameworkCommandName()}: headroom capability enabled but \`headroom\` was not found on PATH; install with \`uv tool install "headroom-ai[all]"\`; launching ${this.toolName} directly\n`,
151
+ warning: `${FRAMEWORK_NAME}: headroom capability enabled but \`headroom\` was not found on PATH; install with \`uv tool install "headroom-ai[all]"\`; launching ${this.toolName} directly\n`,
153
152
  };
154
153
  }
155
154
 
@@ -183,7 +182,7 @@ export abstract class LaunchAdapter {
183
182
  command: this.toolName,
184
183
  args: builtArgs,
185
184
  env,
186
- warning: `${frameworkCommandName()}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
185
+ warning: `${FRAMEWORK_NAME}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
187
186
  };
188
187
  }
189
188
  }
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
 
3
3
  import { buildCompanionGuidance } from "../../../playbooks/companion-guidance";
4
4
  import { getContextModePackageRoot, validateContextModePackage } from "../../context-mode-package";
5
+ import { getClaudePluginRoot, validateClaudePluginAssets } from "../../package-paths";
5
6
  import {
6
7
  getCompanionClaudeMcpConfigPath,
7
8
  getCompanionClaudeSettingsPath,
@@ -13,6 +14,17 @@ export class ClaudeAdapter extends LaunchAdapter {
13
14
  readonly interactive = true;
14
15
 
15
16
  async validateLaunch(context: AdapterContext): Promise<void> {
17
+ // The bundled mate plugin carries the artifact-path guard; never launch a
18
+ // managed session without it.
19
+ try {
20
+ validateClaudePluginAssets();
21
+ } catch (error) {
22
+ throw new Error(
23
+ `Mate Claude plugin is unavailable: ${(error as Error).message}. Reinstall mate to repair it.`,
24
+ { cause: error },
25
+ );
26
+ }
27
+
16
28
  if (context.capabilities.some((capability) => capability.name === "context-mode")) {
17
29
  try {
18
30
  await validateContextModePackage(context.companionPath);
@@ -56,6 +68,10 @@ export class ClaudeAdapter extends LaunchAdapter {
56
68
  buildCompanionGuidance(context),
57
69
  ...settingsArgs,
58
70
  ...mcpConfigArgs,
71
+ // Bundled mate plugin (hooks) resolved from the running mate-core
72
+ // installation, coexisting with the context-mode plugin dir.
73
+ "--plugin-dir",
74
+ getClaudePluginRoot(),
59
75
  ...contextModeArgs,
60
76
  ...args,
61
77
  ];
@@ -9,61 +9,15 @@ import {
9
9
  getOpenCodePluginPackageReference,
10
10
  isMateOpenCodePluginReference,
11
11
  } from "../../opencode-plugin-package";
12
+ import {
13
+ getOpenCodePluginReferences,
14
+ mergeOpenCodeConfigContent,
15
+ readOpenCodeConfig,
16
+ } from "../../../tools/setup/providers/opencode-format";
12
17
  import { buildOpenCodeGuidance } from "../opencode-guidance";
13
18
  import { LaunchPreflightError } from "../types";
14
19
  import { type AdapterContext, LaunchAdapter } from "./base";
15
20
 
16
- function isRecord(value: unknown): value is Record<string, unknown> {
17
- return typeof value === "object" && value !== null && !Array.isArray(value);
18
- }
19
-
20
- function mergeConfig(target: Record<string, unknown>, source: Record<string, unknown>): void {
21
- for (const [key, sourceValue] of Object.entries(source)) {
22
- const targetValue = target[key];
23
- if (isRecord(targetValue) && isRecord(sourceValue)) {
24
- mergeConfig(targetValue, sourceValue);
25
- continue;
26
- }
27
-
28
- target[key] = sourceValue;
29
- }
30
- }
31
-
32
- function mergeOpenCodeConfigContent(
33
- overlay: Record<string, unknown>,
34
- env: NodeJS.ProcessEnv = process.env,
35
- options: { appendSkillPaths?: string[] } = {},
36
- ): string {
37
- let config: Record<string, unknown> = {};
38
- const existing = env.OPENCODE_CONFIG_CONTENT;
39
-
40
- if (existing) {
41
- try {
42
- const parsed = JSON.parse(existing) as unknown;
43
- if (isRecord(parsed)) {
44
- config = parsed;
45
- }
46
- } catch {
47
- // Ignore invalid inherited config content rather than breaking launch.
48
- }
49
- }
50
-
51
- mergeConfig(config, overlay);
52
-
53
- if (options.appendSkillPaths && options.appendSkillPaths.length > 0) {
54
- const skills = isRecord(config.skills) ? config.skills : {};
55
- const paths = Array.isArray(skills.paths) ? skills.paths : [];
56
- const existingPaths = new Set(paths);
57
- skills.paths = [
58
- ...paths,
59
- ...options.appendSkillPaths.filter((skillPath) => !existingPaths.has(skillPath)),
60
- ];
61
- config.skills = skills;
62
- }
63
-
64
- return JSON.stringify(config);
65
- }
66
-
67
21
  export class OpenCodeAdapter extends LaunchAdapter {
68
22
  readonly toolName = "opencode";
69
23
  readonly interactive = true;
@@ -190,18 +144,14 @@ export class OpenCodeAdapter extends LaunchAdapter {
190
144
  ): Promise<string[]> {
191
145
  const configPath = path.join(context.companionPath, configFile);
192
146
 
193
- let config: unknown;
194
- try {
195
- config = JSON.parse(await fs.readFile(configPath, "utf8"));
196
- } catch {
197
- return [`Unreadable OpenCode configuration: ${configPath}`];
198
- }
199
- if (!isRecord(config)) {
147
+ const { present, config } = await readOpenCodeConfig(configPath);
148
+ if (!present) {
200
149
  return [`Unreadable OpenCode configuration: ${configPath}`];
201
150
  }
202
151
 
203
- const plugins = Array.isArray(config.plugin) ? (config.plugin as unknown[]) : [];
204
- const mateReferences = plugins.filter(isMateOpenCodePluginReference) as string[];
152
+ const mateReferences = getOpenCodePluginReferences(config).filter(
153
+ isMateOpenCodePluginReference,
154
+ ) as string[];
205
155
 
206
156
  if (mateReferences.includes(expectedPluginReference)) {
207
157
  return [];