@uniqbit/mate-core 0.17.0-canary.8 → 0.17.1

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 (103) hide show
  1. package/package.json +11 -5
  2. package/src/cli/commands/artifact/finish/index.ts +0 -9
  3. package/src/cli/commands/artifact/finish/openspec.ts +2 -2
  4. package/src/cli/commands/artifact/finish/registry.ts +1 -1
  5. package/src/cli/commands/artifact/pending/discovery.ts +1 -1
  6. package/src/cli/commands/artifact/pending/index.ts +0 -16
  7. package/src/cli/commands/cap/graphify.ts +1 -1
  8. package/src/cli/commands/cap/index-cmd.ts +1 -1
  9. package/src/cli/commands/cap/tokensave.ts +4 -3
  10. package/src/cli/commands/companion/companion.ts +4 -0
  11. package/src/cli/commands/companion/hub.ts +2 -1
  12. package/src/cli/commands/companion/list.ts +2 -1
  13. package/src/cli/commands/companion/prepare.ts +2 -2
  14. package/src/cli/commands/companion/update.ts +334 -0
  15. package/src/cli/commands/doctor.ts +2 -1
  16. package/src/cli/commands/install.ts +59 -5
  17. package/src/cli/commands/launch/shared.ts +2 -1
  18. package/src/cli/commands/plugin/plugin.ts +4 -0
  19. package/src/cli/commands/plugin/verify.ts +25 -0
  20. package/src/cli/commands/report/highlight.ts +1 -4
  21. package/src/cli/commands/report/renderer.ts +0 -2
  22. package/src/cli/commands/studio/access.ts +201 -0
  23. package/src/cli/commands/studio/index.ts +207 -26
  24. package/src/cli/commands/studio/openspec-cli.ts +1 -1
  25. package/src/cli/commands/studio/selection.ts +127 -11
  26. package/src/cli/commands/studio/server.ts +355 -64
  27. package/src/cli/commands/studio/terminal-assets.ts +50 -0
  28. package/src/cli/commands/studio/terminal-launch.ts +71 -0
  29. package/src/cli/commands/studio/terminal.ts +593 -0
  30. package/src/cli/commands/studio/vault.ts +320 -75
  31. package/src/cli/commands/studio/views/client.ts +90 -9
  32. package/src/cli/commands/studio/views/companion-picker.tsx +9 -5
  33. package/src/cli/commands/studio/views/companion-selector.tsx +11 -7
  34. package/src/cli/commands/studio/views/document.tsx +411 -107
  35. package/src/cli/commands/studio/views/model.ts +14 -1
  36. package/src/cli/commands/studio/views/selection-fields.tsx +14 -0
  37. package/src/cli/commands/studio/views/styles.ts +106 -13
  38. package/src/cli/commands/studio/views/terminal.tsx +426 -0
  39. package/src/cli/commands/studio/views/vault/state.ts +42 -0
  40. package/src/cli/commands/studio/views/workflow/steps.ts +10 -3
  41. package/src/cli/commands/update.ts +11 -8
  42. package/src/cli/companion-link-wizard.tsx +1 -1
  43. package/src/cli/companion-selector.tsx +1 -1
  44. package/src/cli/launch-selector.tsx +1 -1
  45. package/src/cli/main.ts +2 -1
  46. package/src/cli/usage.ts +7 -3
  47. package/src/cli/write-json-stdout.ts +2 -2
  48. package/src/lib/components/spinner.tsx +2 -2
  49. package/src/lib/components/startup-progress.tsx +1 -1
  50. package/src/lib/context-mode-package.ts +1 -1
  51. package/src/lib/opencode-plugin-package.ts +3 -0
  52. package/src/lib/orchestrator/adapters/opencode.ts +53 -13
  53. package/src/lib/orchestrator/companion-hub.ts +3 -3
  54. package/src/lib/orchestrator/companion-registry-store.ts +1 -1
  55. package/src/lib/orchestrator/config-store.ts +2 -2
  56. package/src/lib/orchestrator/editor.ts +4 -12
  57. package/src/lib/orchestrator/engine-guard.ts +9 -1
  58. package/src/lib/orchestrator/projection-entries.ts +1 -1
  59. package/src/lib/orchestrator/projection-record.ts +1 -1
  60. package/src/lib/orchestrator/projection-types.ts +0 -4
  61. package/src/lib/orchestrator/repo-local-registry.ts +0 -1
  62. package/src/lib/orchestrator/root-context.ts +1 -1
  63. package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
  64. package/src/lib/orchestrator/setup-preflight.ts +1 -1
  65. package/src/lib/orchestrator/types.ts +1 -1
  66. package/src/lib/orchestrator/working-repo-projection.ts +1 -1
  67. package/src/lib/orchestrator/workspace-inventory.ts +2 -2
  68. package/src/lib/package-paths.ts +1 -11
  69. package/src/lib/package-root.ts +43 -0
  70. package/src/lib/prebuilt-workspace.ts +1 -1
  71. package/src/lib/public-npm.ts +1 -1
  72. package/src/opencode/companion-hooks.ts +131 -153
  73. package/src/opencode/index.ts +12 -6
  74. package/src/opencode/tui.tsx +46 -49
  75. package/src/playbooks/companion-guidance.ts +1 -28
  76. package/src/runtime/companion-sync.ts +0 -3
  77. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
  78. package/src/templates/providers/opencode/opencode.json +6 -4
  79. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +500 -408
  80. package/src/tools/setup/capabilities/context-mode.ts +45 -25
  81. package/src/tools/setup/capabilities/graphify-shared.ts +3 -3
  82. package/src/tools/setup/capabilities/graphify.ts +2 -2
  83. package/src/tools/setup/capabilities/openspec.ts +2 -7
  84. package/src/tools/setup/capabilities/react-doctor.ts +1 -1
  85. package/src/tools/setup/capabilities/rtk.ts +2 -2
  86. package/src/tools/setup/capabilities/tokensave.ts +98 -12
  87. package/src/tools/setup/context-services.ts +3 -6
  88. package/src/tools/setup/dynamic-plugins/declarations.ts +47 -0
  89. package/src/tools/setup/dynamic-plugins/frozen.ts +215 -0
  90. package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -40
  91. package/src/tools/setup/dynamic-plugins/loader.ts +16 -0
  92. package/src/tools/setup/dynamic-plugins/policy.ts +55 -0
  93. package/src/tools/setup/dynamic-plugins/staging.ts +53 -0
  94. package/src/tools/setup/dynamic-plugins/verify.ts +70 -0
  95. package/src/tools/setup/mate.ts +1 -1
  96. package/src/tools/setup/providers/claude-format.ts +1 -1
  97. package/src/tools/setup/providers/claude.ts +1 -1
  98. package/src/tools/setup/providers/opencode-format.ts +220 -25
  99. package/src/tools/setup/providers/opencode.ts +63 -21
  100. package/src/tools/setup/runtime-documents.ts +9 -11
  101. package/src/tools/setup.ts +0 -4
  102. package/src/cli/commands/studio/views/pairings.tsx +0 -38
  103. package/src/templates/providers/opencode/tui.json +0 -3
@@ -1,7 +1,8 @@
1
+ import { spawn } from "node:child_process";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
 
4
- import type { Hooks, Plugin, PluginInput } from "@opencode-ai/plugin";
5
+ import type { Cleanup, Context } from "@opencode/plugin/promise/plugin";
5
6
 
6
7
  import {
7
8
  companionForkRefusal,
@@ -15,7 +16,6 @@ import {
15
16
  extractPatchPaths,
16
17
  isArtifactPath,
17
18
  normalizeTargetPath,
18
- readContext,
19
19
  shouldBlockArtifactWrite,
20
20
  type CompanionContext,
21
21
  } from "./companion-policy";
@@ -43,67 +43,80 @@ const REACT_DOCTOR_ARGS = [
43
43
  "30",
44
44
  ];
45
45
 
46
- async function runReactDoctorScan(
47
- context: CompanionContext,
48
- client: PluginInput["client"],
49
- $: PluginInput["$"],
50
- sessionID: string,
51
- scansInFlight: Set<string>,
52
- ): Promise<void> {
53
- const repo = context.repositoryPath;
54
- if (!repo || scansInFlight.has(sessionID)) return;
55
- scansInFlight.add(sessionID);
56
- try {
57
- const localBin = path.join(repo, "node_modules", ".bin", "react-doctor");
58
- const mateBin = process.env.MATE_REACT_DOCTOR_BIN_PATH;
59
- const doctorBin = mateBin && fs.existsSync(mateBin) ? mateBin : localBin;
60
-
61
- const result = fs.existsSync(doctorBin)
62
- ? await $`${doctorBin} ${REACT_DOCTOR_ARGS}`.cwd(repo).nothrow().quiet()
63
- : await $`npx --yes ${`react-doctor@${REACT_DOCTOR_VERSION}`} ${REACT_DOCTOR_ARGS}`
64
- .cwd(repo)
65
- .nothrow()
66
- .quiet();
67
-
68
- if (result.exitCode === 0) return;
46
+ export type CommandResult = { exitCode: number | null; output: string };
47
+ export type CommandRunner = (
48
+ command: string,
49
+ args: string[],
50
+ cwd: string,
51
+ ) => Promise<CommandResult>;
52
+ type SessionPrompt = Pick<Context["session"], "prompt">;
53
+
54
+ /** Combined stdout and stderr; rejects only when the process cannot be spawned. */
55
+ export const runCommand: CommandRunner = (command, args, cwd) =>
56
+ new Promise((resolve, reject) => {
57
+ const child = spawn(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
58
+ let output = "";
59
+ child.stdout.setEncoding("utf8").on("data", (chunk: string) => (output += chunk));
60
+ child.stderr.setEncoding("utf8").on("data", (chunk: string) => (output += chunk));
61
+ child.once("error", reject);
62
+ child.once("close", (exitCode) => resolve({ exitCode, output: output.trim() }));
63
+ });
64
+
65
+ function reactDoctorCommand(repo: string): { command: string; args: string[] } {
66
+ const localBin = path.join(repo, "node_modules", ".bin", "react-doctor");
67
+ const mateBin = process.env.MATE_REACT_DOCTOR_BIN_PATH;
68
+ const doctorBin = mateBin && fs.existsSync(mateBin) ? mateBin : localBin;
69
+ return fs.existsSync(doctorBin)
70
+ ? { command: doctorBin, args: REACT_DOCTOR_ARGS }
71
+ : {
72
+ command: "npx",
73
+ args: ["--yes", `react-doctor@${REACT_DOCTOR_VERSION}`, ...REACT_DOCTOR_ARGS],
74
+ };
75
+ }
69
76
 
70
- const output = `${result.stdout?.toString() ?? ""}${result.stderr?.toString() ?? ""}`.trim();
71
- if (!output || REACT_DOCTOR_NON_LINT_FAILURES.some((re) => re.test(output))) return;
77
+ /**
78
+ * Tracks edited sessions and scans each at most once per idle, never twice
79
+ * concurrently for the same session.
80
+ */
81
+ export function createReactDoctorScanner(
82
+ context: CompanionContext,
83
+ session: SessionPrompt,
84
+ run: CommandRunner = runCommand,
85
+ ) {
86
+ const dirtySessions = new Set<string>();
87
+ const scansInFlight = new Set<string>();
72
88
 
73
- await client.tui
74
- .showToast({
75
- query: { directory: repo },
76
- body: {
77
- title: "React Doctor",
78
- message: "Found issues in the changed files — review before finishing.",
79
- variant: "warning",
80
- },
81
- })
82
- .catch(() => {});
83
- await client.session
84
- .promptAsync({
85
- path: { id: sessionID },
86
- query: { directory: repo },
87
- body: {
88
- parts: [
89
- {
90
- type: "text",
91
- synthetic: true,
92
- text:
93
- "React Doctor found issues in the changed files. Review this output and fix " +
94
- "the regressions before finishing. For confirmed issues that cannot be fixed " +
95
- "now, create GitHub issues with the rule, file/line, confidence, impact, and " +
96
- `proposed fix.\n\n${output}`,
97
- },
98
- ],
99
- },
100
- })
101
- .catch(() => {});
102
- } catch {
103
- // Never break the session because a diagnostic scan failed.
104
- } finally {
105
- scansInFlight.delete(sessionID);
106
- }
89
+ return {
90
+ markEdited(tool: string, sessionID: string): void {
91
+ if (REACT_DOCTOR_EDIT_TOOLS.has(tool)) dirtySessions.add(sessionID);
92
+ },
93
+ async idle(sessionID: string): Promise<void> {
94
+ const repo = context.repositoryPath;
95
+ if (!repo || scansInFlight.has(sessionID) || !dirtySessions.delete(sessionID)) return;
96
+ scansInFlight.add(sessionID);
97
+ try {
98
+ const { command, args } = reactDoctorCommand(repo);
99
+ const { exitCode, output } = await run(command, args, repo);
100
+ if (exitCode === 0 || !output) return;
101
+ if (REACT_DOCTOR_NON_LINT_FAILURES.some((re) => re.test(output))) return;
102
+
103
+ await session
104
+ .prompt({
105
+ sessionID,
106
+ text:
107
+ "React Doctor found issues in the changed files. Review this output and fix " +
108
+ "the regressions before finishing. For confirmed issues that cannot be fixed " +
109
+ "now, create GitHub issues with the rule, file/line, confidence, impact, and " +
110
+ `proposed fix.\n\n${output}`,
111
+ } as never)
112
+ .catch(() => {});
113
+ } catch {
114
+ /** Never break the session because a diagnostic scan failed. */
115
+ } finally {
116
+ scansInFlight.delete(sessionID);
117
+ }
118
+ },
119
+ };
107
120
  }
108
121
 
109
122
  /**
@@ -120,7 +133,6 @@ const repairedCompanions = new Set<string>();
120
133
  */
121
134
  export async function repairCompanionGitOnce(
122
135
  context: CompanionContext,
123
- client: PluginInput["client"] | undefined,
124
136
  env: Record<string, string | undefined> = process.env,
125
137
  ): Promise<string[]> {
126
138
  if (
@@ -132,27 +144,12 @@ export async function repairCompanionGitOnce(
132
144
  if (repairedCompanions.has(context.companionPath)) return [];
133
145
  repairedCompanions.add(context.companionPath);
134
146
 
135
- const notes = unattendedSyncStalenessLines(syncCompanionUnattended(context.companionPath));
136
- if (notes.length === 0) return [];
137
-
138
147
  /**
139
148
  * Operator-facing only: a model told to run the command would run it
140
- * unattended. Delivery is best-effort in both directions — a client that
141
- * throws rather than rejecting must not cost the caller its notes.
149
+ * unattended. The V2 server API has no toast; the notes reach the operator
150
+ * through the persisted record and the TUI's staleness lines.
142
151
  */
143
- try {
144
- await client?.tui?.showToast({
145
- query: { directory: context.repositoryPath },
146
- body: {
147
- title: `${context.frameworkName} companion`,
148
- message: notes.join("\n"),
149
- variant: "warning",
150
- },
151
- });
152
- } catch {
153
- /** The note survives in the persisted record and the TUI's staleness lines. */
154
- }
155
- return notes;
152
+ return unattendedSyncStalenessLines(syncCompanionUnattended(context.companionPath));
156
153
  }
157
154
 
158
155
  /** Test seam: the once-per-session guard is process-wide by design. */
@@ -180,83 +177,64 @@ export function refuseForkedCompanionWrite(
180
177
  if (refusal) throw new Error(refusal);
181
178
  }
182
179
 
183
- type PluginEventInput = Parameters<NonNullable<Hooks["event"]>>[0];
184
- type ToolBeforeInput = Parameters<NonNullable<Hooks["tool.execute.before"]>>[0];
185
- type ToolBeforeOutput = Parameters<NonNullable<Hooks["tool.execute.before"]>>[1];
186
- type ToolAfterInput = Parameters<NonNullable<Hooks["tool.execute.after"]>>[0];
180
+ /** Throws when the tool call would write an artifact outside the companion. */
181
+ export function guardToolInput(context: CompanionContext, tool: string, input: unknown): void {
182
+ const args = (input ?? {}) as Record<string, unknown>;
183
+ const filePaths =
184
+ tool === "write" || tool === "edit"
185
+ ? [String(args.filePath ?? "")]
186
+ : tool === "apply_patch"
187
+ ? extractPatchPaths(String(args.patchText ?? ""))
188
+ : [];
189
+
190
+ for (const filePath of filePaths) {
191
+ if (context.repositoryPath && filePath && shouldBlockArtifactWrite(context, filePath)) {
192
+ throw new Error(buildArtifactError(context, filePath));
193
+ }
194
+ refuseForkedCompanionWrite(context, filePath);
195
+ }
196
+ }
187
197
 
188
- export const CompanionHooksPlugin: Plugin = async (pluginInput = {} as PluginInput) => {
189
- const { client, $ } = pluginInput;
190
- const context = readContext();
191
- if (!context.companionPath) return {};
198
+ export type CompanionHooksOptions = {
199
+ /** Test seam for the React Doctor process. */
200
+ run?: CommandRunner;
201
+ };
192
202
 
203
+ /**
204
+ * Registers the companion guardrails on an OpenCode V2 plugin context.
205
+ * The caller resolves `context` and skips this when no companion resolved.
206
+ */
207
+ export async function registerCompanionHooks(
208
+ api: Context,
209
+ context: CompanionContext,
210
+ options: CompanionHooksOptions = {},
211
+ ): Promise<Cleanup | undefined> {
193
212
  /**
194
- * The repair sits above the returned hooks, so anything escaping it would
195
- * cost the session every guardrail below — the artifact guard, the React
196
- * Doctor scan — to save a synchronization that is optional by design.
213
+ * Anything escaping the repair would cost the session every guardrail
214
+ * below to save a synchronization that is optional by design.
197
215
  */
198
- await repairCompanionGitOnce(context, client).catch(() => []);
216
+ await repairCompanionGitOnce(context).catch(() => []);
199
217
 
200
- const dirtyReactDoctorSessions = new Set<string>();
201
- const reactDoctorScansInFlight = new Set<string>();
218
+ await api.tool.hook("execute.before", (event) => {
219
+ guardToolInput(context, event.tool, event.input);
220
+ });
202
221
 
203
- return {
204
- ...(context.repositoryPath
205
- ? {
206
- event: async ({ event }: PluginEventInput) => {
207
- if (event.type !== "session.idle") return;
208
- const sessionID = event.properties.sessionID;
209
- if (
210
- !context.reactDoctorEnabled ||
211
- reactDoctorScansInFlight.has(sessionID) ||
212
- !dirtyReactDoctorSessions.delete(sessionID)
213
- ) {
214
- return;
215
- }
216
- await runReactDoctorScan(context, client, $, sessionID, reactDoctorScansInFlight);
217
- },
218
- }
219
- : {}),
220
- "tool.execute.before": async (input: ToolBeforeInput, output: ToolBeforeOutput) => {
221
- const toolName = String(input.tool ?? "");
222
- const args = output.args ?? {};
223
- if (context.repositoryPath && ["write", "edit"].includes(toolName)) {
224
- const filePath = String(args.filePath ?? "");
225
- if (filePath && shouldBlockArtifactWrite(context, filePath)) {
226
- throw new Error(buildArtifactError(context, filePath));
227
- }
228
- refuseForkedCompanionWrite(context, filePath);
229
- }
230
- if (context.repositoryPath && toolName === "apply_patch") {
231
- for (const filePath of extractPatchPaths(String(args.patchText ?? ""))) {
232
- if (shouldBlockArtifactWrite(context, filePath)) {
233
- throw new Error(buildArtifactError(context, filePath));
234
- }
235
- refuseForkedCompanionWrite(context, filePath);
236
- }
237
- }
238
- if (!context.repositoryPath && toolName === "write") {
239
- refuseForkedCompanionWrite(context, String(args.filePath ?? ""));
240
- }
241
- if (!context.repositoryPath && toolName === "edit") {
242
- refuseForkedCompanionWrite(context, String(args.filePath ?? ""));
243
- }
244
- if (!context.repositoryPath && toolName === "apply_patch") {
245
- for (const filePath of extractPatchPaths(String(args.patchText ?? ""))) {
246
- refuseForkedCompanionWrite(context, filePath);
247
- }
248
- }
249
- },
250
- ...(context.repositoryPath
251
- ? {
252
- "tool.execute.after": async (input: ToolAfterInput) => {
253
- if (context.reactDoctorEnabled && REACT_DOCTOR_EDIT_TOOLS.has(input.tool)) {
254
- dirtyReactDoctorSessions.add(input.sessionID);
255
- }
256
- },
257
- }
258
- : {}),
259
- };
260
- };
222
+ if (!context.repositoryPath || !context.reactDoctorEnabled) return;
223
+
224
+ const scanner = createReactDoctorScanner(context, api.session, options.run);
225
+ await api.tool.hook("execute.after", (event) => {
226
+ if (event.status === "completed") scanner.markEdited(event.tool, event.sessionID);
227
+ });
261
228
 
262
- export default CompanionHooksPlugin;
229
+ const controller = new AbortController();
230
+ void (async () => {
231
+ try {
232
+ for await (const event of api.event.subscribe({ signal: controller.signal })) {
233
+ if (event.type === "session.idle") await scanner.idle(event.data.sessionID);
234
+ }
235
+ } catch {
236
+ /** Plugin shutdown aborts the subscription; event delivery is best effort. */
237
+ }
238
+ })();
239
+ return () => controller.abort();
240
+ }
@@ -1,10 +1,16 @@
1
1
  /**
2
- * Building blocks for authoring OpenCode plugins: the Mate TUI plugin, the
3
- * companion guidance hooks, and the companion policy helpers. Session-runtime
4
- * only — modules here must not import framework internals (see the
5
- * import-isolation test).
2
+ * Server-side building blocks for authoring OpenCode plugins: the companion
3
+ * guidance hooks and the companion policy helpers. Session-runtime only —
4
+ * modules here must not import framework internals (see the import-isolation
5
+ * test). Must not reach OpenTUI or Solid: the Mate TUI plugin lives on the
6
+ * `./opencode/tui` subpath.
6
7
  */
7
- export { default as tuiPlugin } from "./tui";
8
- export { CompanionHooksPlugin } from "./companion-hooks";
8
+ export {
9
+ createReactDoctorScanner,
10
+ guardToolInput,
11
+ registerCompanionHooks,
12
+ type CommandRunner,
13
+ type CompanionHooksOptions,
14
+ } from "./companion-hooks";
9
15
  export { resolveOpenCodeGuidance, type OpenCodeGuidanceResolution } from "./projected-guidance";
10
16
  export * from "./companion-policy";
@@ -1,82 +1,79 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  /* oxlint-disable react/no-unknown-property, react/react-in-jsx-scope, react/jsx-key, react-doctor/jsx-key */
3
3
  /**
4
+ * OpenCode V2 TUI plugin module (`{ id, setup }`), OpenCode 2.x only.
4
5
  * Solid JSX, not React: `key` is not a reconciliation prop here and `TextProps`
5
6
  * does not accept it, so the jsx-key rules cannot be satisfied — only suppressed.
6
7
  * Static arrays render through `.map`; a reactive one would need `<For>`.
7
8
  */
8
- import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui";
9
-
10
9
  import { mateVersion } from "../runtime/install";
11
10
  import { readContext } from "./companion-policy";
12
11
 
12
+ /** Minimal structural slice of the OpenCode V2 TUI API; the published SDK ships no V2 TUI types. */
13
+ export type MateTuiApi = {
14
+ /** Host getter: returns the current theme tokens on every read. */
15
+ readonly theme: {
16
+ text: {
17
+ muted: string;
18
+ feedback?: { warning?: { base: string } };
19
+ };
20
+ };
21
+ ui: {
22
+ slot(claim: MateSlotClaim): () => void;
23
+ };
24
+ };
25
+
26
+ export type MateSlotClaim = {
27
+ prepend?: "home.footer";
28
+ append?: "sidebar.content";
29
+ render: () => unknown;
30
+ };
31
+
32
+ export type MateTuiPluginModule = {
33
+ id: string;
34
+ setup(api: MateTuiApi): void | Promise<void>;
35
+ };
36
+
13
37
  const MIDNIGHT_PURPLE_BRIGHT = "#c084fc";
14
- const NARROW_TERMINAL_WIDTH = 80;
15
38
  const MATE_VERSION = process.env.MATE_VERSION ?? mateVersion();
16
39
 
17
- function SessionContext({
18
- api,
19
- compact = false,
20
- sidebar = false,
21
- }: {
22
- api: TuiPluginApi;
23
- compact?: boolean;
24
- sidebar?: boolean;
25
- }) {
40
+ function SessionContext({ api, sidebar = false }: { api: MateTuiApi; sidebar?: boolean }) {
26
41
  const context = readContext();
27
- const theme = api.theme.current;
28
-
29
- if (compact) {
30
- return (
31
- <box width="100%" paddingTop={0} flexShrink={0}>
32
- <text fg={theme.textMuted}>
33
- mate v{MATE_VERSION} | repo: {context.repositoryPath} | mate: {context.companionPath}
34
- </text>
35
- </box>
36
- );
37
- }
42
+ const text = api.theme.text;
43
+ const warning = text.feedback?.warning?.base ?? text.muted;
38
44
 
39
45
  return (
40
- <box width="100%" maxWidth={75} paddingTop={sidebar ? 0 : 2} paddingBottom={1} flexShrink={0}>
46
+ <box
47
+ width="100%"
48
+ maxWidth={sidebar ? undefined : 75}
49
+ paddingLeft={sidebar ? 0 : 2}
50
+ paddingRight={sidebar ? 0 : 2}
51
+ paddingBottom={sidebar ? 1 : 0}
52
+ flexShrink={0}
53
+ >
41
54
  <text fg={MIDNIGHT_PURPLE_BRIGHT}>mate v{MATE_VERSION}</text>
42
- <text fg={theme.textMuted}>repo: {context.repositoryPath}</text>
43
- <text fg={theme.textMuted}>mate: {context.companionPath}</text>
55
+ <text fg={text.muted}>repo: {context.repositoryPath}</text>
56
+ <text fg={text.muted}>mate: {context.companionPath}</text>
44
57
  {context.stalenessLines.map((note) => (
45
- <text fg={theme.warning ?? theme.textMuted}>{note}</text>
58
+ <text fg={warning}>{note}</text>
46
59
  ))}
47
60
  </box>
48
61
  );
49
62
  }
50
63
 
51
- const tui: TuiPlugin = async (api) => {
64
+ function setup(api: MateTuiApi): void {
52
65
  const context = readContext();
53
66
  if (!context.companionPath || !context.repositoryPath) {
54
67
  return;
55
68
  }
56
69
 
57
- api.slots.register({
58
- order: 0,
59
- slots: {
60
- home_bottom() {
61
- return <SessionContext api={api} />;
62
- },
63
- sidebar_content() {
64
- return <SessionContext api={api} sidebar />;
65
- },
66
- app_bottom() {
67
- if (api.renderer.width >= NARROW_TERMINAL_WIDTH || api.route.current.name !== "session") {
68
- return null;
69
- }
70
-
71
- return <SessionContext api={api} compact />;
72
- },
73
- },
74
- });
75
- };
70
+ api.ui.slot({ prepend: "home.footer", render: () => <SessionContext api={api} /> });
71
+ api.ui.slot({ append: "sidebar.content", render: () => <SessionContext api={api} sidebar /> });
72
+ }
76
73
 
77
- const plugin: TuiPluginModule & { id: string } = {
74
+ const plugin: MateTuiPluginModule = {
78
75
  id: "mate-companion-tui",
79
- tui,
76
+ setup,
80
77
  };
81
78
 
82
79
  export default plugin;
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  buildCompanionGuidance as buildCompanionGuidanceWith,
3
- buildCompanionPolicyXml as buildCompanionPolicyXmlWith,
4
3
  type GuidanceContext,
5
4
  } from "../runtime/companion-guidance";
6
5
  import { getWrapperBinPath } from "../lib/package-paths";
@@ -16,33 +15,7 @@ import { getWrapperBinPath } from "../lib/package-paths";
16
15
  * passes the projected path instead, and so calls the runtime builders direct.
17
16
  */
18
17
 
19
- export {
20
- GRAPHIFY_SHARED_COMPANION_PATH_CONTRACT,
21
- GRAPHIFY_COMPANION_PATH_CONTRACT,
22
- buildCodebaseExplorationGuidanceSection,
23
- hasGraphifyCapability,
24
- hasOpenspecCapability,
25
- hasTokensaveCapability,
26
- type GuidanceCapability,
27
- type GuidanceContext,
28
- } from "../runtime/companion-guidance";
29
-
30
- /**
31
- * Build just the `<companion-policy>` XML block: paths, CLI tools, and
32
- * mandatory rules (including the capability-gated `openspec-publish` rule).
33
- * Does not include codebase-exploration guidance — see
34
- * `buildCompanionGuidance` for the merged single-string form, or call
35
- * `buildCodebaseExplorationGuidanceSection` directly when that guidance is
36
- * delivered through its own channel (as OpenCode's guidance contract does).
37
- */
38
- export function buildCompanionPolicyXml(
39
- context: GuidanceContext,
40
- options: { wrapperBinPath?: string } = {},
41
- ): string {
42
- return buildCompanionPolicyXmlWith(context, {
43
- wrapperBinPath: options.wrapperBinPath ?? getWrapperBinPath(),
44
- });
45
- }
18
+ export { GRAPHIFY_SHARED_COMPANION_PATH_CONTRACT } from "../runtime/companion-guidance";
46
19
 
47
20
  /**
48
21
  * Build the merged single-string guidance: the companion-policy XML plus
@@ -12,7 +12,6 @@ import fs from "node:fs";
12
12
  import path from "node:path";
13
13
 
14
14
  import {
15
- companionGitStatePath,
16
15
  COMPANION_SYNC_TTL_MS,
17
16
  FORK_VERDICT_TTL_MS,
18
17
  isCompanionSyncDue,
@@ -294,5 +293,3 @@ export function forkRefusalMessage(companionPath: string, fork: CompanionForkSta
294
293
  `Writing artifacts now risks losing work in the reconciliation. Run \`${COMPANION_SYNC_COMMAND}\` first.`,
295
294
  ].join("\n");
296
295
  }
297
-
298
- export { companionGitStatePath };
@@ -39,7 +39,8 @@ artifacts:
39
39
  template: tasks.md
40
40
  instruction: |
41
41
  Create a flat implementation checklist that satisfies the delta specs. Keep it short: this
42
- workflow intentionally has no explore, proposal, or design artifact.
42
+ profile uses the shared `/openspec-explore` and `/openspec-propose` planning steps but
43
+ intentionally writes no explore, proposal, or design artifact.
43
44
 
44
45
  Number tasks `N.M` and group them under `## N. <group>` headers so apply tracking can address them individually.
45
46
  Read `openspec/mate-conventions.yaml` before writing the artifact and preserve its scope rules.
@@ -2,12 +2,14 @@
2
2
  "$schema": "https://opencode.ai/config.json",
3
3
  "compaction": {
4
4
  "auto": true,
5
- "prune": true,
6
- "reserved": 15000
5
+ "buffer": 20000,
6
+ "keep": {
7
+ "tokens": 15000
8
+ }
7
9
  },
8
10
  "tool_output": {
9
- "max_lines": 500,
10
- "max_bytes": 20000
11
+ "max_lines": 1000,
12
+ "max_bytes": 40000
11
13
  },
12
14
  "references": {
13
15
  "mate": ".."