@uniqbit/mate-core 0.15.5 → 0.16.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/claude-plugin/hooks/hooks.json +3 -6
- package/claude-plugin/hooks/session-guidance.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +19 -0
- package/package.json +6 -4
- package/src/cli/commands/artifact/artifact.ts +19 -3
- package/src/cli/commands/artifact/finish/command.ts +183 -57
- package/src/cli/commands/artifact/finish/engine.ts +80 -91
- package/src/cli/commands/artifact/finish/finisher.ts +26 -33
- package/src/cli/commands/artifact/finish/git.ts +34 -23
- package/src/cli/commands/artifact/finish/index.ts +9 -3
- package/src/cli/commands/artifact/finish/openspec.ts +225 -97
- package/src/cli/commands/artifact/pending/command.ts +175 -0
- package/src/cli/commands/artifact/pending/discovery.ts +249 -0
- package/src/cli/commands/artifact/pending/index.ts +17 -0
- package/src/cli/commands/cap/index-cmd.ts +9 -1
- package/src/cli/commands/cap/index.ts +2 -6
- package/src/cli/commands/companion/companion.ts +5 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/companion/sync.ts +92 -0
- package/src/cli/commands/doctor.ts +0 -3
- package/src/cli/commands/launch/shared.ts +23 -5
- package/src/cli/commands/report/collector.ts +72 -78
- package/src/cli/commands/report/contract.ts +40 -1
- package/src/cli/commands/report/highlight.ts +27 -0
- package/src/cli/commands/report/index.ts +11 -18
- package/src/cli/commands/report/renderer.ts +199 -2
- package/src/cli/commands/report/types.ts +26 -1
- package/src/cli/commands/shared/companion-selection.ts +107 -10
- package/src/cli/commands/studio/areas.ts +68 -0
- package/src/cli/commands/studio/index.ts +69 -0
- package/src/cli/commands/studio/inventory.ts +55 -0
- package/src/cli/commands/studio/mate-inventory.ts +43 -0
- package/src/cli/commands/studio/openspec-cli.ts +198 -0
- package/src/cli/commands/studio/payload.ts +184 -0
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +61 -0
- package/src/cli/commands/studio/server.ts +201 -0
- package/src/cli/commands/studio/snapshot.ts +63 -0
- package/src/cli/commands/studio/topology.ts +199 -0
- package/src/cli/commands/studio/views/client.ts +197 -0
- package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
- package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
- package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
- package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
- package/src/cli/commands/studio/views/document.tsx +256 -0
- package/src/cli/commands/studio/views/error.tsx +20 -0
- package/src/cli/commands/studio/views/model.ts +34 -0
- package/src/cli/commands/studio/views/pairings.tsx +38 -0
- package/src/cli/commands/studio/views/skills/index.tsx +87 -0
- package/src/cli/commands/studio/views/specs/index.tsx +101 -0
- package/src/cli/commands/studio/views/styles.ts +389 -0
- package/src/cli/commands/studio/views/warnings.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
- package/src/cli/commands/unwrap.ts +70 -0
- package/src/cli/commands/wrap.ts +164 -0
- package/src/cli/main.ts +68 -19
- package/src/cli/parse-flags.ts +36 -11
- package/src/cli/usage.ts +12 -3
- package/src/framework.ts +1 -7
- package/src/hooks/session-banner.ts +64 -11
- package/src/hooks/session-guidance.ts +40 -0
- package/src/hooks/validate-artifact-path.ts +108 -35
- package/src/lib/fs-utils.ts +9 -0
- package/src/lib/install.ts +33 -0
- package/src/lib/orchestrator/adapters/base.ts +14 -125
- package/src/lib/orchestrator/adapters/claude.ts +0 -11
- package/src/lib/orchestrator/adapters/opencode.ts +2 -32
- package/src/lib/orchestrator/companion-git-sync.ts +94 -84
- package/src/lib/orchestrator/config-store.ts +2 -21
- package/src/lib/orchestrator/editor.ts +12 -22
- package/src/lib/orchestrator/framework-context.ts +17 -6
- package/src/lib/orchestrator/global-config-store.ts +1 -1
- package/src/lib/orchestrator/launcher.ts +97 -7
- package/src/lib/orchestrator/opencode-guidance.ts +4 -56
- package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
- package/src/lib/orchestrator/projection-companion-link.ts +62 -0
- package/src/lib/orchestrator/projection-entries.ts +377 -0
- package/src/lib/orchestrator/projection-record.ts +56 -0
- package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
- package/src/lib/orchestrator/projection-types.ts +169 -0
- package/src/lib/orchestrator/repo-local-registry.ts +37 -133
- package/src/lib/orchestrator/repo-local-store.ts +96 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
- package/src/lib/orchestrator/types.ts +1 -0
- package/src/lib/orchestrator/working-repo-projection.ts +366 -0
- package/src/lib/orchestrator/workspace-inventory.ts +1 -1
- package/src/lib/package-paths.ts +11 -1
- package/src/opencode/companion-hooks.ts +89 -245
- package/src/opencode/companion-policy.ts +35 -10
- package/src/opencode/index.ts +1 -0
- package/src/opencode/projected-guidance.ts +56 -0
- package/src/opencode/tui.tsx +13 -4
- package/src/playbooks/companion-guidance.ts +32 -116
- package/src/plugins.ts +0 -1
- package/src/runtime/companion-git-state.ts +156 -0
- package/src/runtime/companion-git.ts +203 -0
- package/src/runtime/companion-guidance.ts +222 -0
- package/src/runtime/companion-sync.ts +298 -0
- package/src/runtime/env-names.ts +30 -0
- package/src/runtime/env.ts +67 -35
- package/src/runtime/framework.ts +10 -0
- package/src/runtime/freshness.ts +58 -0
- package/src/runtime/index.ts +104 -0
- package/src/runtime/install.ts +30 -0
- package/src/runtime/policy.ts +66 -0
- package/src/runtime/projected-guidance.ts +45 -0
- package/src/runtime/projection.ts +224 -0
- package/src/runtime/repo-local.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
- package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +147 -0
- package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +147 -0
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/report-assets/README.md +32 -0
- package/src/templates/report-assets/mermaid.LICENSE +21 -0
- package/src/templates/report-assets/mermaid.min.js +4376 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
- package/src/tools/setup/capabilities/graphify.ts +16 -7
- package/src/tools/setup/capabilities/openspec.ts +63 -56
- package/src/tools/setup/capabilities/tokensave.ts +47 -0
- package/src/tools/setup/engine.ts +34 -6
- package/src/tools/setup/mate.ts +42 -13
- package/src/tools/setup/plugin.ts +9 -0
- package/src/tools/setup/plugins/guidance.ts +11 -1
- package/src/tools/setup/providers/claude-format.ts +49 -4
- package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
- package/src/tools/setup/providers/claude.ts +55 -220
- package/src/tools/setup/providers/opencode.ts +41 -14
- package/src/tools/setup/runtime-documents.ts +174 -0
- package/src/tools/setup/surface-target.ts +50 -0
- package/src/tools/setup/working-repo-cleanup.ts +33 -26
- package/src/tools/setup/working-repo-local-state.ts +21 -1
- package/src/tools/setup.ts +25 -3
- package/wrappers/bin/graphify +57 -8
- package/wrappers/bin/openspec +50 -3
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
- package/src/cli/commands/cap/headroom.ts +0 -52
- package/src/cli/commands/workspace/list.ts +0 -25
- package/src/cli/commands/workspace/materialize.ts +0 -46
- package/src/cli/commands/workspace/workspace.ts +0 -22
- package/src/hooks/artifact-finish-nudge.ts +0 -244
- package/src/lib/orchestrator/headroom/proxy.ts +0 -116
- package/src/lib/orchestrator/workspace-materialize.ts +0 -80
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
- package/src/tools/setup/capabilities/headroom.ts +0 -57
- /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
// Git index and merge operations must remain sequential.
|
|
2
2
|
// oxlint-disable no-await-in-loop
|
|
3
|
-
import { execFile as execFileCallback } from "node:child_process";
|
|
3
|
+
import { execFile as execFileCallback, spawn as spawnChild } from "node:child_process";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
describeGitFailure,
|
|
10
|
+
gitEnvironment,
|
|
11
|
+
isAuthenticationFailure,
|
|
12
|
+
outputLines,
|
|
13
|
+
resolveUpstreamTargetWith,
|
|
14
|
+
type GitResult,
|
|
15
|
+
} from "../../runtime/companion-git";
|
|
16
|
+
import { recordCompanionSync } from "../../runtime/companion-git-state";
|
|
8
17
|
import { LaunchPreflightError } from "./types";
|
|
9
18
|
|
|
10
19
|
const execFile = promisify(execFileCallback);
|
|
@@ -12,13 +21,17 @@ const execFile = promisify(execFileCallback);
|
|
|
12
21
|
/** Bounded but far above any diagnostic output; default 1 MiB kills git mid-merge on large trees. */
|
|
13
22
|
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
14
23
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
stderr: string;
|
|
19
|
-
}
|
|
24
|
+
export type GitCommandResult = GitResult;
|
|
25
|
+
|
|
26
|
+
export { describeGitFailure };
|
|
20
27
|
|
|
21
|
-
export type
|
|
28
|
+
export type GitExecutionMode = "captured" | "interactive";
|
|
29
|
+
|
|
30
|
+
export type GitRunner = (
|
|
31
|
+
args: readonly string[],
|
|
32
|
+
cwd: string,
|
|
33
|
+
mode?: GitExecutionMode,
|
|
34
|
+
) => Promise<GitCommandResult>;
|
|
22
35
|
|
|
23
36
|
export interface CompanionGitSyncResult {
|
|
24
37
|
skipped: boolean;
|
|
@@ -69,19 +82,39 @@ export class CompanionGitSyncError extends LaunchPreflightError {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
export const companionGitSyncDeps: { runGit: GitRunner } = {
|
|
72
|
-
runGit: async (args, cwd) => {
|
|
85
|
+
runGit: async (args, cwd, mode = "captured") => {
|
|
86
|
+
const captured = gitEnvironment(process.env);
|
|
87
|
+
const { GIT_TERMINAL_PROMPT: _prompt, ...interactive } = captured;
|
|
88
|
+
const env = mode === "captured" ? captured : interactive;
|
|
89
|
+
|
|
90
|
+
if (mode === "interactive") {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
let settled = false;
|
|
93
|
+
const finish = (result: GitCommandResult) => {
|
|
94
|
+
if (settled) return;
|
|
95
|
+
settled = true;
|
|
96
|
+
resolve(result);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const child = spawnChild("git", [...args], {
|
|
100
|
+
cwd,
|
|
101
|
+
env,
|
|
102
|
+
stdio: "inherit",
|
|
103
|
+
});
|
|
104
|
+
child.once("error", (error) => {
|
|
105
|
+
finish({ status: 1, stdout: "", stderr: error.message });
|
|
106
|
+
});
|
|
107
|
+
child.once("close", (status) => {
|
|
108
|
+
finish({ status: status ?? 1, stdout: "", stderr: "" });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
73
113
|
try {
|
|
74
|
-
const {
|
|
75
|
-
GIT_DIR: _gitDir,
|
|
76
|
-
GIT_WORK_TREE: _gitWorkTree,
|
|
77
|
-
GIT_COMMON_DIR: _gitCommonDir,
|
|
78
|
-
GIT_INDEX_FILE: _gitIndexFile,
|
|
79
|
-
...gitEnv
|
|
80
|
-
} = process.env;
|
|
81
114
|
const result = await execFile("git", [...args], {
|
|
82
115
|
cwd,
|
|
83
116
|
encoding: "utf8",
|
|
84
|
-
env
|
|
117
|
+
env,
|
|
85
118
|
maxBuffer: GIT_MAX_BUFFER,
|
|
86
119
|
});
|
|
87
120
|
return { status: 0, stdout: String(result.stdout), stderr: String(result.stderr) };
|
|
@@ -114,32 +147,14 @@ function isManagedPath(filePath: string): boolean {
|
|
|
114
147
|
return MANAGED_ROOTS.some((root) => normalized === root || normalized.startsWith(`${root}/`));
|
|
115
148
|
}
|
|
116
149
|
|
|
117
|
-
function outputLines(stdout: string): string[] {
|
|
118
|
-
return stdout
|
|
119
|
-
.split("\n")
|
|
120
|
-
.map((line) => line.trim())
|
|
121
|
-
.filter(Boolean);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Matches Git progress-meter lines such as `Updating files: 76% (11340/14790)` or `..., done.` */
|
|
125
|
-
const GIT_PROGRESS_LINE = /^\S.*?: +\d+% \(\d+\/\d+\)(?:, done\.)?$/;
|
|
126
|
-
|
|
127
|
-
function stripGitProgress(text: string): string {
|
|
128
|
-
return text
|
|
129
|
-
.split(/\r\n|\r|\n/)
|
|
130
|
-
.filter((line) => !GIT_PROGRESS_LINE.test(line.trim()))
|
|
131
|
-
.join("\n")
|
|
132
|
-
.trim();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function describeGitFailure(result: GitCommandResult): string {
|
|
136
|
-
return stripGitProgress(result.stderr) || stripGitProgress(result.stdout) || "unknown Git error";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
150
|
export class CompanionGitSync {
|
|
140
151
|
constructor(private readonly runGit: GitRunner = companionGitSyncDeps.runGit) {}
|
|
141
152
|
|
|
142
|
-
async sync(
|
|
153
|
+
async sync(
|
|
154
|
+
companionPath: string,
|
|
155
|
+
workingRepoPath?: string,
|
|
156
|
+
interactiveGit = false,
|
|
157
|
+
): Promise<CompanionGitSyncResult> {
|
|
143
158
|
if (!(await this.assertSafeCompanionRoot(companionPath, workingRepoPath))) {
|
|
144
159
|
return { skipped: true, changed: false, companionPath };
|
|
145
160
|
}
|
|
@@ -157,13 +172,30 @@ export class CompanionGitSync {
|
|
|
157
172
|
}
|
|
158
173
|
const dirty = outputLines(before.stdout).length > 0;
|
|
159
174
|
|
|
160
|
-
const fetch = await this.command(
|
|
175
|
+
const fetch = await this.command(
|
|
176
|
+
companionPath,
|
|
177
|
+
["fetch", target.remote, target.branch],
|
|
178
|
+
interactiveGit ? "interactive" : undefined,
|
|
179
|
+
);
|
|
161
180
|
if (fetch.status !== 0) {
|
|
181
|
+
const authenticationFailure = isAuthenticationFailure(fetch);
|
|
182
|
+
const recovery = interactiveGit
|
|
183
|
+
? authenticationFailure
|
|
184
|
+
? "Retry the launch and complete Git authentication, or use the `--no-git` bypass."
|
|
185
|
+
: "Check the Git or SSH diagnostics above, then retry the launch."
|
|
186
|
+
: authenticationFailure
|
|
187
|
+
? "Provide credentials through a non-interactive Git credential helper or SSH agent, retry from a terminal, or use the `--no-git` bypass."
|
|
188
|
+
: "Check the remote and network, or resolve the issue before retrying.";
|
|
189
|
+
const detail = this.detail(fetch);
|
|
190
|
+
const failureDetail =
|
|
191
|
+
interactiveGit && detail === "unknown Git error"
|
|
192
|
+
? "Git fetch failed; review the Git or SSH diagnostics above."
|
|
193
|
+
: detail;
|
|
162
194
|
throw this.failure(
|
|
163
195
|
companionPath,
|
|
164
|
-
`Unable to fetch ${target.ref}: ${
|
|
196
|
+
`Unable to fetch ${target.ref}: ${failureDetail}`,
|
|
165
197
|
[],
|
|
166
|
-
|
|
198
|
+
recovery,
|
|
167
199
|
);
|
|
168
200
|
}
|
|
169
201
|
|
|
@@ -207,6 +239,8 @@ export class CompanionGitSync {
|
|
|
207
239
|
}
|
|
208
240
|
|
|
209
241
|
const headAfter = await this.command(companionPath, ["rev-parse", "HEAD"]);
|
|
242
|
+
/** Best-effort bookkeeping: a launch that synchronized must not fail on it. */
|
|
243
|
+
recordCompanionSync(companionPath);
|
|
210
244
|
return {
|
|
211
245
|
skipped: false,
|
|
212
246
|
changed: headBefore.stdout.trim() !== headAfter.stdout.trim(),
|
|
@@ -319,35 +353,13 @@ export class CompanionGitSync {
|
|
|
319
353
|
}
|
|
320
354
|
}
|
|
321
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Delegates to `runtime/`: the blind half resolves the same ref, and the two
|
|
358
|
+
* must never disagree about which upstream they mean.
|
|
359
|
+
*/
|
|
322
360
|
private async resolveSyncTarget(companionPath: string): Promise<SyncTarget> {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
"--abbrev-ref",
|
|
326
|
-
"--symbolic-full-name",
|
|
327
|
-
"@{u}",
|
|
328
|
-
]);
|
|
329
|
-
const configured = this.parseRemoteRef(upstream.stdout);
|
|
330
|
-
if (upstream.status === 0 && configured) return configured;
|
|
331
|
-
|
|
332
|
-
const remoteHead = await this.command(companionPath, [
|
|
333
|
-
"symbolic-ref",
|
|
334
|
-
"--short",
|
|
335
|
-
"refs/remotes/origin/HEAD",
|
|
336
|
-
]);
|
|
337
|
-
const fromRemoteHead = this.parseRemoteRef(remoteHead.stdout);
|
|
338
|
-
if (remoteHead.status === 0 && fromRemoteHead) return fromRemoteHead;
|
|
339
|
-
|
|
340
|
-
for (const branch of ["main", "master"]) {
|
|
341
|
-
const exists = await this.command(companionPath, [
|
|
342
|
-
"show-ref",
|
|
343
|
-
"--verify",
|
|
344
|
-
"--quiet",
|
|
345
|
-
`refs/remotes/origin/${branch}`,
|
|
346
|
-
]);
|
|
347
|
-
if (exists.status === 0) {
|
|
348
|
-
return { remote: "origin", branch, ref: `origin/${branch}` };
|
|
349
|
-
}
|
|
350
|
-
}
|
|
361
|
+
const target = await resolveUpstreamTargetWith((args) => this.command(companionPath, args));
|
|
362
|
+
if (target) return target;
|
|
351
363
|
|
|
352
364
|
throw this.failure(
|
|
353
365
|
companionPath,
|
|
@@ -357,17 +369,6 @@ export class CompanionGitSync {
|
|
|
357
369
|
);
|
|
358
370
|
}
|
|
359
371
|
|
|
360
|
-
private parseRemoteRef(stdout: string): SyncTarget | undefined {
|
|
361
|
-
const ref = stdout.trim();
|
|
362
|
-
const separator = ref.indexOf("/");
|
|
363
|
-
if (separator <= 0 || separator === ref.length - 1) return undefined;
|
|
364
|
-
return {
|
|
365
|
-
remote: ref.slice(0, separator),
|
|
366
|
-
branch: ref.slice(separator + 1),
|
|
367
|
-
ref,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
372
|
private async assertNoUnfinishedOperation(companionPath: string): Promise<void> {
|
|
372
373
|
const unfinished = [
|
|
373
374
|
"MERGE_HEAD",
|
|
@@ -464,8 +465,12 @@ export class CompanionGitSync {
|
|
|
464
465
|
}
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
private async command(
|
|
468
|
-
|
|
468
|
+
private async command(
|
|
469
|
+
companionPath: string,
|
|
470
|
+
args: readonly string[],
|
|
471
|
+
mode?: GitExecutionMode,
|
|
472
|
+
): Promise<GitCommandResult> {
|
|
473
|
+
return mode ? this.runGit(args, companionPath, mode) : this.runGit(args, companionPath);
|
|
469
474
|
}
|
|
470
475
|
|
|
471
476
|
private detail(result: GitCommandResult): string {
|
|
@@ -485,6 +490,11 @@ export class CompanionGitSync {
|
|
|
485
490
|
export async function syncCompanionGit(
|
|
486
491
|
companionPath: string,
|
|
487
492
|
workingRepoPath?: string,
|
|
493
|
+
interactiveGit = false,
|
|
488
494
|
): Promise<CompanionGitSyncResult> {
|
|
489
|
-
return new CompanionGitSync(companionGitSyncDeps.runGit).sync(
|
|
495
|
+
return new CompanionGitSync(companionGitSyncDeps.runGit).sync(
|
|
496
|
+
companionPath,
|
|
497
|
+
workingRepoPath,
|
|
498
|
+
interactiveGit,
|
|
499
|
+
);
|
|
490
500
|
}
|
|
@@ -5,8 +5,6 @@ import { getDefaultSetupSelections } from "./setup-compatibilities";
|
|
|
5
5
|
import { YamlFileStore } from "./yaml-file-store";
|
|
6
6
|
import { ConfigError, type FrameworkConfig } from "./types";
|
|
7
7
|
|
|
8
|
-
export const RTK_CAPABILITY_SPLIT_MIGRATION = "rtk-capability-split-v1";
|
|
9
|
-
|
|
10
8
|
const HUB_MEMBER_SOURCE_KINDS = ["git", "local"] as const;
|
|
11
9
|
|
|
12
10
|
function defaultConfigPath(): string {
|
|
@@ -57,20 +55,6 @@ export function mergeWithDefaults(existing: FrameworkConfig): FrameworkConfig {
|
|
|
57
55
|
};
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
export function migrateRtkCapabilitySplit(config: FrameworkConfig): FrameworkConfig {
|
|
61
|
-
const migrations = config.migrations ?? [];
|
|
62
|
-
if (migrations.includes(RTK_CAPABILITY_SPLIT_MIGRATION)) return config;
|
|
63
|
-
|
|
64
|
-
const capabilities = config.capabilities ?? [];
|
|
65
|
-
const hasHeadroom = capabilities.some((capability) => capability.name === "headroom");
|
|
66
|
-
const hasRtk = capabilities.some((capability) => capability.name === "rtk");
|
|
67
|
-
return {
|
|
68
|
-
...config,
|
|
69
|
-
capabilities: hasHeadroom && !hasRtk ? [...capabilities, { name: "rtk" }] : capabilities,
|
|
70
|
-
migrations: [...migrations, RTK_CAPABILITY_SPLIT_MIGRATION],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
58
|
/** Validates the shape of a hub manifest before it is used. */
|
|
75
59
|
export function validateHubConfig(config: FrameworkConfig): void {
|
|
76
60
|
if (config.type !== "hub") {
|
|
@@ -166,14 +150,11 @@ export class ConfigStore extends YamlFileStore<FrameworkConfig> {
|
|
|
166
150
|
const merged = mergeWithDefaults(migrateProfilesToAllowedAgents(await super.load()));
|
|
167
151
|
validateHubConfig(merged);
|
|
168
152
|
validatePluginDeclarations(merged);
|
|
169
|
-
|
|
170
|
-
const config = migrateRtkCapabilitySplit(merged);
|
|
171
|
-
if (needsMigration) await this.save(config);
|
|
172
|
-
return config;
|
|
153
|
+
return merged;
|
|
173
154
|
}
|
|
174
155
|
|
|
175
156
|
protected async onMissing(): Promise<FrameworkConfig> {
|
|
176
|
-
const config =
|
|
157
|
+
const config = defaultConfig();
|
|
177
158
|
await this.save(config);
|
|
178
159
|
return config;
|
|
179
160
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
|
|
5
|
+
import { workspaceDocumentPath } from "./projection-entries";
|
|
6
|
+
import { project } from "./working-repo-projection";
|
|
7
|
+
|
|
6
8
|
type SpawnLike = typeof spawn;
|
|
7
9
|
|
|
8
10
|
const CURSOR_FALLBACK_PATHS = [
|
|
@@ -75,9 +77,8 @@ export function writeMissingEditorCliGuidance(
|
|
|
75
77
|
write(`mate: run "Install '${cli}' command in PATH" from the Command Palette to enable this.\n`);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
80
|
+
/** The Projection Root owns the path; this is the editor's name for it. */
|
|
81
|
+
export const editorWorkspacePath = workspaceDocumentPath;
|
|
81
82
|
|
|
82
83
|
export interface WorkspaceDocument {
|
|
83
84
|
workspacePath: string;
|
|
@@ -87,29 +88,18 @@ export interface WorkspaceDocument {
|
|
|
87
88
|
/**
|
|
88
89
|
* Writes the generated `.mate/workspace.code-workspace` file (working
|
|
89
90
|
* repository first, companion second) and returns its path and folder
|
|
90
|
-
* order.
|
|
91
|
-
*
|
|
92
|
-
* same document for the same pairing.
|
|
91
|
+
* order. Called by editor-launching `injectEditorFolder`, which backs
|
|
92
|
+
* `mate companion open`.
|
|
93
93
|
*/
|
|
94
94
|
export async function writeWorkspaceDocument(
|
|
95
95
|
companionPath: string,
|
|
96
96
|
repoPath: string,
|
|
97
97
|
): Promise<WorkspaceDocument> {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`${JSON.stringify(
|
|
104
|
-
{
|
|
105
|
-
folders: folders.map((candidate) => ({ path: candidate })),
|
|
106
|
-
},
|
|
107
|
-
null,
|
|
108
|
-
2,
|
|
109
|
-
)}\n`,
|
|
110
|
-
"utf8",
|
|
111
|
-
);
|
|
112
|
-
return { workspacePath, folders };
|
|
98
|
+
await project("workspace", { repoPath, companionPath });
|
|
99
|
+
return {
|
|
100
|
+
workspacePath: workspaceDocumentPath(repoPath),
|
|
101
|
+
folders: [path.resolve(repoPath), path.resolve(companionPath)],
|
|
102
|
+
};
|
|
113
103
|
}
|
|
114
104
|
|
|
115
105
|
export async function injectEditorFolder(
|
|
@@ -10,6 +10,7 @@ import { readCompanionRegistry } from "./companion-registry-reader";
|
|
|
10
10
|
import { ConfigStore } from "./config-store";
|
|
11
11
|
import { GlobalConfigStore } from "./global-config-store";
|
|
12
12
|
import { findRepoLocalLinkedRepository } from "./repo-local-registry";
|
|
13
|
+
import { projectWorkingRepositoryBestEffort } from "./working-repo-projection";
|
|
13
14
|
import {
|
|
14
15
|
AmbiguousCompanionError,
|
|
15
16
|
ConfigError,
|
|
@@ -216,6 +217,7 @@ export async function resolveForLaunch(
|
|
|
216
217
|
const repository = (await findRepoLocalLinkedRepository(cwd)) ?? undefined;
|
|
217
218
|
if (repository) {
|
|
218
219
|
await backfillCompanionRegistration(resolution.match.companionPath, repository);
|
|
220
|
+
await projectWorkingRepositoryBestEffort(resolution.match.companionPath, repository);
|
|
219
221
|
}
|
|
220
222
|
const context = await withResolvedHub(
|
|
221
223
|
makeContext(resolution.match.companionPath, "working-repo", repository),
|
|
@@ -280,14 +282,23 @@ export async function resolveForCapability(
|
|
|
280
282
|
return { ...ctx, repositoryId: "" };
|
|
281
283
|
}
|
|
282
284
|
|
|
283
|
-
|
|
285
|
+
/**
|
|
286
|
+
* `resolveWithDiagnostics` only to learn whether the match is ambiguous;
|
|
287
|
+
* `resolve()` is this same call with `logFailures`, so the returned match is
|
|
288
|
+
* unchanged. Unlike `resolveForLaunch` this does not refuse on ambiguity —
|
|
289
|
+
* it only declines to make a guessed companion durable.
|
|
290
|
+
*/
|
|
291
|
+
const resolution = await new CompanionResolver(globalConfigStore).resolveWithDiagnostics(cwd, {
|
|
292
|
+
logFailures: true,
|
|
293
|
+
});
|
|
294
|
+
const match = resolution.match;
|
|
284
295
|
if (match) {
|
|
296
|
+
const repository = (await findRepoLocalLinkedRepository(cwd)) ?? undefined;
|
|
297
|
+
if (repository && resolution.ambiguousMatches.length <= 1) {
|
|
298
|
+
await projectWorkingRepositoryBestEffort(match.companionPath, repository);
|
|
299
|
+
}
|
|
285
300
|
const context = await withResolvedHub(
|
|
286
|
-
makeContext(
|
|
287
|
-
match.companionPath,
|
|
288
|
-
"working-repo",
|
|
289
|
-
(await findRepoLocalLinkedRepository(cwd)) ?? undefined,
|
|
290
|
-
),
|
|
301
|
+
makeContext(match.companionPath, "working-repo", repository),
|
|
291
302
|
);
|
|
292
303
|
return {
|
|
293
304
|
...context,
|
|
@@ -2,7 +2,7 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
|
-
import { pathIsDirectory } from "
|
|
5
|
+
import { pathIsDirectory } from "../fs-utils";
|
|
6
6
|
import { YamlFileStore } from "./yaml-file-store";
|
|
7
7
|
|
|
8
8
|
interface CompanionEntry {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// oxlint-disable no-underscore-dangle
|
|
2
|
-
import { syncCompanionFiles
|
|
2
|
+
import { syncCompanionFiles } from "../../tools/setup";
|
|
3
3
|
import { getActiveDistribution } from "../../distribution";
|
|
4
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
5
|
import type { CapabilityPlugin, LaunchPreflightContext } from "../../tools/setup/plugin";
|
|
5
6
|
import type { LaunchAdapter, AdapterContext } from "./adapters/base";
|
|
6
7
|
import { ClaudeAdapter } from "./adapters/claude";
|
|
@@ -8,6 +9,17 @@ import { OpenCodeAdapter } from "./adapters/opencode";
|
|
|
8
9
|
import { CompanionStore } from "./companion-store";
|
|
9
10
|
import { syncCompanionGit } from "./companion-git-sync";
|
|
10
11
|
import { resolveForLaunch, type LaunchContext } from "./framework-context";
|
|
12
|
+
import {
|
|
13
|
+
firstFailure,
|
|
14
|
+
type ProjectionEntryOutcome,
|
|
15
|
+
type ProjectionInput,
|
|
16
|
+
type ProjectionResult,
|
|
17
|
+
} from "./projection-types";
|
|
18
|
+
import {
|
|
19
|
+
isWorkingRepositoryWrapped,
|
|
20
|
+
project,
|
|
21
|
+
projectWorkingRepositoryBestEffort,
|
|
22
|
+
} from "./working-repo-projection";
|
|
11
23
|
import {
|
|
12
24
|
RepositoryNotSelectedError,
|
|
13
25
|
LaunchPreflightError,
|
|
@@ -38,10 +50,54 @@ interface ResolvedLaunchState {
|
|
|
38
50
|
|
|
39
51
|
export const launcherDeps = {
|
|
40
52
|
syncCompanionFiles,
|
|
41
|
-
|
|
53
|
+
/** The Managed Projection's launch scope; the working repo is written only here. */
|
|
54
|
+
projectWorkingRepo: (input: ProjectionInput) => project("launch", input),
|
|
55
|
+
/**
|
|
56
|
+
* The Projection Root a launch guarantees for itself. `resolveForLaunch` also
|
|
57
|
+
* writes it, but only on the branch that resolved a companion from the
|
|
58
|
+
* Working Repository; asking here makes the guarantee independent of which
|
|
59
|
+
* branch answered. Best-effort — a launch carries its own configuration.
|
|
60
|
+
*/
|
|
61
|
+
refreshProjectionRoot: projectWorkingRepositoryBestEffort,
|
|
42
62
|
syncCompanionGit,
|
|
63
|
+
isWrapped: isWorkingRepositoryWrapped,
|
|
43
64
|
};
|
|
44
65
|
|
|
66
|
+
/**
|
|
67
|
+
* A wrapped Working Repository delivers the companion to sessions the operator
|
|
68
|
+
* starts themselves, through documents the Agent Runtime discovers by its own
|
|
69
|
+
* upward walk. A Managed Session loads those *and* everything the launch
|
|
70
|
+
* injects through `--settings`, `--mcp-config` and `--plugin-dir`, which
|
|
71
|
+
* ADR-014 established can never be withdrawn. Two delivery channels that cannot
|
|
72
|
+
* be merged must therefore not both be live for one repository, so wrapping and
|
|
73
|
+
* launching are exclusive rather than reconciled (ADR-015, superseding
|
|
74
|
+
* ADR-014's judgement that the duplication is bounded enough to accept).
|
|
75
|
+
*
|
|
76
|
+
* Raised before every write the launch itself performs: the companion Git sync,
|
|
77
|
+
* the companion file sync, the launch scope's projection and the runtime
|
|
78
|
+
* invocation. Companion resolution has already refreshed the Projection Root by
|
|
79
|
+
* this point — `resolveForLaunch` writes it while resolving — but that write is
|
|
80
|
+
* idempotent and byte-identical to the one `mate wrap` performs, so it cannot
|
|
81
|
+
* change what the repository holds.
|
|
82
|
+
*/
|
|
83
|
+
function wrappedRepositoryRefusal(repository: LinkedRepository): string {
|
|
84
|
+
return [
|
|
85
|
+
`${FRAMEWORK_NAME}: ${repository.id} is wrapped — \`${FRAMEWORK_NAME} claude\` and \`${FRAMEWORK_NAME} opencode\` do not run in a wrapped working repository.`,
|
|
86
|
+
"Start the session yourself: `claude` or `opencode` — the wrap delivers the companion guidance.",
|
|
87
|
+
`Or restore managed launches: \`${FRAMEWORK_NAME} unwrap\`.`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The shape `projectWorkingRepositoryBestEffort` reports a failed projection
|
|
93
|
+
* in, narrowed to the entry that failed: the launch scope writes several, so
|
|
94
|
+
* "the projection" alone would not tell the operator what to fix.
|
|
95
|
+
*/
|
|
96
|
+
function projectionFailure(outcome: ProjectionEntryOutcome, repository: LinkedRepository): string {
|
|
97
|
+
const error = outcome.error ?? new Error("projection failed");
|
|
98
|
+
return `failed to write ${outcome.path} for ${repository.id}: ${error.message}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
45
101
|
export class FrameworkLauncher {
|
|
46
102
|
private readonly adapters = new Map<string, LaunchAdapter>([
|
|
47
103
|
["claude", new ClaudeAdapter()],
|
|
@@ -62,15 +118,27 @@ export class FrameworkLauncher {
|
|
|
62
118
|
const state = await this.resolveLaunchState(request);
|
|
63
119
|
const adapterContext = this.makeAdapterContext(state);
|
|
64
120
|
|
|
121
|
+
if (await launcherDeps.isWrapped(state.repository.path)) {
|
|
122
|
+
throw new LaunchPreflightError(wrappedRepositoryRefusal(state.repository));
|
|
123
|
+
}
|
|
124
|
+
|
|
65
125
|
if (!request.skipGit && state.config.git === "auto") {
|
|
66
|
-
await launcherDeps.syncCompanionGit(
|
|
126
|
+
await launcherDeps.syncCompanionGit(
|
|
127
|
+
state.companionPath,
|
|
128
|
+
state.repository.path,
|
|
129
|
+
request.interactiveGit ?? false,
|
|
130
|
+
);
|
|
67
131
|
}
|
|
68
132
|
await launcherDeps.syncCompanionFiles(state.companionPath, state.config, state.repository.path);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
133
|
+
this.answerForProjection(
|
|
134
|
+
await launcherDeps.projectWorkingRepo({
|
|
135
|
+
repoPath: state.repository.path,
|
|
136
|
+
companionPath: state.companionPath,
|
|
137
|
+
config: state.config,
|
|
138
|
+
}),
|
|
139
|
+
state.repository,
|
|
73
140
|
);
|
|
141
|
+
await launcherDeps.refreshProjectionRoot(state.companionPath, state.repository);
|
|
74
142
|
await this.runCapabilityPreflight(state, request.tool);
|
|
75
143
|
await state.adapter.validateLaunch(adapterContext);
|
|
76
144
|
|
|
@@ -115,6 +183,28 @@ export class FrameworkLauncher {
|
|
|
115
183
|
};
|
|
116
184
|
}
|
|
117
185
|
|
|
186
|
+
/**
|
|
187
|
+
* `project` reports its failures rather than throwing, so the launch scope's
|
|
188
|
+
* caller is the one that has to answer for them. A non-degradable entry is a
|
|
189
|
+
* guardrail the session was promised — writing the companion into the
|
|
190
|
+
* runtime's allow-list, for one — so the launch fails instead of starting
|
|
191
|
+
* without it. A degradable entry warns in the voice
|
|
192
|
+
* {@link projectWorkingRepositoryBestEffort} uses, and the launch proceeds.
|
|
193
|
+
*/
|
|
194
|
+
private answerForProjection(result: ProjectionResult, repository: LinkedRepository): void {
|
|
195
|
+
for (const outcome of result.outcomes) {
|
|
196
|
+
if (outcome.state !== "failed" || !outcome.degradable) continue;
|
|
197
|
+
console.error(`${FRAMEWORK_NAME}: warning: ${projectionFailure(outcome, repository)}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const failure = firstFailure(result.outcomes);
|
|
201
|
+
if (failure) {
|
|
202
|
+
throw new LaunchPreflightError(
|
|
203
|
+
`${FRAMEWORK_NAME}: ${projectionFailure(failure, repository)}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
118
208
|
private makeAdapterContext(state: ResolvedLaunchState): AdapterContext {
|
|
119
209
|
return {
|
|
120
210
|
repository: state.repository,
|
|
@@ -1,58 +1,6 @@
|
|
|
1
|
-
import { GUIDANCE_FILE_VERSION, type MateGuidanceFile } from "../../runtime/guidance";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
buildCodebaseExplorationGuidanceSection,
|
|
5
|
-
buildCompanionPolicyXml,
|
|
6
|
-
hasGraphifyCapability,
|
|
7
|
-
hasTokensaveCapability,
|
|
8
|
-
} from "../../playbooks/companion-guidance";
|
|
9
|
-
import type { CapabilityConfig } from "./types";
|
|
10
|
-
|
|
11
1
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* session environment, so the same payload shape serves every companion.
|
|
16
|
-
*
|
|
17
|
-
* Real capabilities are passed through (not just the graphify/tokensave
|
|
18
|
-
* flags) so capability-gated companion-policy rules — e.g. openspec-finish —
|
|
19
|
-
* render exactly as they do for the Claude provider.
|
|
2
|
+
* The OpenCode guidance payload. Built in `runtime/` so the plugin can build
|
|
3
|
+
* the same payload from the Projection Root when no launch injected one; this
|
|
4
|
+
* module stays as the orchestrator-side name the launch adapter imports.
|
|
20
5
|
*/
|
|
21
|
-
export
|
|
22
|
-
const companionGuidance = buildCompanionPolicyXml(
|
|
23
|
-
{
|
|
24
|
-
companionPath: "$MATE_ARTIFACT_PATH",
|
|
25
|
-
repository: {
|
|
26
|
-
id: "$MATE_REPO_ID",
|
|
27
|
-
path: "$MATE_REPO_PATH",
|
|
28
|
-
},
|
|
29
|
-
allowedAgents: [],
|
|
30
|
-
capabilities,
|
|
31
|
-
},
|
|
32
|
-
{ wrapperBinPath: "$MATE_WRAPPER_BIN_PATH" },
|
|
33
|
-
);
|
|
34
|
-
const graphifyEnabled = hasGraphifyCapability(capabilities);
|
|
35
|
-
const tokensaveEnabled = hasTokensaveCapability(capabilities);
|
|
36
|
-
const codebaseExplorationGuidance = buildCodebaseExplorationGuidanceSection({
|
|
37
|
-
useGraphify: graphifyEnabled,
|
|
38
|
-
useTokensave: tokensaveEnabled,
|
|
39
|
-
});
|
|
40
|
-
const errors: string[] = [];
|
|
41
|
-
|
|
42
|
-
if (!companionGuidance.includes("<companion-policy ")) {
|
|
43
|
-
errors.push("companion guidance was not injected");
|
|
44
|
-
}
|
|
45
|
-
if (
|
|
46
|
-
(graphifyEnabled || tokensaveEnabled) &&
|
|
47
|
-
!codebaseExplorationGuidance.includes("<codebase-exploration-rules ")
|
|
48
|
-
) {
|
|
49
|
-
errors.push("codebase exploration guidance was not injected");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
version: GUIDANCE_FILE_VERSION,
|
|
54
|
-
companionGuidance,
|
|
55
|
-
codebaseExplorationGuidance,
|
|
56
|
-
errors,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
6
|
+
export { buildOpenCodeGuidance } from "../../runtime/companion-guidance";
|