@uniqbit/mate-core 0.18.3-canary.0 → 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 +56 -2
- 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/types.ts +4 -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
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { getActiveDistribution } from "../../distribution";
|
|
2
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
3
|
+
import { recordCompanionSyncUnfinished } from "../../runtime/companion-git-state";
|
|
4
|
+
import { withCompanionLock, type CompanionLockOptions } from "../../runtime/companion-lock";
|
|
5
|
+
import {
|
|
6
|
+
launchRecoveryStalenessLines,
|
|
7
|
+
syncCompanionUnattended,
|
|
8
|
+
type UnattendedSyncOutcome,
|
|
9
|
+
} from "../../runtime/companion-sync";
|
|
10
|
+
import type { WrapState } from "../../runtime/wrap-state";
|
|
11
|
+
import { syncCompanionFiles } from "../../tools/setup";
|
|
12
|
+
import type { CapabilityPlugin, LaunchPreflightContext } from "../../tools/setup/plugin";
|
|
13
|
+
import { attendedLockOptions, syncCompanionGit } from "./companion-git-sync";
|
|
14
|
+
import { readCompanionConfig } from "./config-store";
|
|
15
|
+
import { entryFailureMessage, firstFailure, type ProjectionChangedPaths } from "./projection-types";
|
|
16
|
+
import { LaunchPreflightError, type FrameworkConfig, type LinkedRepository } from "./types";
|
|
17
|
+
import { project, projectWorkingRepositoryBestEffort } from "./working-repo-projection";
|
|
18
|
+
import { runWrapPass } from "./wrap-pass";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Session Prepare: Git sync, file sync, one projection pass, preflight. Never
|
|
22
|
+
* spawns a runtime.
|
|
23
|
+
* - `attended`: managed launch; waits for the lock, stops at first failure.
|
|
24
|
+
* - `local`: plugin's awaited phase; no remote, only tries the lock.
|
|
25
|
+
* - `sync`: plugin's detached fast-forward, then `local` if the companion moved.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type PrepareMode = "attended" | "local" | "sync";
|
|
29
|
+
export type PrepareRuntime = "claude" | "opencode";
|
|
30
|
+
|
|
31
|
+
export type PrepareStepName =
|
|
32
|
+
| "lock"
|
|
33
|
+
| "git-sync"
|
|
34
|
+
| "file-sync"
|
|
35
|
+
| "launch-projection"
|
|
36
|
+
| "working-target"
|
|
37
|
+
| "preflight";
|
|
38
|
+
|
|
39
|
+
export interface PrepareStep {
|
|
40
|
+
step: PrepareStepName;
|
|
41
|
+
outcome: "ok" | "skipped" | "warning" | "failed";
|
|
42
|
+
detail?: string;
|
|
43
|
+
/** The runtimes a `working-target` or `preflight` step answered for. */
|
|
44
|
+
runtimes?: PrepareRuntime[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PrepareResult {
|
|
48
|
+
/** False when a step failed; warnings alone keep it true. */
|
|
49
|
+
ok: boolean;
|
|
50
|
+
mode: PrepareMode;
|
|
51
|
+
runtime: PrepareRuntime;
|
|
52
|
+
/** Every runtime this prepare reconciled and preflighted. */
|
|
53
|
+
runtimes: PrepareRuntime[];
|
|
54
|
+
repositoryId?: string;
|
|
55
|
+
companionPath: string;
|
|
56
|
+
steps: PrepareStep[];
|
|
57
|
+
warnings: string[];
|
|
58
|
+
/** The sync fast-forwarded the companion. */
|
|
59
|
+
companionChanged?: true;
|
|
60
|
+
/** The first failed step's own error, for an attended caller to raise as it was. */
|
|
61
|
+
failure?: unknown;
|
|
62
|
+
/** The wrap state the `working-target` step's pass left; a launch reads no manifest itself. */
|
|
63
|
+
wrapState?: WrapState;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PrepareSessionInput {
|
|
67
|
+
runtime: PrepareRuntime;
|
|
68
|
+
/**
|
|
69
|
+
* `local`/`sync`: the runtimes the repository is wrapped for, reconciled in
|
|
70
|
+
* one wrap pass and preflighted each; `[runtime]` when absent.
|
|
71
|
+
*/
|
|
72
|
+
runtimes?: readonly PrepareRuntime[];
|
|
73
|
+
companionPath: string;
|
|
74
|
+
/** Absent for a companion-scope launch: nothing is projected. */
|
|
75
|
+
repository?: LinkedRepository;
|
|
76
|
+
mode: PrepareMode;
|
|
77
|
+
/** The companion config the caller already holds; read once here otherwise. */
|
|
78
|
+
config?: FrameworkConfig;
|
|
79
|
+
/** `--no-git`: an attended prepare skips only the Git sync. */
|
|
80
|
+
skipGit?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The detached sync may queue behind an operator answering a merge conflict. */
|
|
84
|
+
const SYNC_LOCK_TIMEOUT_MS = 5 * 60_000;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every enabled Capability's preflight for one runtime, run together. Throws a
|
|
88
|
+
* {@link LaunchPreflightError} listing what failed.
|
|
89
|
+
*/
|
|
90
|
+
async function runCapabilityPreflight(context: LaunchPreflightContext): Promise<void> {
|
|
91
|
+
const { config, providerId } = context;
|
|
92
|
+
const preflights = getActiveDistribution()
|
|
93
|
+
.registry.getAll()
|
|
94
|
+
.flatMap((plugin) => {
|
|
95
|
+
if (plugin.kind !== "capability" || !plugin.isEnabled(config)) return [];
|
|
96
|
+
const capability = plugin as CapabilityPlugin;
|
|
97
|
+
const preflight = capability.forProvider?.[providerId]?.preflight;
|
|
98
|
+
return preflight ? [{ capability, preflight }] : [];
|
|
99
|
+
});
|
|
100
|
+
const results = await Promise.all(
|
|
101
|
+
preflights.map(async ({ capability, preflight }) => {
|
|
102
|
+
try {
|
|
103
|
+
return await preflight(context);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new LaunchPreflightError(
|
|
106
|
+
`Capability preflight failed for ${capability.id} on ${providerId}: ${message(error)}`,
|
|
107
|
+
{ cause: error },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
const diagnostics = results.flat();
|
|
113
|
+
|
|
114
|
+
if (diagnostics.length > 0) {
|
|
115
|
+
throw new LaunchPreflightError(
|
|
116
|
+
["Capability launch preflight failed.", ...diagnostics.map((item) => `- ${item}`)].join("\n"),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const sessionPrepareDeps = {
|
|
122
|
+
readCompanionConfig,
|
|
123
|
+
syncCompanionGit,
|
|
124
|
+
syncCompanionUnattended,
|
|
125
|
+
recordCompanionSyncUnfinished,
|
|
126
|
+
syncCompanionFiles,
|
|
127
|
+
projectLaunch: (input: Parameters<typeof project>[1]) => project("launch", input),
|
|
128
|
+
/**
|
|
129
|
+
* The Projection Root a Claude launch guarantees for itself. Companion
|
|
130
|
+
* resolution also writes it, but only on the branch that resolved a companion
|
|
131
|
+
* from the Working Repository; asking here makes the guarantee independent of
|
|
132
|
+
* which branch answered. Best-effort — a launch carries its own configuration.
|
|
133
|
+
*/
|
|
134
|
+
refreshProjectionRoot: projectWorkingRepositoryBestEffort,
|
|
135
|
+
runWrapPass,
|
|
136
|
+
runCapabilityPreflight,
|
|
137
|
+
withCompanionLock,
|
|
138
|
+
/** As the launch decides it: Git may prompt only with a terminal on both ends. */
|
|
139
|
+
isInteractiveTerminal: () => !!(process.stdin.isTTY && process.stdout.isTTY),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
function message(error: unknown): string {
|
|
143
|
+
return error instanceof Error ? error.message : String(error);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** One prepare's running record; `failed` stops an attended prepare. */
|
|
147
|
+
class StepLog {
|
|
148
|
+
readonly steps: PrepareStep[] = [];
|
|
149
|
+
readonly warnings: string[] = [];
|
|
150
|
+
failure: unknown;
|
|
151
|
+
wrapState: WrapState | undefined;
|
|
152
|
+
stopped = false;
|
|
153
|
+
companionChanged = false;
|
|
154
|
+
|
|
155
|
+
constructor(private readonly attended: boolean) {}
|
|
156
|
+
|
|
157
|
+
push(step: PrepareStep): void {
|
|
158
|
+
this.steps.push(step);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fail(
|
|
162
|
+
step: PrepareStepName,
|
|
163
|
+
error: unknown,
|
|
164
|
+
overrides: Pick<PrepareStep, "detail" | "runtimes"> = {},
|
|
165
|
+
): void {
|
|
166
|
+
this.steps.push({ step, outcome: "failed", detail: message(error), ...overrides });
|
|
167
|
+
if (this.failure === undefined) this.failure = error;
|
|
168
|
+
if (this.attended) this.stopped = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function runFileSync(
|
|
173
|
+
input: PrepareSessionInput,
|
|
174
|
+
config: FrameworkConfig,
|
|
175
|
+
run: StepLog,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const deps = sessionPrepareDeps;
|
|
178
|
+
try {
|
|
179
|
+
if (input.repository) {
|
|
180
|
+
await deps.syncCompanionFiles(input.companionPath, config, input.repository.path);
|
|
181
|
+
} else {
|
|
182
|
+
await deps.syncCompanionFiles(input.companionPath, config);
|
|
183
|
+
}
|
|
184
|
+
run.push({ step: "file-sync", outcome: "ok" });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
run.fail("file-sync", error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function wrapDetail(wrapped: {
|
|
191
|
+
writtenDocuments: string[];
|
|
192
|
+
changed: ProjectionChangedPaths;
|
|
193
|
+
}): string {
|
|
194
|
+
const wrote = [...wrapped.writtenDocuments, ...wrapped.changed.written];
|
|
195
|
+
const withdrew = wrapped.changed.removed;
|
|
196
|
+
const parts: string[] = [];
|
|
197
|
+
if (wrote.length > 0) parts.push(`wrote ${wrote.join(", ")}`);
|
|
198
|
+
if (withdrew.length > 0) parts.push(`withdrew ${withdrew.join(", ")}`);
|
|
199
|
+
return parts.length > 0 ? `updated (${parts.join("; ")})` : "updated";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function runtimesOf(input: PrepareSessionInput): PrepareRuntime[] {
|
|
203
|
+
return input.mode !== "attended" && input.runtimes?.length
|
|
204
|
+
? [...input.runtimes]
|
|
205
|
+
: [input.runtime];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* One wrap pass for `runtimes`, OpenCode's copies included. Every managed
|
|
210
|
+
* launch is this pass followed by the agent, for either runtime.
|
|
211
|
+
*/
|
|
212
|
+
async function reconcileWrap(
|
|
213
|
+
input: PrepareSessionInput,
|
|
214
|
+
repository: LinkedRepository,
|
|
215
|
+
runtimes: PrepareRuntime[],
|
|
216
|
+
config: FrameworkConfig,
|
|
217
|
+
run: StepLog,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
const wrapped = await sessionPrepareDeps.runWrapPass(input.companionPath, repository, {
|
|
220
|
+
runtimes,
|
|
221
|
+
config,
|
|
222
|
+
});
|
|
223
|
+
if (wrapped.kind === "failed") {
|
|
224
|
+
run.fail("working-target", new LaunchPreflightError(wrapped.message), { runtimes });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
run.warnings.push(...wrapped.warnings);
|
|
228
|
+
run.wrapState = wrapped.wrapState;
|
|
229
|
+
run.push({
|
|
230
|
+
step: "working-target",
|
|
231
|
+
outcome: "ok",
|
|
232
|
+
detail: wrapped.kind === "written" ? wrapDetail(wrapped) : "current",
|
|
233
|
+
runtimes,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Claude: the launch scope, then the Projection Root. A non-degradable entry is
|
|
239
|
+
* a guardrail the session was promised, so it fails the prepare; a degradable
|
|
240
|
+
* one warns and the prepare proceeds.
|
|
241
|
+
*/
|
|
242
|
+
async function projectClaude(
|
|
243
|
+
input: PrepareSessionInput,
|
|
244
|
+
repository: LinkedRepository,
|
|
245
|
+
config: FrameworkConfig,
|
|
246
|
+
run: StepLog,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
const deps = sessionPrepareDeps;
|
|
249
|
+
const result = await deps.projectLaunch({
|
|
250
|
+
repoPath: repository.path,
|
|
251
|
+
companionPath: input.companionPath,
|
|
252
|
+
config,
|
|
253
|
+
});
|
|
254
|
+
for (const outcome of result.outcomes) {
|
|
255
|
+
if (outcome.state === "failed" && outcome.degradable) {
|
|
256
|
+
run.warnings.push(entryFailureMessage(outcome, repository.id));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const failure = firstFailure(result.outcomes);
|
|
260
|
+
if (failure) {
|
|
261
|
+
const detail = entryFailureMessage(failure, repository.id);
|
|
262
|
+
run.fail("launch-projection", new LaunchPreflightError(`${FRAMEWORK_NAME}: ${detail}`), {
|
|
263
|
+
detail,
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
run.push({ step: "launch-projection", outcome: "ok" });
|
|
268
|
+
await deps.refreshProjectionRoot(input.companionPath, repository);
|
|
269
|
+
/** A launch is a wrap: created when missing, reconciled when stale, so the launch can defer to it. */
|
|
270
|
+
await reconcileWrap(input, repository, ["claude"], config, run);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Attended: the launch's own runtime. Otherwise one wrap pass for every wrapped runtime. */
|
|
274
|
+
async function projectRepository(
|
|
275
|
+
input: PrepareSessionInput,
|
|
276
|
+
repository: LinkedRepository,
|
|
277
|
+
runtimes: PrepareRuntime[],
|
|
278
|
+
config: FrameworkConfig,
|
|
279
|
+
run: StepLog,
|
|
280
|
+
): Promise<void> {
|
|
281
|
+
if (input.mode === "attended" && input.runtime === "claude") {
|
|
282
|
+
await projectClaude(input, repository, config, run);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await reconcileWrap(input, repository, runtimes, config, run);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** File sync, one projection pass, and the capability preflight per runtime. */
|
|
289
|
+
async function runLocal(
|
|
290
|
+
input: PrepareSessionInput,
|
|
291
|
+
config: FrameworkConfig,
|
|
292
|
+
run: StepLog,
|
|
293
|
+
): Promise<void> {
|
|
294
|
+
await runFileSync(input, config, run);
|
|
295
|
+
if (run.stopped) return;
|
|
296
|
+
|
|
297
|
+
const runtimes = runtimesOf(input);
|
|
298
|
+
if (input.repository) {
|
|
299
|
+
await projectRepository(input, input.repository, runtimes, config, run);
|
|
300
|
+
if (run.stopped) return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Attended prepares one runtime, so running these together never skips a stop. */
|
|
304
|
+
const outcomes = await Promise.allSettled(
|
|
305
|
+
runtimes.map((runtime) =>
|
|
306
|
+
sessionPrepareDeps.runCapabilityPreflight({
|
|
307
|
+
companionPath: input.companionPath,
|
|
308
|
+
config,
|
|
309
|
+
repository: input.repository,
|
|
310
|
+
providerId: runtime,
|
|
311
|
+
}),
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
outcomes.forEach((outcome, index) => {
|
|
315
|
+
const answered = [runtimes[index]!];
|
|
316
|
+
if (outcome.status === "fulfilled") {
|
|
317
|
+
run.push({ step: "preflight", outcome: "ok", runtimes: answered });
|
|
318
|
+
} else {
|
|
319
|
+
run.fail("preflight", outcome.reason, { runtimes: answered });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function runAttended(
|
|
325
|
+
input: PrepareSessionInput,
|
|
326
|
+
config: FrameworkConfig,
|
|
327
|
+
run: StepLog,
|
|
328
|
+
): Promise<void> {
|
|
329
|
+
const deps = sessionPrepareDeps;
|
|
330
|
+
if (input.skipGit) {
|
|
331
|
+
run.push({ step: "git-sync", outcome: "skipped", detail: "--no-git" });
|
|
332
|
+
} else if (config.git !== "auto") {
|
|
333
|
+
run.push({ step: "git-sync", outcome: "skipped", detail: "companion Git mode is not auto" });
|
|
334
|
+
} else {
|
|
335
|
+
try {
|
|
336
|
+
const synced = await deps.syncCompanionGit(
|
|
337
|
+
input.companionPath,
|
|
338
|
+
input.repository?.path,
|
|
339
|
+
deps.isInteractiveTerminal(),
|
|
340
|
+
);
|
|
341
|
+
run.push({ step: "git-sync", outcome: "ok" });
|
|
342
|
+
/** A pull may have changed the companion's own selection. */
|
|
343
|
+
if (synced?.changed) config = await deps.readCompanionConfig(input.companionPath);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
run.fail("git-sync", error);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
await runLocal(input, config, run);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The sync's own config read: only a fast-forward can have changed it. */
|
|
353
|
+
async function runSync(input: PrepareSessionInput, run: StepLog): Promise<void> {
|
|
354
|
+
const deps = sessionPrepareDeps;
|
|
355
|
+
let outcome: UnattendedSyncOutcome;
|
|
356
|
+
try {
|
|
357
|
+
outcome = await deps.syncCompanionUnattended(input.companionPath, { ttlMs: 0 });
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const reason = message(error);
|
|
360
|
+
/** Persisted like any unfinished sync, so the TUI's session lines carry it. */
|
|
361
|
+
deps.recordCompanionSyncUnfinished(input.companionPath, reason);
|
|
362
|
+
outcome = { status: "unfinished", changed: false, companionPath: input.companionPath, reason };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (outcome.status === "unfinished") {
|
|
366
|
+
run.push({ step: "git-sync", outcome: "warning", detail: outcome.reason });
|
|
367
|
+
run.warnings.push(...launchRecoveryStalenessLines(outcome, runtimesOf(input)));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (outcome.status !== "complete") {
|
|
371
|
+
run.push({ step: "git-sync", outcome: "skipped", detail: outcome.status });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!outcome.changed) {
|
|
375
|
+
run.push({ step: "git-sync", outcome: "ok", detail: "current" });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
run.push({ step: "git-sync", outcome: "ok", detail: "fast-forwarded" });
|
|
379
|
+
run.companionChanged = true;
|
|
380
|
+
await runLocal(input, await deps.readCompanionConfig(input.companionPath), run);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function lockOptions(input: PrepareSessionInput): CompanionLockOptions {
|
|
384
|
+
if (input.mode === "attended") return attendedLockOptions(input.companionPath);
|
|
385
|
+
if (input.mode === "local") return { wait: false };
|
|
386
|
+
return { timeoutMs: SYNC_LOCK_TIMEOUT_MS };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function prepareSession(input: PrepareSessionInput): Promise<PrepareResult> {
|
|
390
|
+
const deps = sessionPrepareDeps;
|
|
391
|
+
const run = new StepLog(input.mode === "attended");
|
|
392
|
+
const result = (): PrepareResult => ({
|
|
393
|
+
ok: !run.steps.some((step) => step.outcome === "failed"),
|
|
394
|
+
mode: input.mode,
|
|
395
|
+
runtime: input.runtime,
|
|
396
|
+
runtimes: runtimesOf(input),
|
|
397
|
+
...(input.repository ? { repositoryId: input.repository.id } : {}),
|
|
398
|
+
companionPath: input.companionPath,
|
|
399
|
+
steps: run.steps,
|
|
400
|
+
warnings: run.warnings,
|
|
401
|
+
...(run.companionChanged ? { companionChanged: true as const } : {}),
|
|
402
|
+
...(run.failure === undefined ? {} : { failure: run.failure }),
|
|
403
|
+
...(run.wrapState ? { wrapState: run.wrapState } : {}),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const locked = await deps.withCompanionLock(input.companionPath, lockOptions(input), async () => {
|
|
407
|
+
if (input.mode === "sync") return runSync(input, run);
|
|
408
|
+
const config = input.config ?? (await deps.readCompanionConfig(input.companionPath));
|
|
409
|
+
return input.mode === "attended"
|
|
410
|
+
? runAttended(input, config, run)
|
|
411
|
+
: runLocal(input, config, run);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (!locked.acquired) {
|
|
415
|
+
if (input.mode === "local") {
|
|
416
|
+
/** Another prepare is on it; the projection already in place serves the session. */
|
|
417
|
+
run.push({ step: "lock", outcome: "skipped", detail: "another prepare holds the companion" });
|
|
418
|
+
} else {
|
|
419
|
+
const detail = "another companion sync did not finish in time; skipped";
|
|
420
|
+
run.push({ step: "lock", outcome: "warning", detail });
|
|
421
|
+
run.warnings.push(detail);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return result();
|
|
425
|
+
}
|
|
@@ -173,7 +173,7 @@ export const BUILTIN_SETUP_COMPATIBILITIES: SetupCompatibility[] = [
|
|
|
173
173
|
kind: "capability",
|
|
174
174
|
capability: { name: "react-doctor" },
|
|
175
175
|
label: "React Doctor",
|
|
176
|
-
description: "Install
|
|
176
|
+
description: "Install the mate-react-doctor skill that verifies React changes during apply.",
|
|
177
177
|
defaultSelected: true,
|
|
178
178
|
templateRoot: "capabilities/react-doctor",
|
|
179
179
|
},
|
|
@@ -6,7 +6,7 @@ import { parse } from "yaml";
|
|
|
6
6
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
7
7
|
import { fileExists } from "../fs-utils";
|
|
8
8
|
import { GlobalConfigStore } from "./global-config-store";
|
|
9
|
-
import { CompanionResolver, type CompanionMatch } from "./
|
|
9
|
+
import { CompanionResolver, type CompanionMatch } from "./repository-link";
|
|
10
10
|
|
|
11
11
|
export type SetupPreflightResult =
|
|
12
12
|
| { kind: "existing-companion" }
|
|
@@ -104,6 +104,7 @@ export async function looksLikeWorkingRepo(cwd: string): Promise<boolean> {
|
|
|
104
104
|
const resolvedCwd = path.resolve(cwd);
|
|
105
105
|
|
|
106
106
|
for (const marker of STRONG_FILE_MARKERS) {
|
|
107
|
+
// oxlint-disable-next-line no-await-in-loop -- one strong marker settles it; stop probing
|
|
107
108
|
if (await fileExists(path.join(resolvedCwd, marker))) {
|
|
108
109
|
return true;
|
|
109
110
|
}
|
|
@@ -115,6 +116,7 @@ export async function looksLikeWorkingRepo(cwd: string): Promise<boolean> {
|
|
|
115
116
|
|
|
116
117
|
let weakCount = 0;
|
|
117
118
|
for (const marker of WEAK_FILE_MARKERS) {
|
|
119
|
+
// oxlint-disable-next-line no-await-in-loop -- stops probing at the second weak marker
|
|
118
120
|
if (await fileExists(path.join(resolvedCwd, marker))) {
|
|
119
121
|
weakCount++;
|
|
120
122
|
if (weakCount >= 2) return true;
|
|
@@ -127,9 +127,12 @@ export interface StudioTerminalConfig {
|
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* A named slice of a companion's configuration, activated by `MATE_AUDIENCE`.
|
|
130
|
-
* Plugins add to the base
|
|
130
|
+
* Plugins and capabilities add to the base lists; `studio.terminal.agent`
|
|
131
|
+
* overrides the base. Capabilities here name what a declared plugin provides,
|
|
132
|
+
* never a built-in one, so what `install` writes does not depend on the audience.
|
|
131
133
|
*/
|
|
132
134
|
export interface AudienceConfig {
|
|
135
|
+
capabilities?: CapabilityConfig[];
|
|
133
136
|
plugins?: PluginDeclaration[];
|
|
134
137
|
studio?: { terminal?: StudioTerminalConfig };
|
|
135
138
|
}
|