@uniqbit/mate-core 0.15.1-canary.1 → 0.15.1-canary.2

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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/companion/link.ts +2 -2
  3. package/src/cli/commands/config.ts +2 -2
  4. package/src/cli/commands/doctor.ts +3 -3
  5. package/src/cli/commands/install.ts +15 -6
  6. package/src/cli/commands/launch/shared.ts +6 -3
  7. package/src/cli/commands/setup.ts +3 -3
  8. package/src/cli/commands/update.ts +9 -9
  9. package/src/cli/main.ts +5 -2
  10. package/src/cli/plugin-commands.ts +3 -2
  11. package/src/cli/usage.ts +6 -3
  12. package/src/create-mate.ts +4 -3
  13. package/src/framework.ts +20 -0
  14. package/src/lib/install.ts +4 -9
  15. package/src/lib/orchestrator/adapters/base.ts +5 -4
  16. package/src/lib/orchestrator/config-store.ts +2 -2
  17. package/src/lib/orchestrator/framework-context.ts +4 -4
  18. package/src/lib/orchestrator/global-config-store.ts +2 -3
  19. package/src/lib/orchestrator/repo-local-registry.ts +2 -3
  20. package/src/lib/orchestrator/setup-preflight.ts +5 -5
  21. package/src/lib/orchestrator/working-repo-store.ts +3 -3
  22. package/src/lib/update-checker.ts +8 -8
  23. package/src/playbooks/companion-guidance.ts +8 -9
  24. package/src/runtime/env.ts +1 -0
  25. package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +2 -1
  26. package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/SKILL.md +8 -8
  27. package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/references/openspec.md +3 -3
  28. package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +25 -2
  29. package/src/tools/setup/capabilities/graphify.ts +4 -4
  30. package/src/tools/setup/capabilities/openspec.ts +29 -1
  31. package/src/tools/setup/engine.ts +2 -1
  32. package/src/tools/setup/plugins/guidance.ts +3 -3
  33. package/src/tools/setup/providers/claude.ts +22 -5
  34. package/src/tools/setup/providers/opencode.ts +3 -2
  35. package/src/tools/setup.ts +13 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniqbit/mate-core",
3
- "version": "0.15.1-canary.1",
3
+ "version": "0.15.1-canary.2",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import fs from "node:fs/promises";
5
5
 
6
- import { frameworkConfig } from "../../../framework";
6
+ import { FRAMEWORK_NAME } from "../../../framework";
7
7
  import {
8
8
  selectCompanionLinkInputs,
9
9
  type CompanionLinkInputs,
@@ -48,7 +48,7 @@ interface CompanionLinkCommandDeps {
48
48
 
49
49
  /** All companions surfaced by the "existing companion" picker live under this directory. */
50
50
  function companionsHomeDir(): string {
51
- return path.join(os.homedir(), `.${frameworkConfig.name}`, "companions");
51
+ return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "companions");
52
52
  }
53
53
 
54
54
  function isInsideDir(parentDir: string, candidatePath: string): boolean {
@@ -1,9 +1,9 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
  import { execSync } from "node:child_process";
4
- import { frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME } from "../../framework";
5
5
 
6
- const getGlobalConfigDir = () => path.join(os.homedir(), `.${frameworkConfig.name}`);
6
+ const getGlobalConfigDir = () => path.join(os.homedir(), `.${FRAMEWORK_NAME}`);
7
7
 
8
8
  /**
9
9
  * @command mate config
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
5
5
  import { getActiveDistribution } from "../../distribution";
6
6
  import { CompanionResolver } from "../../lib/orchestrator/companion-resolver";
7
7
  import { CompanionStore, resolvePolicyFromConfig } from "../../lib/orchestrator/companion-store";
@@ -34,7 +34,7 @@ interface ToolCheck {
34
34
  }
35
35
 
36
36
  function storesForCompanion(companionPath: string): CompanionStores {
37
- const configDir = path.join(companionPath, `.${frameworkConfig.name}`, "config");
37
+ const configDir = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config");
38
38
  return {
39
39
  configStore: new ConfigStore(path.join(configDir, "framework.yaml")),
40
40
  workingRepoStore: new WorkingRepoStore(path.join(configDir, "registry.yaml")),
@@ -42,7 +42,7 @@ function storesForCompanion(companionPath: string): CompanionStores {
42
42
  }
43
43
 
44
44
  async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
45
- const localDir = path.join(cwd, `.${frameworkConfig.name}`);
45
+ const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
46
46
  const localLegacyDirs = frameworkConfig.legacyNames.map((name) => path.join(cwd, `.${name}`));
47
47
  await migrateConfigDir(localDir, localLegacyDirs);
48
48
 
@@ -1,4 +1,5 @@
1
1
  import { confirm } from "../confirm";
2
+ import { frameworkCommandName } from "../../framework";
2
3
  import {
3
4
  buildInstallPlan,
4
5
  inspectInstallPlan,
@@ -44,7 +45,9 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
44
45
  if (missing.length > 0 && !skipConfirm) {
45
46
  const ok = await confirm("Install the missing requirements? [y/N] ");
46
47
  if (!ok) {
47
- process.stderr.write("Installation declined. Run `mate install --yes` to continue.\n");
48
+ process.stderr.write(
49
+ `Installation declined. Run \`${frameworkCommandName()} install --yes\` to continue.\n`,
50
+ );
48
51
  process.exitCode = 1;
49
52
  return false;
50
53
  }
@@ -54,7 +57,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
54
57
  printPlanText(plan);
55
58
  if (!skipConfirm) {
56
59
  process.stderr.write(
57
- "mate: installation requires confirmation in a TTY. Re-run with `mate install --yes`.\n",
60
+ `${frameworkCommandName()}: installation requires confirmation in a TTY. Re-run with \`${frameworkCommandName()} install --yes\`.\n`,
58
61
  );
59
62
  process.exitCode = 1;
60
63
  return false;
@@ -73,9 +76,13 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
73
76
  }
74
77
  if (!execution.ok) {
75
78
  for (const result of execution.results.filter((item) => item.status === "failed")) {
76
- process.stderr.write(`mate: ${result.id} failed: ${result.error ?? "unknown error"}\n`);
79
+ process.stderr.write(
80
+ `${frameworkCommandName()}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
81
+ );
77
82
  }
78
- process.stderr.write("Installation is incomplete. Re-run `mate install` after remediation.\n");
83
+ process.stderr.write(
84
+ `Installation is incomplete. Re-run \`${frameworkCommandName()} install\` after remediation.\n`,
85
+ );
79
86
  process.exitCode = 1;
80
87
  return false;
81
88
  }
@@ -85,9 +92,11 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
85
92
  await saveCompleteInstallState(plan, execution.results);
86
93
  } catch (error) {
87
94
  process.stderr.write(
88
- `mate: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
95
+ `${frameworkCommandName()}: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
96
+ );
97
+ process.stderr.write(
98
+ `Installation is incomplete. Re-run \`${frameworkCommandName()} install\`.\n`,
89
99
  );
90
- process.stderr.write("Installation is incomplete. Re-run `mate install`.\n");
91
100
  process.exitCode = 1;
92
101
  return false;
93
102
  }
@@ -1,3 +1,4 @@
1
+ import { frameworkCommandName } from "../../../framework";
1
2
  import { FrameworkLauncher } from "../../../lib/orchestrator/launcher";
2
3
  import {
3
4
  LaunchPreflightError,
@@ -42,7 +43,7 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
42
43
  const agentArgs = separatorIndex >= 0 ? argv.slice(separatorIndex + 1) : [];
43
44
 
44
45
  for (const arg of optionArgs) {
45
- process.stderr.write(`mate: unknown launch option: ${arg}\n`);
46
+ process.stderr.write(`${frameworkCommandName()}: unknown launch option: ${arg}\n`);
46
47
  process.exitCode = 1;
47
48
  return null;
48
49
  }
@@ -107,14 +108,16 @@ export async function runLaunchToolCommand(
107
108
  progress?.stop();
108
109
 
109
110
  if (error instanceof ToolNotAllowedError) {
110
- process.stderr.write(`mate: \`${tool}\` is disallowed by active repository policy.\n`);
111
+ process.stderr.write(
112
+ `${frameworkCommandName()}: \`${tool}\` is disallowed by active repository policy.\n`,
113
+ );
111
114
  process.exitCode = 1;
112
115
  return;
113
116
  }
114
117
 
115
118
  if (error instanceof RepositoryNotSelectedError) {
116
119
  process.stderr.write(`${error.message}\n`);
117
- process.stderr.write("Run `mate companion link` first.\n");
120
+ process.stderr.write(`Run \`${frameworkCommandName()} companion link\` first.\n`);
118
121
  process.exitCode = 1;
119
122
  return;
120
123
  }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { frameworkConfig } from "../../framework";
3
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../../framework";
4
4
  import { ConfigStore, defaultConfig } from "../../lib/orchestrator/config-store";
5
5
  import { executeSetup } from "../../tools/setup";
6
6
  import {
@@ -98,7 +98,7 @@ async function runSetupFlow(
98
98
  if (await checkWorkingRepo(cwd)) {
99
99
  process.stderr.write(
100
100
  `Warning: this directory looks like a working repository (project files detected).\n` +
101
- `Initializing it as a ${frameworkConfig.name} companion here may not be what you want.\n`,
101
+ `Initializing it as a ${frameworkCommandName()} companion here may not be what you want.\n`,
102
102
  );
103
103
  }
104
104
  const ok = await askConfirm("Initialize this directory as a Mate companion repository? [y/N] ");
@@ -156,7 +156,7 @@ async function runSetupFlow(
156
156
  ? getSetupSelectionsFromConfig(defaultConfig())
157
157
  : getSetupSelectionsFromConfig(
158
158
  await new ConfigStore(
159
- path.join(targetCwd, `.${frameworkConfig.name}`, "config", "framework.yaml"),
159
+ path.join(targetCwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
160
160
  ).load(),
161
161
  );
162
162
 
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { spawnSync } from "node:child_process";
3
3
 
4
- import { frameworkConfig } from "../../framework";
4
+ import { frameworkCommandName } from "../../framework";
5
5
  import {
6
6
  OPENCODE_PLUGIN_PACKAGE_NAME,
7
7
  warmOpenCodePluginCache,
@@ -20,7 +20,7 @@ type InstallResult = ReturnType<typeof installPublicPackageSync>;
20
20
  function writeNpmOnlyRecoveryMessage(): void {
21
21
  const { packageName, registry } = getUpdateConfig();
22
22
  process.stderr.write(
23
- `${frameworkConfig.name}: self-update is only supported for npm-installed Mate.\n`,
23
+ `${frameworkCommandName()}: self-update is only supported for npm-installed Mate.\n`,
24
24
  );
25
25
  if (packageName.startsWith("@uniqbit/")) {
26
26
  process.stderr.write(
@@ -88,7 +88,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
88
88
  latest = await updateCommandDeps.fetchLatestVersion();
89
89
  } catch {
90
90
  process.stderr.write(
91
- `${frameworkConfig.name}: could not reach registry to check for updates\n`,
91
+ `${frameworkCommandName()}: could not reach registry to check for updates\n`,
92
92
  );
93
93
  process.exitCode = 1;
94
94
  return;
@@ -96,7 +96,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
96
96
 
97
97
  if (checkOnly) {
98
98
  if (updateCommandDeps.isNewer(latest, current)) {
99
- console.log(`${frameworkConfig.name}: update available (${current} → ${latest})`);
99
+ console.log(`${frameworkCommandName()}: update available (${current} → ${latest})`);
100
100
  process.exitCode = 1;
101
101
  } else {
102
102
  console.log("Up to date.");
@@ -127,11 +127,11 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
127
127
  } catch (error) {
128
128
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
129
129
  process.stderr.write(
130
- `${frameworkConfig.name}: npm is required for self-update but was not found on PATH\n`,
130
+ `${frameworkCommandName()}: npm is required for self-update but was not found on PATH\n`,
131
131
  );
132
132
  } else {
133
133
  process.stderr.write(
134
- `${frameworkConfig.name}: could not verify the npm installation used for self-update\n`,
134
+ `${frameworkCommandName()}: could not verify the npm installation used for self-update\n`,
135
135
  );
136
136
  }
137
137
  writeNpmOnlyRecoveryMessage();
@@ -148,7 +148,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
148
148
  const result = updateCommandDeps.installLatest(latest);
149
149
  if (result.status !== 0 || result.error) {
150
150
  process.stderr.write(
151
- `${frameworkConfig.name}: upgrade command exited with status ${result.status ?? 1}\n`,
151
+ `${frameworkCommandName()}: upgrade command exited with status ${result.status ?? 1}\n`,
152
152
  );
153
153
  process.exitCode = 1;
154
154
  return;
@@ -163,7 +163,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
163
163
  if (!warmed.ok) {
164
164
  process.stderr.write(
165
165
  [
166
- `${frameworkConfig.name}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
166
+ `${frameworkCommandName()}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
167
167
  "The next managed OpenCode launch will download it (requires registry access).",
168
168
  ...(warmed.detail ? [`Details: ${warmed.detail}`] : []),
169
169
  ].join("\n") + "\n",
@@ -173,7 +173,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
173
173
  const postInstall = updateCommandDeps.runPostInstall(skipConfirm);
174
174
  if (postInstall.status !== 0 || postInstall.error) {
175
175
  process.stderr.write(
176
- `\nUpgraded to ${latest}, but installation is incomplete. Run \`mate install\`.\n`,
176
+ `\nUpgraded to ${latest}, but installation is incomplete. Run \`${frameworkCommandName()} install\`.\n`,
177
177
  );
178
178
  process.exitCode = 1;
179
179
  return;
package/src/cli/main.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getActiveDistribution } from "../distribution";
2
+ import { frameworkCommandName } from "../framework";
2
3
  import {
3
4
  enforceUpdateIfRequired,
4
5
  scheduleBackgroundCheck,
@@ -81,8 +82,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
81
82
  if (!isInstallRecoveryCommand(command, subcommand)) {
82
83
  const preflight = await deps.inspectInstallPreflight();
83
84
  if (!preflight.ok) {
84
- console.error(`mate: ${preflight.reason ?? "installation is incomplete"}`);
85
- console.error("Run `mate install` before continuing.");
85
+ console.error(
86
+ `${frameworkCommandName()}: ${preflight.reason ?? "installation is incomplete"}`,
87
+ );
88
+ console.error(`Run \`${frameworkCommandName()} install\` before continuing.`);
86
89
  process.exitCode = 1;
87
90
  return;
88
91
  }
@@ -1,4 +1,5 @@
1
1
  import { getActiveDistribution } from "../distribution";
2
+ import { frameworkCommandName } from "../framework";
2
3
  import { resolveInstallContext } from "../lib/install";
3
4
  import type { FrameworkConfig } from "../lib/orchestrator/types";
4
5
  import type { Plugin, PluginCliCommand } from "../tools/setup/plugin";
@@ -85,7 +86,7 @@ export async function ensureCapabilityEnabled(
85
86
  const loadConfig = deps.loadConfig ?? (async () => (await resolveInstallContext()).config);
86
87
  if (plugin?.isEnabled(await loadConfig())) return true;
87
88
 
88
- const name = distribution.config.name;
89
+ const name = frameworkCommandName();
89
90
  console.error(
90
91
  `${name}: the "${pluginId}" capability is not enabled for this companion. ` +
91
92
  `Enable it via \`${name} companion setup\`.`,
@@ -95,7 +96,7 @@ export async function ensureCapabilityEnabled(
95
96
  }
96
97
 
97
98
  function usageFor(plugin: Plugin): string {
98
- const distributionName = getActiveDistribution().config.name;
99
+ const distributionName = frameworkCommandName();
99
100
  const namespace = namespaceOf(plugin);
100
101
  const lines = (plugin.cliCommands ?? []).map(
101
102
  (command) => ` ${distributionName} cap ${namespace} ${command.name} ${command.description}`,
package/src/cli/usage.ts CHANGED
@@ -1,9 +1,12 @@
1
- import { frameworkConfig } from "../framework";
1
+ import { getActiveDistribution } from "../distribution";
2
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
2
3
 
3
4
  export function usage(): string {
4
- const n = frameworkConfig.name;
5
+ const n = frameworkCommandName();
6
+ const packageName =
7
+ getActiveDistribution().config.update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`;
5
8
  return [
6
- `${n.charAt(0).toUpperCase() + n.slice(1)} CLI (@uniqbit/${n})`,
9
+ `${n.charAt(0).toUpperCase() + n.slice(1)} CLI (${packageName})`,
7
10
  "",
8
11
  "Commands:",
9
12
  ` ${n} install [--yes]`,
@@ -1,5 +1,6 @@
1
1
  import { main } from "./cli/main";
2
2
  import { setActiveDistribution, type DistributionConfig } from "./distribution";
3
+ import { FRAMEWORK_NAME } from "./framework";
3
4
  import { createBunPlugin } from "./tools/setup/package-managers/bun";
4
5
  import { createUvPlugin } from "./tools/setup/package-managers/uv";
5
6
  import type { PluginRegistration } from "./tools/setup/plugin";
@@ -23,15 +24,15 @@ export interface MateCli {
23
24
  * Assemble a distribution from an identity config and plugin registration
24
25
  * entries. Distributions choose their plugin set; the framework always adds
25
26
  * bun and uv as required runtime substrate, plus gitignore management named
26
- * after `config.name` (built last since it reads the full plugin set).
27
- * Returns the runnable CLI.
27
+ * after the framework identity (built last since it reads the full plugin
28
+ * set). Returns the runnable CLI.
28
29
  */
29
30
  export function createMate({ config, plugins, main: runMain = main }: CreateMateOptions): MateCli {
30
31
  const registry = new PluginRegistry([
31
32
  ...plugins,
32
33
  { plugin: createBunPlugin(), policy: "required" },
33
34
  { plugin: createUvPlugin(), policy: "required" },
34
- createGitignorePlugin(config.name),
35
+ createGitignorePlugin(FRAMEWORK_NAME),
35
36
  ]);
36
37
  setActiveDistribution({ config, registry });
37
38
  return {
package/src/framework.ts CHANGED
@@ -1,5 +1,25 @@
1
1
  import { getActiveDistribution } from "./distribution";
2
2
 
3
+ /**
4
+ * Framework identity: names everything identity-shaped regardless of how the
5
+ * CLI is invoked — state directories (`~/.mate`, `.mate/`), managed-block
6
+ * markers, `MATE_*` env values, and the `companion-policy framework`
7
+ * attribute. A whitelabel distribution changes the invocation name only;
8
+ * identity stays `mate`. Use `frameworkCommandName()` for anything the user
9
+ * or an agent types.
10
+ */
11
+ export const FRAMEWORK_NAME = "mate";
12
+
13
+ /**
14
+ * Invocation name: how this distribution's CLI is invoked, derived from the
15
+ * package's `bin` key (`config.name`). Drives usage output, command hints,
16
+ * error prefixes, agent guidance, and permission entries — never paths,
17
+ * markers, or env values (use `FRAMEWORK_NAME` for those).
18
+ */
19
+ export function frameworkCommandName(): string {
20
+ return getActiveDistribution().config.name;
21
+ }
22
+
3
23
  /**
4
24
  * Live view of the active distribution's identity. Framework modules read
5
25
  * identity through this object; the values always come from the config the
@@ -8,7 +8,7 @@ import { CompanionResolver } from "./orchestrator/companion-resolver";
8
8
  import { checkEngineRequirement } from "./orchestrator/engine-guard";
9
9
  import { GlobalConfigStore } from "./orchestrator/global-config-store";
10
10
  import { YamlFileStore } from "./orchestrator/yaml-file-store";
11
- import { frameworkConfig } from "../framework";
11
+ import { FRAMEWORK_NAME } from "../framework";
12
12
  import type { FrameworkConfig } from "./orchestrator/types";
13
13
  import { getActiveDistribution } from "../distribution";
14
14
  import { createBunPlugin } from "../tools/setup/package-managers/bun";
@@ -55,7 +55,7 @@ export interface InstallState {
55
55
  }
56
56
 
57
57
  function defaultInstallStateRoot(): string {
58
- return path.join(os.homedir(), `.${frameworkConfig.name}`, "install-state");
58
+ return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "install-state");
59
59
  }
60
60
 
61
61
  export function getInstallStatePath(
@@ -107,12 +107,7 @@ function coreContext(): InstallContext {
107
107
  }
108
108
 
109
109
  async function contextForCompanion(companionPath: string): Promise<InstallContext> {
110
- const configPath = path.join(
111
- companionPath,
112
- `.${frameworkConfig.name}`,
113
- "config",
114
- "framework.yaml",
115
- );
110
+ const configPath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
116
111
  const config = mergeWithDefaults(await new ConfigStore(configPath).load());
117
112
  return {
118
113
  kind: "companion",
@@ -124,7 +119,7 @@ async function contextForCompanion(companionPath: string): Promise<InstallContex
124
119
 
125
120
  async function hasLocalConfig(cwd: string): Promise<boolean> {
126
121
  try {
127
- await fs.access(path.join(cwd, `.${frameworkConfig.name}`, "config", "framework.yaml"));
122
+ await fs.access(path.join(cwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
128
123
  return true;
129
124
  } catch {
130
125
  return false;
@@ -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 { frameworkConfig } from "../../../framework";
6
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../../../framework";
7
7
  import { getReactDoctorBinPath, getWrapperBinPath } from "../../package-paths";
8
8
  import {
9
9
  GRAPHIFY_OUTPUT_SUBDIR,
@@ -77,7 +77,8 @@ export abstract class LaunchAdapter {
77
77
  const wrapperBinPath = getWrapperBinPath();
78
78
  const env: NodeJS.ProcessEnv = {
79
79
  ...process.env,
80
- MATE_NAME: frameworkConfig.name,
80
+ MATE_NAME: FRAMEWORK_NAME,
81
+ MATE_COMMAND: frameworkCommandName(),
81
82
  MATE_VERSION: version,
82
83
  MATE_ARTIFACT_PATH: context.companionPath,
83
84
  MATE_WRAPPER_BIN_PATH: wrapperBinPath,
@@ -148,7 +149,7 @@ export abstract class LaunchAdapter {
148
149
  command: this.toolName,
149
150
  args: builtArgs,
150
151
  env,
151
- warning: `${frameworkConfig.name}: headroom capability enabled but \`headroom\` was not found on PATH; install with \`uv tool install "headroom-ai[all]"\`; launching ${this.toolName} directly\n`,
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`,
152
153
  };
153
154
  }
154
155
 
@@ -182,7 +183,7 @@ export abstract class LaunchAdapter {
182
183
  command: this.toolName,
183
184
  args: builtArgs,
184
185
  env,
185
- warning: `${frameworkConfig.name}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
186
+ warning: `${frameworkCommandName()}: headroom proxy failed to become ready on port ${port} after ${maxProxyStartAttempts} attempts; launching ${this.toolName} directly${errorDetail}\n`,
186
187
  };
187
188
  }
188
189
  }
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { frameworkConfig } from "../../framework";
3
+ import { FRAMEWORK_NAME } from "../../framework";
4
4
  import { getDefaultSetupSelections } from "./setup-compatibilities";
5
5
  import { YamlFileStore } from "./yaml-file-store";
6
6
  import { type FrameworkConfig } from "./types";
@@ -8,7 +8,7 @@ import { type FrameworkConfig } from "./types";
8
8
  export const RTK_CAPABILITY_SPLIT_MIGRATION = "rtk-capability-split-v1";
9
9
 
10
10
  function defaultConfigPath(): string {
11
- return `.${frameworkConfig.name}/config/framework.yaml`;
11
+ return `.${FRAMEWORK_NAME}/config/framework.yaml`;
12
12
  }
13
13
 
14
14
  export function defaultConfig(): FrameworkConfig {
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
5
5
  import { CompanionResolver } from "./companion-resolver";
6
6
  import { ConfigStore } from "./config-store";
7
7
  import { GlobalConfigStore } from "./global-config-store";
@@ -45,7 +45,7 @@ function makeContext(
45
45
  contextKind: FrameworkContext["contextKind"],
46
46
  repository?: LinkedRepository,
47
47
  ): FrameworkContext {
48
- const configDir = path.join(companionPath, `.${frameworkConfig.name}`, "config");
48
+ const configDir = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config");
49
49
  return {
50
50
  configStore: new ConfigStore(path.join(configDir, "framework.yaml")),
51
51
  workingRepoStore: new WorkingRepoStore(path.join(configDir, "registry.yaml")),
@@ -79,7 +79,7 @@ export async function resolveFrameworkContext(
79
79
  );
80
80
  }
81
81
 
82
- const localDir = path.join(cwd, `.${frameworkConfig.name}`);
82
+ const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
83
83
  const localLegacyDirs = frameworkConfig.legacyNames.map((n) => path.join(cwd, `.${n}`));
84
84
  await migrateConfigDir(localDir, localLegacyDirs);
85
85
 
@@ -170,7 +170,7 @@ export async function resolveForCapability(
170
170
 
171
171
  // Fallback: cwd is the companion directory — resolve from its local config.
172
172
  // This is specific to mate cap; resolveForLaunch does NOT get this fallback.
173
- const localDir = path.join(cwd, `.${frameworkConfig.name}`);
173
+ const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
174
174
  const localLegacyDirs = frameworkConfig.legacyNames.map((n) => path.join(cwd, `.${n}`));
175
175
  await migrateConfigDir(localDir, localLegacyDirs);
176
176
 
@@ -1,7 +1,7 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
- import { frameworkConfig } from "../../framework";
4
+ import { FRAMEWORK_NAME, frameworkConfig } from "../../framework";
5
5
  import { YamlFileStore } from "./yaml-file-store";
6
6
  import { migrateConfigDir } from "./migration";
7
7
 
@@ -21,9 +21,8 @@ function normalizeGlobalConfig(config: GlobalConfig | null): GlobalConfig {
21
21
  };
22
22
  }
23
23
 
24
- // Lazy: the distribution name is only known once createMate has run.
25
24
  function defaultGlobalConfigPath(): string {
26
- return path.join(os.homedir(), `.${frameworkConfig.name}`, "config.yaml");
25
+ return path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "config.yaml");
27
26
  }
28
27
 
29
28
  export class GlobalConfigStore extends YamlFileStore<GlobalConfig> {
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import { parse, stringify } from "yaml";
5
5
 
6
- import { frameworkConfig } from "../../framework";
6
+ import { FRAMEWORK_NAME } from "../../framework";
7
7
  import { resolveGitInfoExcludePath } from "../../tools/setup/git-utils";
8
8
  import { YamlFileStore } from "./yaml-file-store";
9
9
  import type { CompanionSource, LinkedRepository } from "./types";
@@ -19,8 +19,7 @@ export interface RepoLocalRegistry {
19
19
  companions: RepoLocalCompanionPointer[];
20
20
  }
21
21
 
22
- // Lazy: the distribution name is only known once createMate has run.
23
- const repoLocalDirName = () => `.${frameworkConfig.name}`;
22
+ const repoLocalDirName = () => `.${FRAMEWORK_NAME}`;
24
23
  const repoLocalExcludeEntry = () => `${repoLocalDirName()}/`;
25
24
  const repoLocalScanSkipDirNames = () =>
26
25
  new Set([
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import { parse } from "yaml";
5
5
 
6
- import { frameworkConfig } from "../../framework";
6
+ import { FRAMEWORK_NAME, frameworkCommandName, frameworkConfig } from "../../framework";
7
7
  import { fileExists } from "../fs-utils";
8
8
  import { GlobalConfigStore } from "./global-config-store";
9
9
  import { CompanionResolver, type CompanionMatch } from "./companion-resolver";
@@ -127,7 +127,7 @@ export async function looksLikeWorkingRepo(cwd: string): Promise<boolean> {
127
127
  export async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
128
128
  const resolvedCwd = path.resolve(cwd);
129
129
  const candidatePaths = [
130
- path.join(resolvedCwd, `.${frameworkConfig.name}`, "config", "framework.yaml"),
130
+ path.join(resolvedCwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
131
131
  ...frameworkConfig.legacyNames.map((name) =>
132
132
  path.join(resolvedCwd, `.${name}`, "config", "framework.yaml"),
133
133
  ),
@@ -145,7 +145,7 @@ export async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
145
145
  async function hasWorkingRepoConfig(cwd: string): Promise<boolean> {
146
146
  try {
147
147
  const raw = await fs.readFile(
148
- path.join(path.resolve(cwd), `.${frameworkConfig.name}`, "config", "framework.yaml"),
148
+ path.join(path.resolve(cwd), `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
149
149
  "utf8",
150
150
  );
151
151
  const config = parse(raw) as { type?: unknown } | null;
@@ -190,7 +190,7 @@ export async function inspectSetupPreflight(
190
190
 
191
191
  export function formatSetupGuardrailError(cwd: string, match: CompanionMatch): string {
192
192
  return [
193
- `\`${frameworkConfig.name} setup\` cannot run here: ${path.resolve(cwd)} is inside linked working repo \`${match.repositoryId}\`.`,
194
- `Run \`${frameworkConfig.name} setup\` from the companion repo instead: ${match.companionPath}`,
193
+ `\`${frameworkCommandName()} setup\` cannot run here: ${path.resolve(cwd)} is inside linked working repo \`${match.repositoryId}\`.`,
194
+ `Run \`${frameworkCommandName()} setup\` from the companion repo instead: ${match.companionPath}`,
195
195
  ].join("\n");
196
196
  }
@@ -1,12 +1,12 @@
1
1
  import path from "node:path";
2
2
 
3
- import { frameworkConfig } from "../../framework";
3
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../../framework";
4
4
  import { migrateRegistryData } from "./migration";
5
5
  import { YamlFileStore } from "./yaml-file-store";
6
6
  import { ConfigError, type WorkingRepoConfig } from "./types";
7
7
 
8
8
  export function getDefaultWorkingRepoPath(): string {
9
- return `.${frameworkConfig.name}/config/registry.yaml`;
9
+ return `.${FRAMEWORK_NAME}/config/registry.yaml`;
10
10
  }
11
11
 
12
12
  export class WorkingRepoStore extends YamlFileStore<WorkingRepoConfig> {
@@ -21,7 +21,7 @@ export class WorkingRepoStore extends YamlFileStore<WorkingRepoConfig> {
21
21
 
22
22
  protected async onMissing(): Promise<WorkingRepoConfig> {
23
23
  throw new ConfigError(
24
- `Working repo config not found. Please run \`${frameworkConfig.name} companion setup\` to initialize the framework.`,
24
+ `Working repo config not found. Please run \`${frameworkCommandName()} companion setup\` to initialize the framework.`,
25
25
  );
26
26
  }
27
27
 
@@ -2,7 +2,7 @@ import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
4
  import { getActiveDistribution, type DistributionUpdateConfig } from "../distribution";
5
- import { frameworkConfig } from "../framework";
5
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
6
6
  import { fetchPublicPackageVersion, PUBLIC_NPM_REGISTRY } from "./public-npm";
7
7
  import { YamlFileStore } from "./orchestrator/yaml-file-store";
8
8
 
@@ -22,7 +22,7 @@ export const updateCheckerDeps = {
22
22
 
23
23
  export class UpdateStateStore extends YamlFileStore<UpdateState> {
24
24
  constructor() {
25
- super(path.join(os.homedir(), `.${frameworkConfig.name}`, "update-state.yaml"));
25
+ super(path.join(os.homedir(), `.${FRAMEWORK_NAME}`, "update-state.yaml"));
26
26
  }
27
27
 
28
28
  protected onMissing(): Promise<UpdateState> {
@@ -35,9 +35,9 @@ export function getCurrentVersion(): string {
35
35
  }
36
36
 
37
37
  export function getUpdateConfig(): Required<DistributionUpdateConfig> {
38
- const { name, update } = getActiveDistribution().config;
38
+ const { update } = getActiveDistribution().config;
39
39
  return {
40
- packageName: update?.packageName ?? `@uniqbit/${name}`,
40
+ packageName: update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`,
41
41
  registry: update?.registry ?? PUBLIC_NPM_REGISTRY,
42
42
  enforce: update?.enforce ?? false,
43
43
  };
@@ -62,9 +62,9 @@ export async function showUpdateBannerIfAvailable(store: UpdateStateStore): Prom
62
62
  const current = getCurrentVersion();
63
63
  if (!isNewer(state.latestVersion, current)) return;
64
64
  process.stderr.write(
65
- `\n${frameworkConfig.name}: update available (${current} → ${state.latestVersion})\n`,
65
+ `\n${frameworkCommandName()}: update available (${current} → ${state.latestVersion})\n`,
66
66
  );
67
- process.stderr.write(` Run \`${frameworkConfig.name} update\` to upgrade.\n\n`);
67
+ process.stderr.write(` Run \`${frameworkCommandName()} update\` to upgrade.\n\n`);
68
68
  } catch {
69
69
  // never block the main command
70
70
  }
@@ -83,9 +83,9 @@ export async function enforceUpdateIfRequired(store: UpdateStateStore): Promise<
83
83
  const current = getCurrentVersion();
84
84
  if (!isNewer(state.latestVersion, current)) return false;
85
85
  process.stderr.write(
86
- `\n${frameworkConfig.name}: update required (${current} → ${state.latestVersion})\n`,
86
+ `\n${frameworkCommandName()}: update required (${current} → ${state.latestVersion})\n`,
87
87
  );
88
- process.stderr.write(` Run \`${frameworkConfig.name} update\` before continuing.\n\n`);
88
+ process.stderr.write(` Run \`${frameworkCommandName()} update\` before continuing.\n\n`);
89
89
  return true;
90
90
  } catch {
91
91
  return false;
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { frameworkConfig } from "../framework";
3
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
4
4
  import type { AdapterContext } from "../lib/orchestrator/adapters/base";
5
5
  import {
6
6
  hasGraphifyCapability,
@@ -35,7 +35,7 @@ export function buildCodebaseExplorationGuidanceSection(
35
35
  2. If tokensave is empty/file-only/irrelevant, run graphify query "<question>"; use graphify path/explain to deepen.
36
36
  3. Use grep/glob/read only after tokensave and graphify were tried.</order>
37
37
  <notes>Dirty graph files are expected. Use wiki/index.md for broad navigation; GRAPH_REPORT.md only if query/path/explain fall short.</notes>
38
- <post-edit>After code changes, run mate cap index.</post-edit>
38
+ <post-edit>After code changes, run ${frameworkCommandName()} cap index.</post-edit>
39
39
  </codebase-exploration-rules>`;
40
40
  }
41
41
 
@@ -48,7 +48,7 @@ export function buildCodebaseExplorationGuidanceSection(
48
48
  2. Use graphify path "<A>" "<B>" or graphify explain "<concept>" to deepen.
49
49
  3. Use grep/glob/read only after graphify was tried.</order>
50
50
  <notes>Dirty graph files are expected. Use wiki/index.md for broad navigation; GRAPH_REPORT.md only if query/path/explain fall short.</notes>
51
- <post-edit>After code changes, run mate cap index --graphify.</post-edit>
51
+ <post-edit>After code changes, run ${frameworkCommandName()} cap index --graphify.</post-edit>
52
52
  </codebase-exploration-rules>`;
53
53
  }
54
54
 
@@ -57,7 +57,7 @@ export function buildCodebaseExplorationGuidanceSection(
57
57
  <order>tokensave -> grep/glob/read. MUST try tokensave before raw source.
58
58
  1. tokensave_context first.
59
59
  2. Use grep/glob/read only after tokensave is empty or irrelevant.</order>
60
- <post-edit>After code changes, run mate cap index --tokensave.</post-edit>
60
+ <post-edit>After code changes, run ${frameworkCommandName()} cap index --tokensave.</post-edit>
61
61
  </codebase-exploration-rules>`;
62
62
  }
63
63
 
@@ -73,13 +73,12 @@ export function buildCompanionPolicyXml(
73
73
  context: AdapterContext,
74
74
  options: { wrapperBinPath?: string } = {},
75
75
  ): string {
76
- const name = frameworkConfig.name;
77
76
  const wrapperBinPath = options.wrapperBinPath ?? getWrapperBinPath();
78
77
  const lines = [
79
78
  "## MANDATORY RULES - NON-NEGOTIABLE",
80
79
  "",
81
- `<companion-policy framework="${name}" priority="mandatory">`,
82
- ` <overview>You are operating inside the ${name} companion repository.</overview>`,
80
+ `<companion-policy framework="${FRAMEWORK_NAME}" priority="mandatory">`,
81
+ ` <overview>You are operating inside the ${FRAMEWORK_NAME} companion repository.</overview>`,
83
82
  " <context>",
84
83
  " <paths>",
85
84
  ` <path role="working-repository" env="MATE_REPO_PATH">${context.repository.path}</path>`,
@@ -89,7 +88,7 @@ export function buildCompanionPolicyXml(
89
88
  " <cli-tools>",
90
89
  ` <cli name="openspec" type="wrapper" invokeAs="${path.join(wrapperBinPath, "openspec")}" />`,
91
90
  ` <cli name="graphify" type="wrapper" invokeAs="${path.join(wrapperBinPath, "graphify")}" />`,
92
- ' <cli name="mate" type="global" invokeAs="mate" />',
91
+ ` <cli name="${FRAMEWORK_NAME}" type="global" invokeAs="${frameworkCommandName()}" />`,
93
92
  " </cli-tools>",
94
93
  ` <linked-repository id="${context.repository.id}" profile="${context.repository.profile}" />`,
95
94
  " </context>",
@@ -104,7 +103,7 @@ export function buildCompanionPolicyXml(
104
103
 
105
104
  if (hasOpenspecCapability(context.capabilities)) {
106
105
  lines.push(
107
- ' <rule id="openspec-finish" severity="critical">Finish OpenSpec changes ONLY with: mate artifact finish "<name>" --json — never hand-commit or hand-tag a finish. Finishing a still-active change applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>',
106
+ ` <rule id="openspec-finish" severity="critical">Finish OpenSpec changes ONLY with: ${frameworkCommandName()} artifact finish "<name>" --json — never hand-commit or hand-tag a finish. Finishing a still-active change applies its delta specs itself, so do not pre-apply them to openspec/specs right before finishing. Finishing an already-archived change resumes without re-applying delta specs, so an archive flow that already synced specs (e.g. openspec-sync-specs) composes fine with a finish afterwards.</rule>`,
108
107
  );
109
108
  }
110
109
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
  export const MATE_ENV = {
8
8
  frameworkName: "MATE_NAME",
9
+ commandName: "MATE_COMMAND",
9
10
  version: "MATE_VERSION",
10
11
  companionPath: "MATE_ARTIFACT_PATH",
11
12
  wrapperBinPath: "MATE_WRAPPER_BIN_PATH",
@@ -146,9 +146,10 @@ const change = extractArchiveCommand(command) || extractMoveCommand(command);
146
146
  if (!change) process.exit(0);
147
147
  if (isClearFailure(input.tool_response)) process.exit(0);
148
148
 
149
+ const mateCommand = process.env.MATE_COMMAND || "mate";
149
150
  const context =
150
151
  "OpenSpec change " + change + " was just archived. Invoke the mate-artifact-finish " +
151
- "skill, then run `mate artifact finish \"" + change + "\" --json` to complete the " +
152
+ "skill, then run `" + mateCommand + " artifact finish \"" + change + "\" --json` to complete the " +
152
153
  "finish workflow (commit, tag, and push).";
153
154
  process.stdout.write(JSON.stringify({
154
155
  hookSpecificOutput: {
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: mate-artifact-finish
3
- description: Finish a completed artifact in one step via `mate artifact finish`. Use when the user wants to finish, ship, or archive-and-push a completed artifact and anchor it with a dated revert tag.
4
- allowed-tools: Bash(mate:*), Bash(git:*), Bash(openspec:*)
3
+ description: Finish a completed artifact in one step via `{{MATE_COMMAND}} artifact finish`. Use when the user wants to finish, ship, or archive-and-push a completed artifact and anchor it with a dated revert tag.
4
+ allowed-tools: Bash({{MATE_COMMAND}}:*), Bash(git:*), Bash(openspec:*)
5
5
  license: MIT
6
- compatibility: Requires the mate CLI and the openspec capability enabled.
6
+ compatibility: Requires the {{MATE_COMMAND}} CLI and the openspec capability enabled.
7
7
  metadata:
8
8
  author: mate
9
9
  version: "1.2"
@@ -13,7 +13,7 @@ Finish a completed artifact as one deterministic mate workflow and leave a dated
13
13
 
14
14
  ## Workflow
15
15
 
16
- `mate artifact finish` is the deterministic, non-interactive finish pipeline.
16
+ `{{MATE_COMMAND}} artifact finish` is the deterministic, non-interactive finish pipeline.
17
17
 
18
18
  The CLI performs normal work; only conflict recovery requires agent judgment.
19
19
 
@@ -24,10 +24,10 @@ The CLI performs normal work; only conflict recovery requires agent judgment.
24
24
  2. **Run the CLI.**
25
25
 
26
26
  ```bash
27
- mate artifact finish "<artifact-name>" --json
27
+ {{MATE_COMMAND}} artifact finish "<artifact-name>" --json
28
28
  ```
29
29
 
30
- Run it from the companion repository. Finish mutates only that repository; the linked working repository is capability-indexing context. Do not manually invoke `mate cap index`.
30
+ Run it from the companion repository. Finish mutates only that repository; the linked working repository is capability-indexing context. Do not manually invoke `{{MATE_COMMAND}} cap index`.
31
31
 
32
32
  Add `--force` only if the user explicitly wants to override the not-complete guard (validation is never bypassable). Unrelated companion changes are preserved and do not require `--force`. Add `--no-push` for a local-only finish.
33
33
 
@@ -44,8 +44,8 @@ The CLI performs normal work; only conflict recovery requires agent judgment.
44
44
 
45
45
  - **CRITICAL — no manual finishing**: Never hand-commit or hand-tag instead of this skill. For a still-active change the finish pipeline applies delta specs itself (via `openspec archive`) — do not pre-apply them, or produce fails with "already exists". A change whose specs were already synced (e.g. via `openspec-sync-specs`) must be archived first; finish then resumes from the archive without re-applying delta specs.
46
46
  - Always pass `--json` and parse the result; do not scrape human-readable output.
47
- - Never re-run `mate artifact finish` blindly after a `conflict`.
47
+ - Never re-run `{{MATE_COMMAND}} artifact finish` blindly after a `conflict`.
48
48
  - Never auto-resolve a provider-specific conflict you do not understand — ask the user.
49
49
  - Use the JSON fields the CLI returns; do not recompute names, dates, or tags by hand.
50
- - Invoke the CLI as `mate`, never through a companion-local wrapper.
50
+ - Invoke the CLI as `{{MATE_COMMAND}}`, never through a companion-local wrapper.
51
51
  - Only the companion repository is a finish Git target; the working repository is an index input.
@@ -13,7 +13,7 @@ Use this reference when you need the OpenSpec-specific parts of `mate-artifact-f
13
13
 
14
14
  The CLI is **resumable**.
15
15
 
16
- If the change is already archived — because the developer ran `openspec archive` by hand, or because a prior finish partially completed — `mate artifact finish` detects the existing:
16
+ If the change is already archived — because the developer ran `openspec archive` by hand, or because a prior finish partially completed — `{{MATE_COMMAND}} artifact finish` detects the existing:
17
17
 
18
18
  ```text
19
19
  openspec/changes/archive/<date>-<name>/
@@ -21,7 +21,7 @@ openspec/changes/archive/<date>-<name>/
21
21
 
22
22
  It then skips the archive step and continues from commit → tag → push. In that case the result includes `resumed: true`.
23
23
 
24
- This means a developer can archive manually first and still rely on `mate artifact finish` for the commit/tag/push tail.
24
+ This means a developer can archive manually first and still rely on `{{MATE_COMMAND}} artifact finish` for the commit/tag/push tail.
25
25
 
26
26
  ## JSON Contract
27
27
 
@@ -80,7 +80,7 @@ Failure behavior matters:
80
80
 
81
81
  ## OpenSpec Conflict Workflow
82
82
 
83
- If `status` is `conflict`, do **not** rerun `mate artifact finish`.
83
+ If `status` is `conflict`, do **not** rerun `{{MATE_COMMAND}} artifact finish`.
84
84
 
85
85
  At that point:
86
86
 
@@ -4,6 +4,10 @@
4
4
  Artifact-like files may be written in the working repository only when the
5
5
  target path is already ignored by git. That keeps local-only scratch files
6
6
  possible without weakening the default repo split.
7
+
8
+ Editing a file that already exists is always allowed: the guard exists to
9
+ stop new agent artifacts from landing in the working repo, not to freeze
10
+ files that are already part of it.
7
11
  """
8
12
 
9
13
  from __future__ import annotations
@@ -74,6 +78,15 @@ def companion_root() -> str | None:
74
78
  return os.path.normpath(companion) if companion else None
75
79
 
76
80
 
81
+ def claude_config_root() -> str:
82
+ """Claude Code's own config/state directory (plan files, settings, todos).
83
+ Writes there are agent-runtime state, never Mate artifacts."""
84
+ configured = os.environ.get("CLAUDE_CONFIG_DIR")
85
+ if configured:
86
+ return os.path.normpath(configured)
87
+ return os.path.normpath(os.path.join(os.path.expanduser("~"), ".claude"))
88
+
89
+
77
90
  def normalize_path(file_path: str) -> str:
78
91
  if os.path.isabs(file_path):
79
92
  return os.path.normpath(file_path)
@@ -161,7 +174,7 @@ def block_write(target: str, companion: str) -> None:
161
174
  raise SystemExit(2)
162
175
 
163
176
 
164
- def check_file_path(file_path: str, companion: str | None) -> None:
177
+ def check_file_path(file_path: str, companion: str | None, allow_existing: bool = False) -> None:
165
178
  if not file_path or not companion:
166
179
  return
167
180
 
@@ -169,6 +182,12 @@ def check_file_path(file_path: str, companion: str | None) -> None:
169
182
  if normalized.startswith(companion):
170
183
  return
171
184
 
185
+ if is_under(normalized, claude_config_root()):
186
+ return
187
+
188
+ if allow_existing and os.path.isfile(normalized):
189
+ return
190
+
172
191
  if is_product_documentation_path(normalized):
173
192
  return
174
193
 
@@ -203,7 +222,11 @@ def main() -> int:
203
222
  tool_input = payload.get("tool_input", {})
204
223
 
205
224
  if tool_name in {"Write", "Edit", "MultiEdit"}:
206
- check_file_path(str(tool_input.get("file_path", "")), companion)
225
+ check_file_path(
226
+ str(tool_input.get("file_path", "")),
227
+ companion,
228
+ allow_existing=tool_name in {"Edit", "MultiEdit"},
229
+ )
207
230
  return 0
208
231
 
209
232
  if tool_name == "Bash":
@@ -10,7 +10,7 @@ import {
10
10
  runShellCommand,
11
11
  } from "../utils";
12
12
  import { confirm } from "../../../cli/confirm";
13
- import { frameworkConfig } from "../../../framework";
13
+ import { FRAMEWORK_NAME } from "../../../framework";
14
14
  import { isInstalledViaUvTool } from "../package-managers/uv";
15
15
 
16
16
  // Storage contract: <companionPath>/.graphify/<repositoryId>/graphify-out/
@@ -43,9 +43,9 @@ const GRAPHIFY_PROVIDER_DIRS: Record<string, string> = {
43
43
  export const GRAPHIFY_COMPANION_OUT_PREFIX =
44
44
  "$MATE_ARTIFACT_PATH/.graphify/$MATE_REPO_ID/graphify-out/";
45
45
 
46
- // Lazy: marker names derive from the active distribution's identity.
47
- const GRAPHIFY_START = () => `<!-- ${frameworkConfig.name.toUpperCase()}:GRAPHIFY:START -->`;
48
- const GRAPHIFY_END = () => `<!-- ${frameworkConfig.name.toUpperCase()}:GRAPHIFY:END -->`;
46
+ // Marker names derive from the framework identity, never the invocation name.
47
+ const GRAPHIFY_START = () => `<!-- ${FRAMEWORK_NAME.toUpperCase()}:GRAPHIFY:START -->`;
48
+ const GRAPHIFY_END = () => `<!-- ${FRAMEWORK_NAME.toUpperCase()}:GRAPHIFY:END -->`;
49
49
 
50
50
  // Per-provider root agent instruction files used for graphify cleanup on teardown.
51
51
  const GRAPHIFY_AGENT_FILES: Record<string, string> = {
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
 
6
+ import { frameworkCommandName } from "../../../framework";
6
7
  import { getOpenSpecSchemaSelection } from "../../../lib/orchestrator/setup-compatibilities";
7
8
  import { fetchPublicPackageVersion } from "../../../lib/public-npm";
8
9
  import { isNewer } from "../../../lib/update-checker";
@@ -134,6 +135,33 @@ async function teardownToolRuntime(companionPath: string, tool: OpenSpecTool): P
134
135
  await teardownOpenspecSkills(getSkillsDir(companionPath, tool), companionPath);
135
136
  }
136
137
 
138
+ const MATE_COMMAND_PLACEHOLDER = "{{MATE_COMMAND}}";
139
+
140
+ // Like mergeDir, but renders the invocation-name placeholder at deploy time.
141
+ // Files without the placeholder are copied byte-identical.
142
+ export async function deployMateSkillDir(src: string, dest: string): Promise<void> {
143
+ await fs.mkdir(dest, { recursive: true });
144
+ const entries = await fs.readdir(src, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ const srcPath = path.join(src, entry.name);
147
+ const destPath = path.join(dest, entry.name);
148
+ if (entry.isDirectory()) {
149
+ await deployMateSkillDir(srcPath, destPath);
150
+ continue;
151
+ }
152
+ const content = await fs.readFile(srcPath, "utf8");
153
+ if (content.includes(MATE_COMMAND_PLACEHOLDER)) {
154
+ await fs.writeFile(
155
+ destPath,
156
+ content.replaceAll(MATE_COMMAND_PLACEHOLDER, frameworkCommandName()),
157
+ "utf8",
158
+ );
159
+ } else {
160
+ await fs.copyFile(srcPath, destPath);
161
+ }
162
+ }
163
+ }
164
+
137
165
  async function applyMateOpenspecSkills(
138
166
  companionPath: string,
139
167
  tools: OpenSpecTool[],
@@ -141,7 +169,7 @@ async function applyMateOpenspecSkills(
141
169
  for (const tool of tools) {
142
170
  const skillsDir = getSkillsDir(companionPath, tool);
143
171
  for (const skill of MATE_ARTIFACT_SKILLS) {
144
- await mergeDir(path.join(MATE_SKILLS_SOURCE, skill), path.join(skillsDir, skill));
172
+ await deployMateSkillDir(path.join(MATE_SKILLS_SOURCE, skill), path.join(skillsDir, skill));
145
173
  }
146
174
  }
147
175
  }
@@ -1,4 +1,5 @@
1
1
  import { getActiveDistribution } from "../../distribution";
2
+ import { FRAMEWORK_NAME } from "../../framework";
2
3
  import type { FrameworkConfig } from "../../lib/orchestrator/types";
3
4
  import { collectHostingProviders, ContextServiceMediator } from "./context-services";
4
5
  import type { CapabilityPlugin, Plugin, PluginRegistration, SetupContext } from "./plugin";
@@ -106,7 +107,7 @@ export async function executeSetupInstallationPlan(
106
107
  const distribution = getActiveDistribution();
107
108
  const mediator = new ContextServiceMediator({
108
109
  ctx,
109
- frameworkName: distribution.config.name,
110
+ frameworkName: FRAMEWORK_NAME,
110
111
  hostingProviders: collectHostingProviders(plugins, plan.activeProviders),
111
112
  assetOverrideRoots: distribution.config.assetRoots,
112
113
  warn: (message) => {
@@ -1,11 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { frameworkConfig } from "../../../framework";
4
+ import { FRAMEWORK_NAME, frameworkConfig } from "../../../framework";
5
5
  import { removeGraphifySection } from "../capabilities/graphify";
6
6
 
7
- // Lazy: marker names derive from the active distribution's identity.
8
- const upperName = () => frameworkConfig.name.toUpperCase();
7
+ // Marker names derive from the framework identity, never the invocation name.
8
+ const upperName = () => FRAMEWORK_NAME.toUpperCase();
9
9
 
10
10
  function allMateStarts(): string[] {
11
11
  return [
@@ -2,6 +2,7 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
+ import { frameworkCommandName } from "../../../framework";
5
6
  import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
6
7
  import { getWrapperBinPath } from "../../../lib/package-paths";
7
8
  import type { FrameworkConfig } from "../../../lib/orchestrator/types";
@@ -51,10 +52,16 @@ const MANAGED_HOOK_MARKERS = [
51
52
  ];
52
53
 
53
54
  // Base `permissions.allow` entries that Claude gets for Mate-managed workflows.
54
- // Read/Glob are scoped to the companion path so routine reads of skills,
55
- // specs, and change artifacts don't prompt for approval on every file.
55
+ // Read/Edit are scoped to the companion path so routine reads and artifact
56
+ // writes of skills, specs, and change artifacts don't prompt for approval on
57
+ // every file. Claude Code ignores `Glob()` rules for file-permission checks
58
+ // (only Read/Edit rules gate file tools), so no Glob entry is emitted.
56
59
  function getBaseManagedPermissionEntries(companionPath: string): string[] {
57
- return ["Bash(mate:*)", `Read(${companionPath}/**)`, `Glob(${companionPath}/**)`];
60
+ return [
61
+ `Bash(${frameworkCommandName()}:*)`,
62
+ `Read(${companionPath}/**)`,
63
+ `Edit(${companionPath}/**)`,
64
+ ];
58
65
  }
59
66
 
60
67
  const LEGACY_MANAGED_PERMISSION_ENTRIES = [
@@ -73,14 +80,14 @@ function getCapabilityPermissionEntries(): Record<string, string[]> {
73
80
  "Skill(openspec-archive-change)",
74
81
  "Skill(mate-artifact-finish)",
75
82
  "Bash(openspec:*)",
76
- "Bash(mate cap graphify:*)",
83
+ `Bash(${frameworkCommandName()} cap graphify:*)`,
77
84
  `Bash(${path.join(wrapperBinPath, "openspec")}:*)`,
78
85
  ],
79
86
  rtk: ["Bash(rtk:*)"],
80
87
  graphify: [
81
88
  "Skill(graphify)",
82
89
  "Bash(graphify:*)",
83
- "Bash(mate cap graphify:*)",
90
+ `Bash(${frameworkCommandName()} cap graphify:*)`,
84
91
  `Bash(${path.join(wrapperBinPath, "graphify")}:*)`,
85
92
  ],
86
93
  "react-doctor": [
@@ -89,6 +96,12 @@ function getCapabilityPermissionEntries(): Record<string, string[]> {
89
96
  "Bash(npx react-doctor@latest *)",
90
97
  ],
91
98
  tokensave: ["mcp__tokensave__*"],
99
+ // The context-mode Claude plugin exposes its skill and MCP tools under the
100
+ // plugin namespace; pre-seed both so routine routing doesn't prompt.
101
+ "context-mode": [
102
+ "Skill(context-mode:context-mode)",
103
+ "mcp__plugin_context-mode_context-mode__*",
104
+ ],
92
105
  };
93
106
  }
94
107
 
@@ -97,6 +110,10 @@ function getAllManagedPermissionEntries(companionPath: string): Set<string> {
97
110
  ...getBaseManagedPermissionEntries(companionPath),
98
111
  ...Object.values(getCapabilityPermissionEntries()).flat(),
99
112
  ...LEGACY_MANAGED_PERMISSION_ENTRIES,
113
+ // Legacy base entry: Claude Code never matched Glob() rules for file
114
+ // permission checks and warns about them, so setup no longer emits it.
115
+ // Keep it managed so re-running setup strips it from existing installs.
116
+ `Glob(${companionPath}/**)`,
100
117
  ]);
101
118
  }
102
119
 
@@ -2,6 +2,7 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
+ import { frameworkCommandName } from "../../../framework";
5
6
  import {
6
7
  OPENCODE_PLUGIN_PACKAGE_NAME,
7
8
  getOpenCodePluginPackageReference,
@@ -243,9 +244,9 @@ async function warmPluginCacheForSetup(pluginReference: string): Promise<void> {
243
244
 
244
245
  process.stderr.write(
245
246
  [
246
- `mate: could not pre-fetch ${pluginReference} into OpenCode's plugin environment.`,
247
+ `${frameworkCommandName()}: could not pre-fetch ${pluginReference} into OpenCode's plugin environment.`,
247
248
  "The first managed OpenCode launch will download it, which requires registry access.",
248
- "Re-run `mate companion setup` with network access to warm the cache ahead of time.",
249
+ `Re-run \`${frameworkCommandName()} companion setup\` with network access to warm the cache ahead of time.`,
249
250
  ...(warmed.detail ? [`Details: ${warmed.detail}`] : []),
250
251
  ].join("\n") + "\n",
251
252
  );
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- import { frameworkConfig } from "../framework";
5
+ import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
6
6
  import { ConfigStore, mergeWithDefaults } from "../lib/orchestrator/config-store";
7
7
  import { GlobalConfigStore } from "../lib/orchestrator/global-config-store";
8
8
  import {
@@ -72,11 +72,7 @@ export async function updateProjectGitignore(
72
72
  const ctx: SetupContext = { companionPath, config, mode: "sync", activeProviders: [] };
73
73
  const plugins = getActiveDistribution().registry.getAll();
74
74
  const entries = collectManagedGitignoreEntries(ctx, plugins);
75
- await writeManagedGitignoreBlock(
76
- path.join(companionPath, ".gitignore"),
77
- frameworkConfig.name,
78
- entries,
79
- );
75
+ await writeManagedGitignoreBlock(path.join(companionPath, ".gitignore"), FRAMEWORK_NAME, entries);
80
76
  }
81
77
 
82
78
  export async function syncCompanionFiles(
@@ -93,14 +89,17 @@ export async function syncCompanionFiles(
93
89
  );
94
90
  }
95
91
 
96
- function mateFolderReadme(): string {
97
- const n = frameworkConfig.name;
92
+ // Exported for the distribution-identity tests.
93
+ export function mateFolderReadme(): string {
94
+ const n = frameworkCommandName();
95
+ const packageName =
96
+ getActiveDistribution().config.update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`;
98
97
  return [
99
- `# .${n}`,
98
+ `# .${FRAMEWORK_NAME}`,
100
99
  ``,
101
- `This directory is managed by the **${n}** companion framework (\`@uniqbit/${n}\`).`,
100
+ `This directory is managed by the **${FRAMEWORK_NAME}** companion framework (\`${packageName}\`).`,
102
101
  ``,
103
- `The ${n} framework keeps your AI agent's companion artifacts separate from the code it works on.`,
102
+ `The ${FRAMEWORK_NAME} framework keeps your AI agent's companion artifacts separate from the code it works on.`,
104
103
  `Specs, notes, and agent config live here; code stays in the linked working repository.`,
105
104
  ``,
106
105
  `## Common commands`,
@@ -118,7 +117,7 @@ function mateFolderReadme(): string {
118
117
  ``,
119
118
  `## Configuration`,
120
119
  ``,
121
- `Edit \`.${n}/config/framework.yaml\` to configure:`,
120
+ `Edit \`.${FRAMEWORK_NAME}/config/framework.yaml\` to configure:`,
122
121
  ``,
123
122
  `- **profiles** — per-profile allowed agents list`,
124
123
  `- **capabilities** — skill and CLI tool capabilities (e.g. react-doctor, openspec, tokensave, headroom, rtk)`,
@@ -128,7 +127,7 @@ function mateFolderReadme(): string {
128
127
  }
129
128
 
130
129
  async function writeMateReadme(companionPath: string): Promise<void> {
131
- const readmePath = path.join(companionPath, `.${frameworkConfig.name}`, "README.md");
130
+ const readmePath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "README.md");
132
131
  await fs.mkdir(path.dirname(readmePath), { recursive: true });
133
132
  await fs.writeFile(readmePath, mateFolderReadme(), "utf8");
134
133
  }
@@ -158,7 +157,7 @@ export async function executeSetup(
158
157
 
159
158
  const configStore =
160
159
  deps.configStore ??
161
- new ConfigStore(path.join(cwd, `.${frameworkConfig.name}`, "config", "framework.yaml"));
160
+ new ConfigStore(path.join(cwd, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"));
162
161
  const config = mergeWithDefaults(await configStore.load());
163
162
  const defaultProfile = config.profiles.default;
164
163