@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.
- package/package.json +1 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/config.ts +2 -2
- package/src/cli/commands/doctor.ts +3 -3
- package/src/cli/commands/install.ts +15 -6
- package/src/cli/commands/launch/shared.ts +6 -3
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/update.ts +9 -9
- package/src/cli/main.ts +5 -2
- package/src/cli/plugin-commands.ts +3 -2
- package/src/cli/usage.ts +6 -3
- package/src/create-mate.ts +4 -3
- package/src/framework.ts +20 -0
- package/src/lib/install.ts +4 -9
- package/src/lib/orchestrator/adapters/base.ts +5 -4
- package/src/lib/orchestrator/config-store.ts +2 -2
- package/src/lib/orchestrator/framework-context.ts +4 -4
- package/src/lib/orchestrator/global-config-store.ts +2 -3
- package/src/lib/orchestrator/repo-local-registry.ts +2 -3
- package/src/lib/orchestrator/setup-preflight.ts +5 -5
- package/src/lib/orchestrator/working-repo-store.ts +3 -3
- package/src/lib/update-checker.ts +8 -8
- package/src/playbooks/companion-guidance.ts +8 -9
- package/src/runtime/env.ts +1 -0
- package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +2 -1
- package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/SKILL.md +8 -8
- package/src/templates/capabilities/openspec-cap/mate-skills/mate-artifact-finish/references/openspec.md +3 -3
- package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +25 -2
- package/src/tools/setup/capabilities/graphify.ts +4 -4
- package/src/tools/setup/capabilities/openspec.ts +29 -1
- package/src/tools/setup/engine.ts +2 -1
- package/src/tools/setup/plugins/guidance.ts +3 -3
- package/src/tools/setup/providers/claude.ts +22 -5
- package/src/tools/setup/providers/opencode.ts +3 -2
- package/src/tools/setup.ts +13 -14
package/package.json
CHANGED
|
@@ -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 {
|
|
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(), `.${
|
|
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 {
|
|
4
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
5
|
|
|
6
|
-
const getGlobalConfigDir = () => path.join(os.homedir(), `.${
|
|
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, `.${
|
|
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, `.${
|
|
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(
|
|
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
|
-
|
|
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(
|
|
79
|
+
process.stderr.write(
|
|
80
|
+
`${frameworkCommandName()}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
|
|
81
|
+
);
|
|
77
82
|
}
|
|
78
|
-
process.stderr.write(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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 ${
|
|
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, `.${
|
|
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 {
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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(`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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
|
|
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(
|
|
85
|
-
|
|
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 =
|
|
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 =
|
|
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 {
|
|
1
|
+
import { getActiveDistribution } from "../distribution";
|
|
2
|
+
import { FRAMEWORK_NAME, frameworkCommandName } from "../framework";
|
|
2
3
|
|
|
3
4
|
export function usage(): string {
|
|
4
|
-
const n =
|
|
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 (
|
|
9
|
+
`${n.charAt(0).toUpperCase() + n.slice(1)} CLI (${packageName})`,
|
|
7
10
|
"",
|
|
8
11
|
"Commands:",
|
|
9
12
|
` ${n} install [--yes]`,
|
package/src/create-mate.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
package/src/lib/install.ts
CHANGED
|
@@ -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 {
|
|
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(), `.${
|
|
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, `.${
|
|
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 {
|
|
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:
|
|
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: `${
|
|
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: `${
|
|
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 {
|
|
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 `.${
|
|
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, `.${
|
|
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, `.${
|
|
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, `.${
|
|
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(), `.${
|
|
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 {
|
|
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
|
-
|
|
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, `.${
|
|
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), `.${
|
|
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
|
-
`\`${
|
|
194
|
-
`Run \`${
|
|
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 {
|
|
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 `.${
|
|
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 \`${
|
|
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 {
|
|
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(), `.${
|
|
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 {
|
|
38
|
+
const { update } = getActiveDistribution().config;
|
|
39
39
|
return {
|
|
40
|
-
packageName: update?.packageName ?? `@uniqbit/${
|
|
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${
|
|
65
|
+
`\n${frameworkCommandName()}: update available (${current} → ${state.latestVersion})\n`,
|
|
66
66
|
);
|
|
67
|
-
process.stderr.write(` Run \`${
|
|
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${
|
|
86
|
+
`\n${frameworkCommandName()}: update required (${current} → ${state.latestVersion})\n`,
|
|
87
87
|
);
|
|
88
|
-
process.stderr.write(` Run \`${
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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="${
|
|
82
|
-
` <overview>You are operating inside the ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/runtime/env.ts
CHANGED
|
@@ -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 `
|
|
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 `
|
|
4
|
-
allowed-tools: Bash(
|
|
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
|
|
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
|
-
`
|
|
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
|
-
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 — `
|
|
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 `
|
|
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 `
|
|
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(
|
|
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 {
|
|
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
|
-
//
|
|
47
|
-
const GRAPHIFY_START = () => `<!-- ${
|
|
48
|
-
const 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
|
|
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:
|
|
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
|
-
//
|
|
8
|
-
const upperName = () =>
|
|
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/
|
|
55
|
-
// specs, and change artifacts don't prompt for approval on
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|
package/src/tools/setup.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
`# .${
|
|
98
|
+
`# .${FRAMEWORK_NAME}`,
|
|
100
99
|
``,
|
|
101
|
-
`This directory is managed by the **${
|
|
100
|
+
`This directory is managed by the **${FRAMEWORK_NAME}** companion framework (\`${packageName}\`).`,
|
|
102
101
|
``,
|
|
103
|
-
`The ${
|
|
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 \`.${
|
|
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, `.${
|
|
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, `.${
|
|
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
|
|