@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.
Files changed (61) hide show
  1. package/claude-plugin/hooks/session-banner.mjs +1 -1
  2. package/opencode-plugin/server.ts +5 -0
  3. package/opencode-plugin/src/add-dir.ts +118 -0
  4. package/opencode-plugin/src/companion.ts +180 -0
  5. package/opencode-plugin/src/server.ts +26 -0
  6. package/opencode-plugin/src/tui.tsx +1 -0
  7. package/opencode-plugin/tui.tsx +2 -0
  8. package/package.json +13 -6
  9. package/src/cli/commands/artifact/finish/engine.ts +1 -1
  10. package/src/cli/commands/artifact/finish/git.ts +75 -51
  11. package/src/cli/commands/companion/hub.ts +1 -1
  12. package/src/cli/commands/companion/link.ts +13 -59
  13. package/src/cli/commands/doctor.ts +30 -4
  14. package/src/cli/commands/install.ts +17 -1
  15. package/src/cli/commands/plugin/plugin.ts +0 -4
  16. package/src/cli/commands/report/formatter.ts +0 -4
  17. package/src/cli/commands/studio/selection.ts +3 -0
  18. package/src/cli/commands/studio/terminal-launch.ts +1 -51
  19. package/src/cli/commands/studio/terminal.ts +2 -13
  20. package/src/cli/commands/studio/vault.ts +5 -3
  21. package/src/cli/commands/studio/views/workflow/steps.ts +5 -4
  22. package/src/cli/commands/update.ts +4 -20
  23. package/src/cli/commands/working/cleanup.ts +4 -1
  24. package/src/cli/launch-selector.tsx +0 -318
  25. package/src/cli/main.ts +0 -29
  26. package/src/cli/setup-selector.tsx +1 -1
  27. package/src/cli/usage.ts +0 -1
  28. package/src/hooks/session-banner.ts +7 -5
  29. package/src/hooks/validate-artifact-path.ts +17 -6
  30. package/src/lib/opencode-plugin-package.ts +7 -28
  31. package/src/lib/orchestrator/adapters/opencode.ts +28 -67
  32. package/src/lib/orchestrator/companion-git-sync.ts +29 -93
  33. package/src/lib/orchestrator/companion-hub.ts +71 -110
  34. package/src/lib/orchestrator/repo-local-registry.ts +0 -21
  35. package/src/lib/orchestrator/types.ts +0 -6
  36. package/src/lib/orchestrator/working-repo-projection.ts +13 -9
  37. package/src/lib/package-paths.ts +32 -0
  38. package/src/lib/prebuilt-workspace.ts +0 -3
  39. package/src/opencode/companion-hooks.ts +15 -10
  40. package/src/runtime/companion-git.ts +327 -84
  41. package/src/runtime/companion-sync.ts +34 -43
  42. package/src/runtime/index.ts +7 -5
  43. package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +2 -2
  44. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +25 -0
  45. package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +9 -7
  46. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +34 -4
  47. package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +2 -2
  48. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +25 -0
  49. package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +9 -7
  50. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +34 -4
  51. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +68 -77
  52. package/src/tools/setup/capabilities/graphify.ts +0 -4
  53. package/src/tools/setup/dynamic-plugins/frozen.ts +24 -3
  54. package/src/tools/setup/dynamic-plugins/loader.ts +16 -0
  55. package/src/tools/setup/dynamic-plugins/policy.ts +55 -0
  56. package/src/tools/setup/dynamic-plugins/staging.ts +55 -0
  57. package/src/tools/setup/dynamic-plugins/verify.ts +22 -18
  58. package/src/tools/setup/providers/opencode-format.ts +4 -0
  59. package/src/tools/setup/providers/opencode.ts +9 -65
  60. package/src/tools/setup/runtime-documents.ts +8 -9
  61. 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
- spawnSync?: typeof spawnSync;
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 spawn = deps.spawnSync ?? spawnSync;
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 sshUrl = toSshUrl(gitUrl);
169
- const cloneUrls = sshUrl && sshUrl !== gitUrl ? [sshUrl, gitUrl] : [gitUrl];
170
- let cloneResult:
171
- | {
172
- status: number | null;
173
- stderr?: string | Buffer;
174
- stdout?: string | Buffer;
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
- await writeJsonStdout(report);
359
- return;
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
- renderHumanReport(report);
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
- await reconcileInstalledCompanion(plan);
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());
@@ -11,7 +11,3 @@ export function formatTokens(count: number): string {
11
11
  export function formatCost(amount: number): string {
12
12
  return `$${amount.toFixed(2)}`;
13
13
  }
14
-
15
- export function formatPercentage(value: number): string {
16
- return `${value.toFixed(1)}%`;
17
- }
@@ -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, resolved);
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(root: string, candidate: string): Promise<string> {
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(root, path.dirname(lexical));
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${changeNote}`;
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} for ${CHANGE_PLACEHOLDER}`,
176
- copyPrompt: `/${name} for`,
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;