@uniqbit/mate-core 0.18.3-canary.1 → 0.18.3
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/hooks/hooks.json +10 -1
- package/claude-plugin/hooks/session-banner.mjs +4 -3
- package/claude-plugin/hooks/session-prepare.mjs +9 -0
- package/opencode-plugin/src/companion.ts +47 -12
- package/opencode-plugin/src/server.ts +39 -4
- package/package.json +3 -3
- package/src/cli/commands/artifact/finish/openspec.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +60 -21
- package/src/cli/commands/companion/link.ts +16 -11
- package/src/cli/commands/companion/list.ts +4 -5
- package/src/cli/commands/companion/open.ts +4 -5
- package/src/cli/commands/companion/tui.ts +1 -1
- package/src/cli/commands/doctor.ts +1 -1
- package/src/cli/commands/report/renderer.ts +9 -19
- package/src/cli/commands/session/prepare.ts +119 -0
- package/src/cli/commands/shared/companion-selection.ts +2 -2
- package/src/cli/commands/studio/reports.ts +0 -1
- package/src/cli/commands/studio/server.ts +6 -2
- package/src/cli/commands/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +53 -59
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +3 -1
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +43 -8
- package/src/lib/orchestrator/adapters/claude.ts +130 -29
- package/src/lib/orchestrator/adapters/opencode.ts +17 -19
- package/src/lib/orchestrator/companion-git-sync.ts +28 -1
- package/src/lib/orchestrator/companion-hub.ts +2 -1
- package/src/lib/orchestrator/config-store.ts +9 -0
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +54 -22
- package/src/lib/orchestrator/projection-entries.ts +78 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
- package/src/lib/orchestrator/projection-record.ts +7 -1
- package/src/lib/orchestrator/projection-runtime-documents.ts +83 -130
- package/src/lib/orchestrator/projection-types.ts +38 -33
- package/src/lib/orchestrator/repository-link.ts +473 -0
- package/src/lib/orchestrator/root-context.ts +6 -2
- package/src/lib/orchestrator/session-prepare.ts +425 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/working-repo-projection.ts +71 -159
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +131 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +24 -132
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- package/src/opencode/session-notes.ts +76 -0
- package/src/opencode/session-prepare.ts +139 -0
- package/src/opencode/tui.tsx +56 -9
- package/src/runtime/claude-delivery-plan.ts +73 -0
- package/src/runtime/claude-hook-group.ts +21 -0
- package/src/runtime/companion-lock.ts +121 -0
- package/src/runtime/companion-sync.ts +66 -9
- package/src/runtime/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +335 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +90 -0
- package/src/runtime/write-atomic.ts +27 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +5 -0
- package/src/templates/capabilities/react-doctor/mate-react-doctor/SKILL.md +84 -0
- package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md +18 -15
- package/src/templates/mate-skills/agents/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +1 -1
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +1 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +850 -499
- package/src/tools/setup/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -1
- package/src/tools/setup/engine.ts +15 -4
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/providers/claude-format.ts +11 -18
- package/src/tools/setup/providers/claude-plugin-hooks.ts +61 -24
- package/src/tools/setup/providers/claude.ts +23 -6
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/working-repo-local-state.ts +82 -13
- package/src/tools/setup.ts +2 -2
- package/src/lib/orchestrator/companion-registry-reader.ts +0 -37
- package/src/lib/orchestrator/companion-resolver.ts +0 -179
- package/src/lib/orchestrator/companion-store.ts +0 -94
- package/src/lib/orchestrator/repo-local-registry.ts +0 -143
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +0 -144
- package/src/templates/capabilities/react-doctor/skill/SKILL.md +0 -51
- package/src/templates/report-assets/README.md +0 -32
- package/src/templates/report-assets/mermaid.LICENSE +0 -21
- package/src/templates/report-assets/mermaid.min.js +0 -4376
|
@@ -4,8 +4,15 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
6
6
|
import { MATE_ENV } from "../../../runtime/env-names";
|
|
7
|
+
import { getWrapperBinPath } from "../../package-paths";
|
|
7
8
|
import { buildProjection, companionEnvironment, projectionEnvironment } from "../projection-record";
|
|
8
|
-
import type {
|
|
9
|
+
import type { PrepareResult } from "../session-prepare";
|
|
10
|
+
import {
|
|
11
|
+
LaunchPreflightError,
|
|
12
|
+
type CapabilityConfig,
|
|
13
|
+
type GitModeProfile,
|
|
14
|
+
type LinkedRepository,
|
|
15
|
+
} from "../types";
|
|
9
16
|
|
|
10
17
|
export interface AdapterContext {
|
|
11
18
|
repository?: LinkedRepository;
|
|
@@ -47,15 +54,41 @@ function prependPathEntry(pathValue: string | undefined, entry: string): string
|
|
|
47
54
|
return [entry, ...entries.filter((value) => value !== entry)].join(path.delimiter);
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
export
|
|
57
|
+
export function prepareFailure(result: PrepareResult): string {
|
|
58
|
+
return [
|
|
59
|
+
`${FRAMEWORK_NAME}: preparing ${result.repositoryId} for OpenCode failed.`,
|
|
60
|
+
...result.steps.flatMap((step) =>
|
|
61
|
+
step.outcome === "failed" ? [`- ${step.step}: ${step.detail ?? "failed"}`] : [],
|
|
62
|
+
),
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `C` is the context one launch carries from `planLaunch` to `run`: returned,
|
|
68
|
+
* never stored, since one adapter instance serves every launch.
|
|
69
|
+
*/
|
|
70
|
+
export abstract class LaunchAdapter<C extends AdapterContext = AdapterContext> {
|
|
51
71
|
abstract readonly toolName: string;
|
|
52
72
|
readonly interactive: boolean = false;
|
|
53
73
|
|
|
54
|
-
abstract buildArgs(context:
|
|
74
|
+
abstract buildArgs(context: C, args: string[]): string[];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The runtime's share of a launch after a successful Session Prepare. Nothing
|
|
78
|
+
* to plan by default; an adapter that narrows `C` overrides it.
|
|
79
|
+
*/
|
|
80
|
+
async planLaunch(context: AdapterContext, _prepared: PrepareResult): Promise<C> {
|
|
81
|
+
return context as C;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What a failed prepare raises: the failed step's own error, else a summary. */
|
|
85
|
+
launchFailure(prepared: PrepareResult): unknown {
|
|
86
|
+
return prepared.failure ?? new LaunchPreflightError(prepareFailure(prepared));
|
|
87
|
+
}
|
|
55
88
|
|
|
56
|
-
async validateLaunch(_context:
|
|
89
|
+
async validateLaunch(_context: C): Promise<void> {}
|
|
57
90
|
|
|
58
|
-
extendEnvironment(_context:
|
|
91
|
+
extendEnvironment(_context: C): NodeJS.ProcessEnv {
|
|
59
92
|
return {};
|
|
60
93
|
}
|
|
61
94
|
|
|
@@ -67,7 +100,9 @@ export abstract class LaunchAdapter {
|
|
|
67
100
|
environment(context: AdapterContext): NodeJS.ProcessEnv {
|
|
68
101
|
const reactDoctorEnabled = context.capabilities.some((c) => c.name === "react-doctor");
|
|
69
102
|
const projection = context.repository
|
|
70
|
-
? projectionEnvironment(
|
|
103
|
+
? projectionEnvironment(
|
|
104
|
+
buildProjection(context.companionPath, context.repository, getWrapperBinPath()),
|
|
105
|
+
)
|
|
71
106
|
: companionEnvironment(context.companionPath);
|
|
72
107
|
const env: NodeJS.ProcessEnv = {
|
|
73
108
|
...process.env,
|
|
@@ -92,7 +127,7 @@ export abstract class LaunchAdapter {
|
|
|
92
127
|
return env;
|
|
93
128
|
}
|
|
94
129
|
|
|
95
|
-
async prepareLaunch(context:
|
|
130
|
+
async prepareLaunch(context: C, args: string[]): Promise<PreparedLaunch> {
|
|
96
131
|
const builtArgs = this.buildArgs(context, args);
|
|
97
132
|
const baseEnv = this.environment(context);
|
|
98
133
|
const extendedEnv = this.extendEnvironment(context);
|
|
@@ -120,7 +155,7 @@ export abstract class LaunchAdapter {
|
|
|
120
155
|
return !(this.interactive && process.stdin.isTTY);
|
|
121
156
|
}
|
|
122
157
|
|
|
123
|
-
async run(context:
|
|
158
|
+
async run(context: C, args: string[]): Promise<AdapterResult> {
|
|
124
159
|
const launch = await this.prepareLaunch(context, args);
|
|
125
160
|
|
|
126
161
|
return new Promise((resolve, reject) => {
|
|
@@ -1,21 +1,128 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
|
|
5
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
3
6
|
import { buildCompanionGuidance } from "../../../playbooks/companion-guidance";
|
|
4
|
-
import {
|
|
5
|
-
|
|
7
|
+
import {
|
|
8
|
+
planClaudeDelivery,
|
|
9
|
+
type ClaudeDeliveryPlan,
|
|
10
|
+
type SessionKind,
|
|
11
|
+
} from "../../../runtime/claude-delivery-plan";
|
|
12
|
+
import { hookGroupKey, type ClaudeHookGroup } from "../../../runtime/claude-hook-group";
|
|
13
|
+
import { NOT_WRAPPED } from "../../../runtime/wrap-state";
|
|
14
|
+
import { writeAtomic } from "../../../runtime/write-atomic";
|
|
6
15
|
import {
|
|
7
16
|
getCompanionClaudeMcpConfigPath,
|
|
8
17
|
getCompanionClaudeSettingsPath,
|
|
9
18
|
} from "../../../tools/setup/providers/claude";
|
|
19
|
+
import type { ClaudeSettings } from "../../../tools/setup/providers/claude-format";
|
|
20
|
+
import { getContextModePackageRoot, validateContextModePackage } from "../../context-mode-package";
|
|
21
|
+
import { getClaudePluginRoot, validateClaudePluginAssets } from "../../package-paths";
|
|
22
|
+
import type { PrepareResult } from "../session-prepare";
|
|
23
|
+
import { LaunchPreflightError } from "../types";
|
|
10
24
|
import { type AdapterContext, LaunchAdapter } from "./base";
|
|
11
25
|
|
|
12
|
-
|
|
26
|
+
/** The paths are present only where the plan names that channel and the file exists. */
|
|
27
|
+
interface ClaudeDelivery {
|
|
28
|
+
plan: ClaudeDeliveryPlan;
|
|
29
|
+
settingsPath?: string;
|
|
30
|
+
mcpConfigPath?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A Claude launch's context: the delivery its plan chose, and the files it chose or wrote. */
|
|
34
|
+
interface ClaudeLaunchContext extends AdapterContext {
|
|
35
|
+
delivery: ClaudeDelivery;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Where a launch with a Working Repository writes its derived `--settings`; git-ignored. */
|
|
39
|
+
export function launchSettingsPath(companionPath: string, repositoryId: string): string {
|
|
40
|
+
const safeId = repositoryId.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
41
|
+
return path.join(companionPath, ".claude", "state", `launch-settings.${safeId}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Companion settings minus `exclude`, so each wrap-carried group runs once. A
|
|
46
|
+
* file, not inline JSON (platform arg limits); per repository, so concurrent
|
|
47
|
+
* launches never race.
|
|
48
|
+
*/
|
|
49
|
+
async function writeLaunchSettings(
|
|
50
|
+
companionSettingsPath: string,
|
|
51
|
+
target: string,
|
|
52
|
+
exclude: readonly ClaudeHookGroup[],
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const settings = JSON.parse(await fs.readFile(companionSettingsPath, "utf8")) as ClaudeSettings;
|
|
55
|
+
const excluded = new Set(exclude.map(hookGroupKey));
|
|
56
|
+
const hooks: Record<string, ClaudeHookGroup[]> = {};
|
|
57
|
+
for (const [event, groups] of Object.entries(settings.hooks ?? {})) {
|
|
58
|
+
const kept = (groups ?? []).filter((group) => !excluded.has(hookGroupKey(group)));
|
|
59
|
+
if (kept.length > 0) hooks[event] = kept;
|
|
60
|
+
}
|
|
61
|
+
const derived: ClaudeSettings = { ...settings, hooks };
|
|
62
|
+
if (Object.keys(hooks).length === 0) delete derived.hooks;
|
|
63
|
+
|
|
64
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
65
|
+
await writeAtomic(target, `${JSON.stringify(derived, null, 2)}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The `--settings` file the plan's hook owner calls for, writing it when derived. */
|
|
69
|
+
async function settingsPathFor(
|
|
70
|
+
plan: ClaudeDeliveryPlan,
|
|
71
|
+
companionPath: string,
|
|
72
|
+
repository: AdapterContext["repository"],
|
|
73
|
+
): Promise<string | undefined> {
|
|
74
|
+
const companionSettingsPath = getCompanionClaudeSettingsPath(companionPath);
|
|
75
|
+
if (plan.hooks.owner === "none" || !existsSync(companionSettingsPath)) return undefined;
|
|
76
|
+
if (plan.hooks.owner === "settings") return companionSettingsPath;
|
|
77
|
+
/** Owner `wrap` is planned only for `managed-repo`, which has a repository. */
|
|
78
|
+
const settingsPath = launchSettingsPath(companionPath, repository!.id);
|
|
79
|
+
await writeLaunchSettings(companionSettingsPath, settingsPath, plan.hooks.launchSettingsExclude);
|
|
80
|
+
return settingsPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class ClaudeAdapter extends LaunchAdapter<ClaudeLaunchContext> {
|
|
13
84
|
readonly toolName = "claude";
|
|
14
85
|
readonly interactive = true;
|
|
15
86
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Plans from the wrap state the prepare's pass left, never a fresh read. A
|
|
89
|
+
* repository left without a Claude wrap fails preflight: the plan would defer
|
|
90
|
+
* to a wrap that is not there.
|
|
91
|
+
*/
|
|
92
|
+
async planLaunch(context: AdapterContext, prepared: PrepareResult): Promise<ClaudeLaunchContext> {
|
|
93
|
+
const { repository, companionPath } = context;
|
|
94
|
+
const kind: SessionKind = repository ? "managed-repo" : "managed-companion";
|
|
95
|
+
const wrap = prepared.wrapState ?? NOT_WRAPPED;
|
|
96
|
+
if (repository && !wrap.runtimes.has("claude")) {
|
|
97
|
+
throw new LaunchPreflightError(
|
|
98
|
+
`${FRAMEWORK_NAME}: ${repository.id} is not wrapped for Claude after its prepare; run \`${FRAMEWORK_NAME} wrap\` in ${repository.path}.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const plan = planClaudeDelivery(kind, wrap);
|
|
102
|
+
const mcpConfigPath = getCompanionClaudeMcpConfigPath(companionPath);
|
|
103
|
+
const settingsPath = await settingsPathFor(plan, companionPath, repository);
|
|
104
|
+
const delivery: ClaudeDelivery = {
|
|
105
|
+
plan,
|
|
106
|
+
...(plan.mcp === "mcp-config" && existsSync(mcpConfigPath) ? { mcpConfigPath } : {}),
|
|
107
|
+
...(settingsPath ? { settingsPath } : {}),
|
|
108
|
+
};
|
|
109
|
+
return { ...context, delivery };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A failed Claude wrap pass names the entry that failed. */
|
|
113
|
+
launchFailure(prepared: PrepareResult): unknown {
|
|
114
|
+
const failed = prepared.steps.find((step) => step.outcome === "failed");
|
|
115
|
+
if (failed?.step === "working-target") {
|
|
116
|
+
return new LaunchPreflightError(
|
|
117
|
+
`${FRAMEWORK_NAME}: reconciling the Claude wrap of ${prepared.repositoryId} failed: ${failed.detail ?? "failed"}`,
|
|
118
|
+
{ cause: prepared.failure },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return super.launchFailure(prepared);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async validateLaunch(context: ClaudeLaunchContext): Promise<void> {
|
|
125
|
+
/** The bundled plugin carries the artifact-path guard; never launch without it. */
|
|
19
126
|
try {
|
|
20
127
|
validateClaudePluginAssets();
|
|
21
128
|
} catch (error) {
|
|
@@ -37,24 +144,20 @@ export class ClaudeAdapter extends LaunchAdapter {
|
|
|
37
144
|
}
|
|
38
145
|
}
|
|
39
146
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const settingsArgs =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const companionMcpConfigPath = getCompanionClaudeMcpConfigPath(context.companionPath);
|
|
55
|
-
const mcpConfigArgs = existsSync(companionMcpConfigPath)
|
|
56
|
-
? ["--mcp-config", companionMcpConfigPath]
|
|
57
|
-
: [];
|
|
147
|
+
/**
|
|
148
|
+
* Plan in, flags out; decides nothing. `--append-system-prompt`, `--plugin-dir`
|
|
149
|
+
* and context-mode are unconditional: no settings document can carry them.
|
|
150
|
+
*/
|
|
151
|
+
buildArgs(context: ClaudeLaunchContext, args: string[]): string[] {
|
|
152
|
+
const { plan, settingsPath, mcpConfigPath } = context.delivery;
|
|
153
|
+
/** Layered on the user's own sources so global skills and plugins still load. */
|
|
154
|
+
const settingsArgs =
|
|
155
|
+
plan.hooks.owner !== "none" && settingsPath
|
|
156
|
+
? ["--setting-sources", "user,project,local", "--settings", settingsPath]
|
|
157
|
+
: [];
|
|
158
|
+
const addDirArgs = plan.dirAccess === "add-dir" ? ["--add-dir", context.companionPath] : [];
|
|
159
|
+
const mcpConfigArgs =
|
|
160
|
+
plan.mcp === "mcp-config" && mcpConfigPath ? ["--mcp-config", mcpConfigPath] : [];
|
|
58
161
|
const contextModeArgs = context.capabilities.some(
|
|
59
162
|
(capability) => capability.name === "context-mode",
|
|
60
163
|
)
|
|
@@ -62,14 +165,12 @@ export class ClaudeAdapter extends LaunchAdapter {
|
|
|
62
165
|
: [];
|
|
63
166
|
|
|
64
167
|
return [
|
|
65
|
-
|
|
66
|
-
context.companionPath,
|
|
168
|
+
...addDirArgs,
|
|
67
169
|
"--append-system-prompt",
|
|
68
170
|
buildCompanionGuidance(context),
|
|
69
171
|
...settingsArgs,
|
|
70
172
|
...mcpConfigArgs,
|
|
71
|
-
|
|
72
|
-
// installation, coexisting with the context-mode plugin dir.
|
|
173
|
+
/** The bundled plugin from the running install, beside context-mode's. */
|
|
73
174
|
"--plugin-dir",
|
|
74
175
|
getClaudePluginRoot(),
|
|
75
176
|
...contextModeArgs,
|
|
@@ -77,7 +178,7 @@ export class ClaudeAdapter extends LaunchAdapter {
|
|
|
77
178
|
];
|
|
78
179
|
}
|
|
79
180
|
|
|
80
|
-
extendEnvironment(_context:
|
|
181
|
+
extendEnvironment(_context: ClaudeLaunchContext): NodeJS.ProcessEnv {
|
|
81
182
|
return {
|
|
82
183
|
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1",
|
|
83
184
|
};
|
|
@@ -7,15 +7,12 @@ const path = await import("node:path");
|
|
|
7
7
|
import { MATE_ENV } from "../../../runtime/env";
|
|
8
8
|
import { validateGuidanceData, type MateGuidanceFile } from "../../../runtime/guidance";
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
mergeOpenCodeConfigContent,
|
|
13
|
-
readOpenCodeConfig,
|
|
14
|
-
} from "../../../tools/setup/providers/opencode-format";
|
|
15
|
-
import { renderCompanionExternalDirectoryPermissions } from "../../../tools/setup/providers/opencode";
|
|
10
|
+
import { validateOpenCodePluginAssets } from "../../package-paths";
|
|
11
|
+
import { readOpenCodeConfig } from "../../../tools/setup/providers/opencode-format";
|
|
16
12
|
import { buildOpenCodeGuidance } from "../opencode-guidance";
|
|
13
|
+
import type { PrepareResult } from "../session-prepare";
|
|
17
14
|
import { LaunchPreflightError } from "../types";
|
|
18
|
-
import { type AdapterContext, LaunchAdapter } from "./base";
|
|
15
|
+
import { type AdapterContext, LaunchAdapter, prepareFailure } from "./base";
|
|
19
16
|
|
|
20
17
|
/** The Mate plugin is a V2 plugin; an older host rejects it at load time. */
|
|
21
18
|
const MIN_OPENCODE_MAJOR = 2;
|
|
@@ -62,6 +59,13 @@ export class OpenCodeAdapter extends LaunchAdapter {
|
|
|
62
59
|
);
|
|
63
60
|
}
|
|
64
61
|
|
|
62
|
+
/** A prepared repository reports its failure as one summary, as it always has. */
|
|
63
|
+
launchFailure(prepared: PrepareResult): unknown {
|
|
64
|
+
return prepared.repositoryId
|
|
65
|
+
? new LaunchPreflightError(prepareFailure(prepared))
|
|
66
|
+
: super.launchFailure(prepared);
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
buildArgs(context: AdapterContext, args: string[]): string[] {
|
|
66
70
|
const codeDir = context.launchWorkingDirectory;
|
|
67
71
|
const isTui = args.length === 0 || args[0]?.startsWith("-");
|
|
@@ -74,20 +78,14 @@ export class OpenCodeAdapter extends LaunchAdapter {
|
|
|
74
78
|
return hostsServer && !choosesServer ? [command, STANDALONE_FLAG, ...rest] : [command, ...rest];
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
/**
|
|
82
|
+
* No `OPENCODE_CONFIG_DIR` and no config overlay: OpenCode 2.x treats the
|
|
83
|
+
* config dir as a replacement for the user's global one, and everything the
|
|
84
|
+
* overlay carried now lives in the wrapped `.opencode/opencode.json`. An
|
|
85
|
+
* inherited `OPENCODE_CONFIG_DIR` or `OPENCODE_CONFIG_CONTENT` passes through.
|
|
86
|
+
*/
|
|
77
87
|
extendEnvironment(context: AdapterContext): NodeJS.ProcessEnv {
|
|
78
88
|
return {
|
|
79
|
-
OPENCODE_CONFIG_DIR: path.join(context.companionPath, ".opencode"),
|
|
80
|
-
OPENCODE_CONFIG_CONTENT: mergeOpenCodeConfigContent(
|
|
81
|
-
{
|
|
82
|
-
permissions: renderCompanionExternalDirectoryPermissions(context.companionPath),
|
|
83
|
-
plugins: [getOpenCodePluginRoot()],
|
|
84
|
-
references: {
|
|
85
|
-
mate: context.companionPath,
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
process.env,
|
|
89
|
-
{ appendSkillPaths: [path.join(context.companionPath, ".agents", "skills")] },
|
|
90
|
-
),
|
|
91
89
|
// Built fresh per launch from the live capability config; always set so
|
|
92
90
|
// a stale value inherited from an outer Mate session can never leak in.
|
|
93
91
|
[MATE_ENV.guidanceJson]: JSON.stringify(this.buildGuidance(context)),
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
type UpstreamTarget,
|
|
13
13
|
} from "../../runtime/companion-git";
|
|
14
14
|
import { recordCompanionSync } from "../../runtime/companion-git-state";
|
|
15
|
+
import { withCompanionLock, type CompanionLockOptions } from "../../runtime/companion-lock";
|
|
16
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
15
17
|
import { LaunchPreflightError } from "./types";
|
|
16
18
|
|
|
17
19
|
type GitCommandResult = GitResult;
|
|
@@ -427,10 +429,35 @@ export class CompanionGitSync {
|
|
|
427
429
|
}
|
|
428
430
|
}
|
|
429
431
|
|
|
432
|
+
/**
|
|
433
|
+
* An attended operation waits for whoever holds the companion lock, however
|
|
434
|
+
* long: the holder may be an operator answering a merge conflict. The line
|
|
435
|
+
* explains the pause; a crashed holder frees the lock by itself.
|
|
436
|
+
*/
|
|
437
|
+
export function attendedLockOptions(companionPath: string): CompanionLockOptions {
|
|
438
|
+
return {
|
|
439
|
+
timeoutMs: Number.POSITIVE_INFINITY,
|
|
440
|
+
onWait: () =>
|
|
441
|
+
console.error(
|
|
442
|
+
`${FRAMEWORK_NAME}: waiting for another companion sync to finish (${companionPath})…`,
|
|
443
|
+
),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Serialized with every other synchronization of this companion. */
|
|
430
448
|
export async function syncCompanionGit(
|
|
431
449
|
companionPath: string,
|
|
432
450
|
workingRepoPath?: string,
|
|
433
451
|
interactiveGit = false,
|
|
434
452
|
): Promise<CompanionGitSyncResult> {
|
|
435
|
-
|
|
453
|
+
const locked = await withCompanionLock(companionPath, attendedLockOptions(companionPath), () =>
|
|
454
|
+
new CompanionGitSync().sync(companionPath, workingRepoPath, interactiveGit),
|
|
455
|
+
);
|
|
456
|
+
if (!locked.acquired) {
|
|
457
|
+
throw new CompanionGitSyncError(
|
|
458
|
+
companionPath,
|
|
459
|
+
"Another companion synchronization is still running.",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
return locked.value;
|
|
436
463
|
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "../../tools/setup/dynamic-plugins/install";
|
|
16
16
|
import { hydrateDynamicPlugins } from "../../tools/setup/dynamic-plugins/hydrate";
|
|
17
17
|
import { applySetupCompatibilities } from "../../tools/setup";
|
|
18
|
-
import { findRepoLocalRegistryFile } from "./
|
|
18
|
+
import { findRepoLocalRegistryFile } from "./repository-link";
|
|
19
19
|
import { companionGit, describeGitFailure, type CompanionGit } from "../../runtime/companion-git";
|
|
20
20
|
|
|
21
21
|
export interface HubGitOptions {
|
|
@@ -319,6 +319,7 @@ export async function syncHub(
|
|
|
319
319
|
const { config, store } = await assertHubRoot(hubPath);
|
|
320
320
|
const results: HubSyncResult[] = [];
|
|
321
321
|
for (const member of config.hub!.companions) {
|
|
322
|
+
// oxlint-disable-next-line no-await-in-loop -- members sync one at a time, in declared order
|
|
322
323
|
const result = await syncHubMember(hubPath, member, options);
|
|
323
324
|
results.push(result);
|
|
324
325
|
if (result.materializedCommit && result.status === "updated") {
|
|
@@ -310,3 +310,12 @@ export class ConfigStore extends YamlFileStore<FrameworkConfig> {
|
|
|
310
310
|
return config;
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
|
+
|
|
314
|
+
/** The companion's own selection, read from files: wrapping runs no setup pass. */
|
|
315
|
+
export async function readCompanionConfig(companionPath: string): Promise<FrameworkConfig> {
|
|
316
|
+
return mergeWithDefaults(
|
|
317
|
+
await new ConfigStore(
|
|
318
|
+
path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
|
|
319
|
+
).load(),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -4,12 +4,13 @@ import path from "node:path";
|
|
|
4
4
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
import { CompanionRegistryStore } from "./companion-registry-store";
|
|
7
|
-
import { CompanionResolver } from "./companion-resolver";
|
|
8
|
-
import { companionRootedStore } from "./companion-store";
|
|
9
|
-
import { readCompanionRegistry } from "./companion-registry-reader";
|
|
10
7
|
import { ConfigStore } from "./config-store";
|
|
11
8
|
import { GlobalConfigStore } from "./global-config-store";
|
|
12
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
backfillCompanionRegistration,
|
|
11
|
+
CompanionResolver,
|
|
12
|
+
findRepoLocalLinkedRepository,
|
|
13
|
+
} from "./repository-link";
|
|
13
14
|
import { projectWorkingRepositoryBestEffort } from "./working-repo-projection";
|
|
14
15
|
import {
|
|
15
16
|
AmbiguousCompanionError,
|
|
@@ -257,31 +258,6 @@ export async function resolveForLaunch(
|
|
|
257
258
|
throw new WorkingRepoRequiredError();
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
/**
|
|
261
|
-
* Self-heals repos linked before the companion-side registry write existed
|
|
262
|
-
* (or whose companion registry was reset): backfills the companion-side
|
|
263
|
-
* entry from the already-trusted repo-local pointer on every launch
|
|
264
|
-
* resolution. Best-effort — never blocks a launch.
|
|
265
|
-
*/
|
|
266
|
-
async function backfillCompanionRegistration(
|
|
267
|
-
companionPath: string,
|
|
268
|
-
repository: LinkedRepository,
|
|
269
|
-
): Promise<void> {
|
|
270
|
-
try {
|
|
271
|
-
const { repos } = await readCompanionRegistry(companionPath).catch(() => ({ repos: [] }));
|
|
272
|
-
const existing = repos.find((repo) => repo.id === repository.id);
|
|
273
|
-
if (existing?.path === repository.path) return;
|
|
274
|
-
|
|
275
|
-
await companionRootedStore(companionPath).registerRepository(repository);
|
|
276
|
-
} catch (error) {
|
|
277
|
-
console.error(
|
|
278
|
-
`${FRAMEWORK_NAME}: warning: failed to backfill companion registry for ${repository.id}: ${
|
|
279
|
-
error instanceof Error ? error.message : String(error)
|
|
280
|
-
}`,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
261
|
// Resolves context for `mate cap` commands. Unlike resolveForLaunch, this allows
|
|
286
262
|
// running from the companion directory itself — cap commands manage companion state
|
|
287
263
|
// and don't need a working-repo cwd. resolveForLaunch intentionally lacks this
|