@uniqbit/mate-core 0.18.1-canary.0 → 0.18.1-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/hooks/session-banner.mjs +1 -1
- package/opencode-plugin/server.ts +5 -0
- package/opencode-plugin/src/add-dir.ts +118 -0
- package/opencode-plugin/src/companion.ts +180 -0
- package/opencode-plugin/src/server.ts +26 -0
- package/opencode-plugin/src/tui.tsx +1 -0
- package/opencode-plugin/tui.tsx +2 -0
- package/package.json +13 -6
- package/src/cli/commands/artifact/finish/engine.ts +1 -1
- package/src/cli/commands/artifact/finish/git.ts +75 -51
- package/src/cli/commands/companion/hub.ts +1 -1
- package/src/cli/commands/companion/link.ts +13 -59
- package/src/cli/commands/doctor.ts +30 -4
- package/src/cli/commands/install.ts +17 -1
- package/src/cli/commands/plugin/plugin.ts +0 -4
- package/src/cli/commands/report/formatter.ts +0 -4
- package/src/cli/commands/studio/selection.ts +3 -0
- package/src/cli/commands/studio/terminal-launch.ts +1 -51
- package/src/cli/commands/studio/terminal.ts +2 -13
- package/src/cli/commands/studio/vault.ts +5 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +5 -4
- package/src/cli/commands/update.ts +4 -20
- package/src/cli/commands/working/cleanup.ts +4 -1
- package/src/cli/launch-selector.tsx +0 -318
- package/src/cli/main.ts +0 -29
- package/src/cli/setup-selector.tsx +1 -1
- package/src/cli/usage.ts +0 -1
- package/src/hooks/session-banner.ts +7 -5
- package/src/hooks/validate-artifact-path.ts +17 -6
- package/src/lib/opencode-plugin-package.ts +7 -28
- package/src/lib/orchestrator/adapters/opencode.ts +28 -67
- package/src/lib/orchestrator/companion-git-sync.ts +29 -93
- package/src/lib/orchestrator/companion-hub.ts +71 -110
- package/src/lib/orchestrator/repo-local-registry.ts +0 -21
- package/src/lib/orchestrator/types.ts +0 -6
- package/src/lib/orchestrator/working-repo-projection.ts +13 -9
- package/src/lib/package-paths.ts +32 -0
- package/src/lib/prebuilt-workspace.ts +0 -3
- package/src/opencode/companion-hooks.ts +15 -10
- package/src/runtime/companion-git.ts +327 -84
- package/src/runtime/companion-sync.ts +34 -43
- package/src/runtime/index.ts +7 -5
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +2 -2
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +25 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +9 -7
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +34 -4
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +2 -2
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +25 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +9 -7
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +34 -4
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +68 -77
- package/src/tools/setup/capabilities/graphify.ts +0 -4
- package/src/tools/setup/dynamic-plugins/frozen.ts +24 -3
- package/src/tools/setup/dynamic-plugins/loader.ts +16 -0
- package/src/tools/setup/dynamic-plugins/policy.ts +55 -0
- package/src/tools/setup/dynamic-plugins/staging.ts +55 -0
- package/src/tools/setup/dynamic-plugins/verify.ts +22 -18
- package/src/tools/setup/providers/opencode-format.ts +4 -0
- package/src/tools/setup/providers/opencode.ts +9 -65
- package/src/tools/setup/runtime-documents.ts +8 -9
- package/src/cli/commands/plugin/verify.ts +0 -29
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
3
|
import fs from "node:fs/promises";
|
|
5
4
|
|
|
6
5
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
@@ -19,16 +18,16 @@ import { runSetupFlowAtPath } from "../setup";
|
|
|
19
18
|
import { runInstallCommand } from "../install";
|
|
20
19
|
import type { CompanionSource, LinkedRepository } from "../../../lib/orchestrator/types";
|
|
21
20
|
import { invalidateInstallState } from "../../../lib/install";
|
|
21
|
+
import { companionGit, describeGitFailure, type GitResult } from "../../../runtime/companion-git";
|
|
22
22
|
|
|
23
23
|
interface CompanionLinkCommandDeps {
|
|
24
24
|
selectCompanionLinkInputs?: (
|
|
25
25
|
options: CompanionLinkWizardOptions,
|
|
26
26
|
) => Promise<CompanionLinkInputs | null>;
|
|
27
27
|
extractRepoName?: (url: string) => string | null;
|
|
28
|
-
|
|
28
|
+
cloneCompanion?: (url: string, destination: string) => Promise<GitResult>;
|
|
29
29
|
mkdir?: typeof fs.mkdir;
|
|
30
30
|
stat?: typeof fs.stat;
|
|
31
|
-
rm?: typeof fs.rm;
|
|
32
31
|
listCompanions?: () => Promise<string[]>;
|
|
33
32
|
registerCompanion?: (companionPath: string) => Promise<void>;
|
|
34
33
|
resolveCompanion?: (cwd: string) => ReturnType<CompanionResolver["resolve"]>;
|
|
@@ -61,23 +60,6 @@ export async function runCompanionLinkCommand(argv: string[]): Promise<void> {
|
|
|
61
60
|
return companionLinkCommandDeps.runCompanionLinkCommandWithDeps(argv);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
function toSshUrl(url: string): string | null {
|
|
65
|
-
try {
|
|
66
|
-
const parsed = new URL(url.trim());
|
|
67
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
68
|
-
if (!parsed.hostname || parsed.pathname === "/") return null;
|
|
69
|
-
|
|
70
|
-
const repositoryPath = parsed.pathname.replace(/^\/+/, "");
|
|
71
|
-
if (!repositoryPath) return null;
|
|
72
|
-
|
|
73
|
-
// Keep the usual scp-style form when no SSH port is specified.
|
|
74
|
-
if (!parsed.port) return `git@${parsed.hostname}:${repositoryPath}`;
|
|
75
|
-
return `ssh://git@${parsed.hostname}:${parsed.port}/${repositoryPath}`;
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
63
|
function exitWithError(message: string): never {
|
|
82
64
|
process.stderr.write(`${message}\n`);
|
|
83
65
|
process.exit(1);
|
|
@@ -96,10 +78,12 @@ export async function runCompanionLinkCommandWithDeps(
|
|
|
96
78
|
|
|
97
79
|
const selectInputs = deps.selectCompanionLinkInputs ?? selectCompanionLinkInputs;
|
|
98
80
|
const extractName = deps.extractRepoName ?? extractRepoName;
|
|
99
|
-
const
|
|
81
|
+
const cloneCompanion =
|
|
82
|
+
deps.cloneCompanion ??
|
|
83
|
+
((url: string, destination: string) =>
|
|
84
|
+
companionGit(path.dirname(destination), { prompt: "if-terminal" }).clone(url, destination));
|
|
100
85
|
const mkdir = deps.mkdir ?? fs.mkdir;
|
|
101
86
|
const stat = deps.stat ?? fs.stat;
|
|
102
|
-
const rm = deps.rm ?? fs.rm;
|
|
103
87
|
const listCompanions =
|
|
104
88
|
deps.listCompanions ??
|
|
105
89
|
(() => {
|
|
@@ -165,44 +149,14 @@ export async function runCompanionLinkCommandWithDeps(
|
|
|
165
149
|
const existingTarget = await stat(companionPath).catch(() => null);
|
|
166
150
|
if (!existingTarget?.isDirectory()) {
|
|
167
151
|
await mkdir(companionsDir, { recursive: true });
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
| undefined;
|
|
177
|
-
const cloneErrors: string[] = [];
|
|
178
|
-
|
|
179
|
-
for (const cloneUrl of cloneUrls) {
|
|
180
|
-
cloneResult = spawn("git", ["clone", cloneUrl, companionPath], {
|
|
181
|
-
encoding: "utf8",
|
|
182
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
183
|
-
});
|
|
184
|
-
if (cloneResult.status === 0) {
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
cloneErrors.push(
|
|
189
|
-
String(cloneResult.stderr || cloneResult.stdout || `Clone failed for ${cloneUrl}`),
|
|
152
|
+
const clone = await cloneCompanion(gitUrl, companionPath);
|
|
153
|
+
if (clone.status !== 0) {
|
|
154
|
+
const detail = describeGitFailure(clone);
|
|
155
|
+
exitWithError(
|
|
156
|
+
`mate: failed to clone companion repository:\n${
|
|
157
|
+
detail === "unknown Git error" ? "review the Git or SSH diagnostics above" : detail
|
|
158
|
+
}`,
|
|
190
159
|
);
|
|
191
|
-
try {
|
|
192
|
-
await rm(companionPath, { recursive: true, force: true });
|
|
193
|
-
} catch {
|
|
194
|
-
// Ignore cleanup errors after a failed clone.
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (!cloneResult || cloneResult.status !== 0) {
|
|
199
|
-
try {
|
|
200
|
-
await rm(companionPath, { recursive: true, force: true });
|
|
201
|
-
} catch {
|
|
202
|
-
// Ignore cleanup errors after a failed clone.
|
|
203
|
-
}
|
|
204
|
-
const output = cloneErrors.join("\n\n");
|
|
205
|
-
exitWithError(`mate: failed to clone companion repository:\n${output}`);
|
|
206
160
|
}
|
|
207
161
|
}
|
|
208
162
|
companionPaths = [companionPath];
|
|
@@ -2,12 +2,17 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
|
+
import { gitEnvironment } from "../../runtime/companion-git";
|
|
5
6
|
import { getActiveDistribution } from "../../distribution";
|
|
6
7
|
import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
|
|
7
8
|
import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
|
|
8
9
|
import { pathIsDirectory } from "../../lib/orchestrator/repo-local-registry";
|
|
9
10
|
import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
|
|
10
11
|
import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
|
|
12
|
+
import {
|
|
13
|
+
verifyDeclaredPlugins,
|
|
14
|
+
type PluginVerificationFailure,
|
|
15
|
+
} from "../../tools/setup/dynamic-plugins/verify";
|
|
11
16
|
import { getRequiredPluginDrift } from "../../tools/setup/policy";
|
|
12
17
|
import { resolveCommandOnPath } from "../../tools/setup/utils";
|
|
13
18
|
import { writeJsonStdout } from "../write-json-stdout";
|
|
@@ -19,6 +24,7 @@ interface DoctorDeps {
|
|
|
19
24
|
pathValue?: string;
|
|
20
25
|
/** Returns a member checkout's HEAD commit, or null when unreadable. */
|
|
21
26
|
gitHead?: (memberPath: string) => string | null;
|
|
27
|
+
verifyPlugins?: (companionPath: string) => Promise<PluginVerificationFailure[]>;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export type DoctorKind = "core" | "working" | "companion" | "hub";
|
|
@@ -51,6 +57,7 @@ export interface DoctorReport {
|
|
|
51
57
|
capabilities?: string[];
|
|
52
58
|
toolInstallations?: DoctorToolInstallation[];
|
|
53
59
|
requiredPluginDrift?: Array<{ pluginId: string; kind: string; reason: string }>;
|
|
60
|
+
pluginFailures?: PluginVerificationFailure[];
|
|
54
61
|
engineRequirement?: { range: string; ok: boolean; detail: string };
|
|
55
62
|
hub?: { members: DoctorHubMember[] };
|
|
56
63
|
resolutionFailures: Array<{ companionPath: string; message: string }>;
|
|
@@ -74,6 +81,7 @@ function defaultGitHead(memberPath: string): string | null {
|
|
|
74
81
|
try {
|
|
75
82
|
return execFileSync("git", ["-C", memberPath, "rev-parse", "HEAD"], {
|
|
76
83
|
stdio: ["ignore", "pipe", "ignore"],
|
|
84
|
+
env: gitEnvironment(),
|
|
77
85
|
})
|
|
78
86
|
.toString()
|
|
79
87
|
.trim();
|
|
@@ -237,6 +245,10 @@ export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<Doctor
|
|
|
237
245
|
report.engineRequirement = collectEngineRequirement(root.config);
|
|
238
246
|
}
|
|
239
247
|
|
|
248
|
+
if (root.rootPath && root.config) {
|
|
249
|
+
report.pluginFailures = await (deps.verifyPlugins ?? verifyDeclaredPlugins)(root.rootPath);
|
|
250
|
+
}
|
|
251
|
+
|
|
240
252
|
return report;
|
|
241
253
|
}
|
|
242
254
|
|
|
@@ -329,6 +341,15 @@ function renderHumanReport(report: DoctorReport): void {
|
|
|
329
341
|
),
|
|
330
342
|
);
|
|
331
343
|
}
|
|
344
|
+
if (report.pluginFailures && report.pluginFailures.length > 0) {
|
|
345
|
+
printSection(
|
|
346
|
+
"Declared Plugins",
|
|
347
|
+
renderTable(
|
|
348
|
+
["Plugin", "Issue"],
|
|
349
|
+
report.pluginFailures.map((failure) => [failure.package, failure.reason]),
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
332
353
|
if (report.engineRequirement) {
|
|
333
354
|
printSection("Version Requirement", report.engineRequirement.detail);
|
|
334
355
|
}
|
|
@@ -350,13 +371,18 @@ function renderHumanReport(report: DoctorReport): void {
|
|
|
350
371
|
* working repository), `companion`, `hub`, or `core` (not linked) — with
|
|
351
372
|
* kind-specific diagnostics: policy and capabilities for working repos,
|
|
352
373
|
* capabilities and tool checks for companions, member health for hubs.
|
|
374
|
+
* Declared plugins are verified strictly (allowed by `MATE_ALLOWED_PLUGINS`,
|
|
375
|
+
* installed, loadable); any failure is named on stderr and exits non-zero.
|
|
353
376
|
* `--json` emits the same report as one JSON document.
|
|
354
377
|
*/
|
|
355
378
|
export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
|
|
356
379
|
const report = await collectDoctorReport(deps);
|
|
357
|
-
if (argv.includes("--json"))
|
|
358
|
-
|
|
359
|
-
|
|
380
|
+
if (argv.includes("--json")) await writeJsonStdout(report);
|
|
381
|
+
else renderHumanReport(report);
|
|
382
|
+
|
|
383
|
+
const failures = report.pluginFailures ?? [];
|
|
384
|
+
for (const failure of failures) {
|
|
385
|
+
process.stderr.write(`${FRAMEWORK_NAME}: plugin ${failure.package}: ${failure.reason}\n`);
|
|
360
386
|
}
|
|
361
|
-
|
|
387
|
+
if (failures.length > 0) process.exitCode = 1;
|
|
362
388
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
FrozenInstallError,
|
|
20
20
|
installDeclaredPluginsFrozen,
|
|
21
21
|
} from "../../tools/setup/dynamic-plugins/frozen";
|
|
22
|
+
import { verifyTrackedPluginOutputs } from "../../tools/setup/dynamic-plugins/staging";
|
|
22
23
|
import { verifyDeclaredPlugins } from "../../tools/setup/dynamic-plugins/verify";
|
|
23
24
|
|
|
24
25
|
export function reportPluginInstallResults(results: PluginInstallResult[]): boolean {
|
|
@@ -158,7 +159,22 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
try {
|
|
161
|
-
|
|
162
|
+
if (frozen && plan.context.companionPath) {
|
|
163
|
+
const { syncCompanionFiles } = await import("../../tools/setup");
|
|
164
|
+
const drift = await verifyTrackedPluginOutputs(plan.context.companionPath, (staged) =>
|
|
165
|
+
syncCompanionFiles(staged, plan.context.config),
|
|
166
|
+
);
|
|
167
|
+
if (drift.length > 0) {
|
|
168
|
+
process.stderr.write(
|
|
169
|
+
`${FRAMEWORK_NAME}: plugin-generated files differ from the committed checkout: ${drift.join(", ")}\n` +
|
|
170
|
+
`Prepare and commit them in the companion's authoring flow; the checkout was left unchanged.\n`,
|
|
171
|
+
);
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
await reconcileInstalledCompanion(plan);
|
|
177
|
+
}
|
|
162
178
|
await saveCompleteInstallState(plan, execution.results);
|
|
163
179
|
} catch (error) {
|
|
164
180
|
process.stderr.write(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { usage } from "../../usage";
|
|
2
2
|
import { runPluginInstallCommand } from "./install";
|
|
3
|
-
import { runPluginVerifyCommand } from "./verify";
|
|
4
3
|
|
|
5
4
|
export async function runPluginCommand(
|
|
6
5
|
subcommand: string | undefined,
|
|
@@ -10,9 +9,6 @@ export async function runPluginCommand(
|
|
|
10
9
|
case "install":
|
|
11
10
|
await runPluginInstallCommand(argv);
|
|
12
11
|
return;
|
|
13
|
-
case "verify":
|
|
14
|
-
await runPluginVerifyCommand(argv);
|
|
15
|
-
return;
|
|
16
12
|
default:
|
|
17
13
|
console.error(`Unknown plugin command: ${subcommand ?? ""}`);
|
|
18
14
|
console.error(usage());
|
|
@@ -30,6 +30,9 @@ export const STUDIO_PARAMS: readonly string[] = [
|
|
|
30
30
|
/** Filled into a prompt when no change is named, so a prompt is never half-written. */
|
|
31
31
|
export const CHANGE_PLACEHOLDER = "<change-name>";
|
|
32
32
|
|
|
33
|
+
/** Filled into pre-explore prompts, which run before any change exists. */
|
|
34
|
+
export const IDEA_PLACEHOLDER = "<idea>";
|
|
35
|
+
|
|
33
36
|
const DIGEST_LENGTH = 10;
|
|
34
37
|
|
|
35
38
|
interface SelectionBase {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
4
3
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
@@ -27,56 +26,11 @@ export async function launchableAgents(
|
|
|
27
26
|
return TERMINAL_AGENTS.filter((agent) => allowed.includes(agent) && installed(agent));
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
/** Lowercase letters, digits and hyphens, so a name can never carry shell syntax or a flag. */
|
|
31
|
-
export const AGENT_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
32
|
-
|
|
33
|
-
const AGENT_DEFINITION_DIRS: Record<TerminalAgent, string> = {
|
|
34
|
-
claude: path.join(".claude", "agents"),
|
|
35
|
-
opencode: path.join(".opencode", "agents"),
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export interface DefaultAgentOutcome {
|
|
39
|
-
agentArgs?: string[];
|
|
40
|
-
notice?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The companion's `studio.terminal.agent` for one provider. Never throws and
|
|
45
|
-
* never echoes the configured value into the notice.
|
|
46
|
-
*/
|
|
47
|
-
export async function defaultAgentFor(
|
|
48
|
-
companionPath: string,
|
|
49
|
-
agent: TerminalAgent,
|
|
50
|
-
): Promise<DefaultAgentOutcome> {
|
|
51
|
-
const configPath = path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml");
|
|
52
|
-
if (!fs.existsSync(configPath)) return {};
|
|
53
|
-
let name: unknown;
|
|
54
|
-
try {
|
|
55
|
-
name = (await new ConfigStore(configPath).load()).studio?.terminal?.agent;
|
|
56
|
-
} catch {
|
|
57
|
-
return {};
|
|
58
|
-
}
|
|
59
|
-
if (name === undefined || name === null) return {};
|
|
60
|
-
if (typeof name !== "string" || !AGENT_NAME_PATTERN.test(name)) {
|
|
61
|
-
return {
|
|
62
|
-
notice: "Default agent not applied: studio.terminal.agent is not a valid agent name.",
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
const definition = path.join(companionPath, AGENT_DEFINITION_DIRS[agent], `${name}.md`);
|
|
66
|
-
if (!fs.existsSync(definition)) {
|
|
67
|
-
return {
|
|
68
|
-
notice: `Default agent "${name}" not applied: this companion has no ${agent} definition for it.`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return { agentArgs: ["--agent", name] };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
29
|
export interface LaunchResolverOptions {
|
|
75
30
|
collectInventory: () => Promise<StudioInventory>;
|
|
76
31
|
/** Pinned by `serve --companion`; overrides the page selection. */
|
|
77
32
|
launchCompanion?: string | null;
|
|
78
33
|
launchableAgents?: (companionPath: string) => Promise<TerminalAgent[]>;
|
|
79
|
-
defaultAgent?: (companionPath: string, agent: TerminalAgent) => Promise<DefaultAgentOutcome>;
|
|
80
34
|
}
|
|
81
35
|
|
|
82
36
|
/** The companion a launch targets: the pinned one, else the page's, both read from the current inventory. */
|
|
@@ -96,7 +50,6 @@ export function createLaunchResolver(
|
|
|
96
50
|
options: LaunchResolverOptions,
|
|
97
51
|
): (agent: string, digest: string | null) => Promise<TerminalLaunchResolution> {
|
|
98
52
|
const agents = options.launchableAgents ?? launchableAgents;
|
|
99
|
-
const defaultAgent = options.defaultAgent ?? defaultAgentFor;
|
|
100
53
|
return async (agent, digest) => {
|
|
101
54
|
const companion = await effectiveLaunchCompanion(
|
|
102
55
|
await options.collectInventory(),
|
|
@@ -113,9 +66,6 @@ export function createLaunchResolver(
|
|
|
113
66
|
if (!(await agents(companion.path)).includes(agent as TerminalAgent)) {
|
|
114
67
|
return { reason: `${agent} is not allowed by this companion or is not installed` };
|
|
115
68
|
}
|
|
116
|
-
return {
|
|
117
|
-
companionPath: companion.path,
|
|
118
|
-
...(await defaultAgent(companion.path, agent as TerminalAgent)),
|
|
119
|
-
};
|
|
69
|
+
return { companionPath: companion.path };
|
|
120
70
|
};
|
|
121
71
|
}
|
|
@@ -177,15 +177,7 @@ export interface TerminalSessionInfo {
|
|
|
177
177
|
startedAt: number;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
export type TerminalLaunchResolution =
|
|
181
|
-
| {
|
|
182
|
-
companionPath: string;
|
|
183
|
-
/** Extra argv entries for the managed launch; set only by companion configuration. */
|
|
184
|
-
agentArgs?: string[];
|
|
185
|
-
/** Written to the terminal before the agent starts. */
|
|
186
|
-
notice?: string;
|
|
187
|
-
}
|
|
188
|
-
| { reason: string };
|
|
180
|
+
export type TerminalLaunchResolution = { companionPath: string } | { reason: string };
|
|
189
181
|
|
|
190
182
|
export interface TerminalRegistryOptions {
|
|
191
183
|
detachMs: number;
|
|
@@ -396,7 +388,7 @@ export class TerminalRegistry {
|
|
|
396
388
|
void this.finish(detached, { type: "ended", reason: "evicted" });
|
|
397
389
|
}
|
|
398
390
|
|
|
399
|
-
const session = this.launch(agent, resolved.companionPath, size
|
|
391
|
+
const session = this.launch(agent, resolved.companionPath, size);
|
|
400
392
|
this.attachTo(connection, session, size);
|
|
401
393
|
}
|
|
402
394
|
|
|
@@ -404,7 +396,6 @@ export class TerminalRegistry {
|
|
|
404
396
|
agent: TerminalAgent,
|
|
405
397
|
companionPath: string,
|
|
406
398
|
size: { cols: number; rows: number },
|
|
407
|
-
extras: { agentArgs?: string[]; notice?: string } = {},
|
|
408
399
|
): Session {
|
|
409
400
|
const env: Record<string, string> = {};
|
|
410
401
|
for (const [key, value] of Object.entries(this.options.env ?? process.env)) {
|
|
@@ -422,7 +413,6 @@ export class TerminalRegistry {
|
|
|
422
413
|
"--companion",
|
|
423
414
|
"--yes",
|
|
424
415
|
...(this.options.noGit ? ["--no-git"] : []),
|
|
425
|
-
...(extras.agentArgs ?? []),
|
|
426
416
|
];
|
|
427
417
|
|
|
428
418
|
const session = {
|
|
@@ -437,7 +427,6 @@ export class TerminalRegistry {
|
|
|
437
427
|
ending: null,
|
|
438
428
|
exited: false,
|
|
439
429
|
} as Omit<Session, "process"> as Session;
|
|
440
|
-
if (extras.notice) session.ring.push(new TextEncoder().encode(`${extras.notice}\r\n`));
|
|
441
430
|
session.process = this.spawn({
|
|
442
431
|
argv,
|
|
443
432
|
cwd: companionPath,
|
|
@@ -4,6 +4,8 @@ import * as fsp from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
|
|
7
|
+
import { gitEnvironment } from "../../../runtime/companion-git";
|
|
8
|
+
|
|
7
9
|
export interface VaultTreeNode {
|
|
8
10
|
name: string;
|
|
9
11
|
path: string;
|
|
@@ -108,7 +110,7 @@ function inside(root: string, candidate: string): boolean {
|
|
|
108
110
|
);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
async function realpathContaining(
|
|
113
|
+
async function realpathContaining(candidate: string): Promise<string> {
|
|
112
114
|
let current = candidate;
|
|
113
115
|
while (true) {
|
|
114
116
|
try {
|
|
@@ -146,7 +148,7 @@ export async function resolveVaultPath(
|
|
|
146
148
|
absolute = await fsp.realpath(lexical);
|
|
147
149
|
} catch (error) {
|
|
148
150
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
149
|
-
const containing = await realpathContaining(
|
|
151
|
+
const containing = await realpathContaining(path.dirname(lexical));
|
|
150
152
|
if (!inside(root, containing))
|
|
151
153
|
throw new VaultPathError("the requested path leaves the companion root");
|
|
152
154
|
absolute = lexical;
|
|
@@ -167,7 +169,7 @@ export async function resolveVaultPath(
|
|
|
167
169
|
|
|
168
170
|
function runGit(args: string[], input?: string): Promise<VaultGitResult> {
|
|
169
171
|
return new Promise((resolve, reject) => {
|
|
170
|
-
const child = spawn("git", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
172
|
+
const child = spawn("git", args, { stdio: ["pipe", "pipe", "pipe"], env: gitEnvironment() });
|
|
171
173
|
const stdout: Buffer[] = [];
|
|
172
174
|
const stderr: Buffer[] = [];
|
|
173
175
|
child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CHANGE_PLACEHOLDER } from "../../selection";
|
|
1
|
+
import { CHANGE_PLACEHOLDER, IDEA_PLACEHOLDER } from "../../selection";
|
|
2
2
|
|
|
3
3
|
export type WorkflowStepKind = "skill" | "review" | "completion" | "optional";
|
|
4
4
|
export type WorkflowStepBadge = "optional" | "skill" | "openspec" | "human-in-loop";
|
|
@@ -56,11 +56,12 @@ function stepPrompt(
|
|
|
56
56
|
includeChange = true,
|
|
57
57
|
): string {
|
|
58
58
|
const changeNote = includeChange ? ` for ${CHANGE_PLACEHOLDER}` : " for";
|
|
59
|
+
const ideaNote = includeChange ? ` about ${IDEA_PLACEHOLDER}` : " about";
|
|
59
60
|
const schemaNote = profile && profile !== "mate-v1" ? ` (use schema: ${profile})` : "";
|
|
60
61
|
|
|
61
62
|
switch (step.id) {
|
|
62
63
|
case "pre-explore":
|
|
63
|
-
return `/mate-interview-me or /mate-grill-me${
|
|
64
|
+
return `/mate-interview-me or /mate-grill-me${ideaNote}`;
|
|
64
65
|
case "explore":
|
|
65
66
|
return `/openspec-explore${schemaNote}${changeNote}`;
|
|
66
67
|
case "proposal":
|
|
@@ -172,8 +173,8 @@ export function workflowPlan(
|
|
|
172
173
|
name === "mate-interview-me"
|
|
173
174
|
? "Difference: guided, one-question-at-a-time clarification. Best when: the request is ambiguous and intent, constraints, or scope still need confirmation."
|
|
174
175
|
: "Difference: adversarial, round-based review of the full design-tree frontier. Best when: the direction is mostly known but hidden assumptions, risks, and downstream impacts need surfacing.",
|
|
175
|
-
prompt: `/${name}
|
|
176
|
-
copyPrompt: `/${name}
|
|
176
|
+
prompt: `/${name} about ${IDEA_PLACEHOLDER}`,
|
|
177
|
+
copyPrompt: `/${name} about`,
|
|
177
178
|
});
|
|
178
179
|
}
|
|
179
180
|
const start = optionalStep(
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
|
-
import {
|
|
5
|
-
OPENCODE_PLUGIN_PACKAGE_NAME,
|
|
6
|
-
warmOpenCodePluginCache,
|
|
7
|
-
} from "../../lib/opencode-plugin-package";
|
|
8
4
|
import { installPublicPackageSync, isPackageInstalledAtNpmGlobalRoot } from "../../lib/public-npm";
|
|
9
5
|
import {
|
|
10
6
|
fetchLatestVersion,
|
|
@@ -66,8 +62,6 @@ export const updateCommandDeps = {
|
|
|
66
62
|
},
|
|
67
63
|
spawnPostInstall: (executable: string, args: string[]): InstallResult =>
|
|
68
64
|
spawnSync(executable, args, { stdio: "inherit" }) as InstallResult,
|
|
69
|
-
warmOpenCodePluginCache: (latest: string) =>
|
|
70
|
-
warmOpenCodePluginCache(latest, process.env, getUpdateConfig().registry),
|
|
71
65
|
};
|
|
72
66
|
|
|
73
67
|
/**
|
|
@@ -157,20 +151,6 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
157
151
|
|
|
158
152
|
await updateCommandDeps.saveUpdateState(latest);
|
|
159
153
|
|
|
160
|
-
// The plugin package is released in lockstep with the CLI; pre-fetch the
|
|
161
|
-
// newly coordinated version into OpenCode's plugin environment so the next
|
|
162
|
-
// managed launch does not stall on a registry download. Best effort only.
|
|
163
|
-
const warmed = await updateCommandDeps.warmOpenCodePluginCache(latest);
|
|
164
|
-
if (!warmed.ok) {
|
|
165
|
-
process.stderr.write(
|
|
166
|
-
[
|
|
167
|
-
`${FRAMEWORK_NAME}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
|
|
168
|
-
"The next managed OpenCode launch will download it (requires registry access).",
|
|
169
|
-
...(warmed.detail ? [`Details: ${warmed.detail}`] : []),
|
|
170
|
-
].join("\n") + "\n",
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
154
|
const postInstall = updateCommandDeps.runPostInstall(skipConfirm);
|
|
175
155
|
if (postInstall.status !== 0 || postInstall.error) {
|
|
176
156
|
process.stderr.write(
|
|
@@ -182,4 +162,8 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
182
162
|
|
|
183
163
|
console.log(`\nUpgraded to ${latest}.`);
|
|
184
164
|
console.log("Post-update installation complete.");
|
|
165
|
+
/** Managed launches run standalone; only a long-lived service keeps the replaced plugin loaded. */
|
|
166
|
+
console.log(
|
|
167
|
+
"Restart any running OpenCode service to load the new plugin: `opencode service restart`.",
|
|
168
|
+
);
|
|
185
169
|
}
|
|
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { parse } from "yaml";
|
|
7
7
|
|
|
8
8
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
9
|
+
import { gitEnvironment } from "../../../runtime/companion-git";
|
|
9
10
|
import type { RootKind } from "../../../lib/orchestrator/root-context";
|
|
10
11
|
import {
|
|
11
12
|
collectWorkspaceInventory,
|
|
@@ -39,7 +40,9 @@ export interface WorkingCleanupCommandDeps {
|
|
|
39
40
|
|
|
40
41
|
async function resolveGitRoot(cwd: string): Promise<string | null> {
|
|
41
42
|
try {
|
|
42
|
-
const { stdout } = await execFileAsync("git", ["-C", cwd, "rev-parse", "--show-toplevel"]
|
|
43
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
44
|
+
env: gitEnvironment(),
|
|
45
|
+
});
|
|
43
46
|
return canonicalPath(stdout.trim());
|
|
44
47
|
} catch {
|
|
45
48
|
return null;
|