@uniqbit/mate-core 0.18.3-canary.1 → 0.18.4
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/add-dir.ts +10 -76
- package/opencode-plugin/src/audience-layer.ts +91 -0
- package/opencode-plugin/src/companion.ts +70 -18
- package/opencode-plugin/src/launch-environment.ts +29 -0
- package/opencode-plugin/src/server.ts +60 -8
- 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/cap/index-cmd.ts +2 -11
- 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 +87 -5
- package/src/cli/commands/install.ts +8 -0
- 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/studio/terminal-launch.ts +16 -7
- package/src/cli/commands/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +79 -60
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +5 -2
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-guidance.ts +20 -2
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +45 -9
- package/src/lib/orchestrator/adapters/claude.ts +241 -30
- package/src/lib/orchestrator/adapters/opencode.ts +72 -34
- 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 +83 -1
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/openspec-store.ts +202 -0
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +126 -41
- package/src/lib/orchestrator/projection-entries.ts +124 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
- package/src/lib/orchestrator/projection-openspec-store.ts +164 -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 +47 -34
- 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 +503 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/working-repo-projection.ts +73 -160
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +222 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +30 -135
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- package/src/opencode/projected-guidance.ts +6 -13
- 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/audience-layer.ts +84 -0
- 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/guidance.ts +17 -0
- package/src/runtime/opencode-audience-layer.ts +176 -0
- package/src/runtime/policy.ts +14 -1
- package/src/runtime/projection.ts +1 -1
- package/src/runtime/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +352 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +94 -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/audience-isolation.fixture.ts +325 -0
- package/src/tools/setup/audience-layer.ts +207 -0
- package/src/tools/setup/audience-registrations.ts +52 -0
- package/src/tools/setup/capabilities/openspec.ts +9 -0
- package/src/tools/setup/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/capabilities/tokensave.ts +11 -0
- package/src/tools/setup/contribution-ledger.ts +207 -0
- package/src/tools/setup/dynamic-plugins/audiences.ts +89 -9
- package/src/tools/setup/dynamic-plugins/declarations.ts +3 -2
- package/src/tools/setup/dynamic-plugins/hydrate.ts +8 -5
- package/src/tools/setup/engine.ts +152 -10
- package/src/tools/setup/isolation-diagnostics.ts +139 -0
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/plugin.ts +19 -4
- package/src/tools/setup/plugins/gitignore.ts +7 -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 +50 -8
- package/src/tools/setup/providers/opencode.ts +10 -0
- package/src/tools/setup/registry.ts +24 -2
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/surface-target.ts +13 -4
- package/src/tools/setup/upgrade-sweep.ts +153 -0
- package/src/tools/setup/working-repo-cleanup.ts +2 -0
- package/src/tools/setup/working-repo-local-state.ts +91 -14
- package/src/tools/setup.ts +11 -8
- package/wrappers/bin/openspec +42 -1
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Mate hooks
|
|
2
|
+
"description": "Mate hooks \u2014 PreToolUse artifact-path guardrail, SessionStart banner, companion guidance and session prepare",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -28,6 +28,15 @@
|
|
|
28
28
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-guidance.mjs\""
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-prepare.mjs\"",
|
|
37
|
+
"timeout": 45
|
|
38
|
+
}
|
|
39
|
+
]
|
|
31
40
|
}
|
|
32
41
|
]
|
|
33
42
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Thin shim for src/hooks/session-banner.ts, loaded through ts-loader.mjs: the installed
|
|
4
|
+
* plugin sits inside node_modules, where node's native type stripping is off.
|
|
5
|
+
*/
|
|
5
6
|
import "./ts-loader.mjs";
|
|
6
7
|
|
|
7
8
|
const { run } = await import("../../src/hooks/session-banner.ts");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Thin shim for src/hooks/session-prepare.ts, loaded through ts-loader.mjs: the installed
|
|
4
|
+
* plugin sits inside node_modules, where node's native type stripping is off.
|
|
5
|
+
*/
|
|
6
|
+
import "./ts-loader.mjs";
|
|
7
|
+
|
|
8
|
+
const { run } = await import("../../src/hooks/session-prepare.ts");
|
|
9
|
+
process.exitCode = await run();
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
4
3
|
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
description?: string;
|
|
10
|
-
path: string;
|
|
11
|
-
content: string;
|
|
12
|
-
};
|
|
5
|
+
import { loadSkillInfos } from "../../src/runtime/opencode-audience-layer";
|
|
13
6
|
|
|
14
7
|
function companionSkillPaths(companionPath: string): string[] {
|
|
15
8
|
return [
|
|
@@ -23,87 +16,28 @@ function isWithin(root: string, candidate: string): boolean {
|
|
|
23
16
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
24
17
|
}
|
|
25
18
|
|
|
26
|
-
function parseFrontmatter(content: string): { name?: string; description?: string } {
|
|
27
|
-
const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
28
|
-
if (!match) return {};
|
|
29
|
-
|
|
30
|
-
const scalar = (key: string) => {
|
|
31
|
-
const value = match[1].match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
32
|
-
return value?.replace(
|
|
33
|
-
/^(?:"([\s\S]*)"|'([\s\S]*)')$/,
|
|
34
|
-
(_all, doubleQuoted, singleQuoted) => doubleQuoted ?? singleQuoted,
|
|
35
|
-
);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
return { name: scalar("name"), description: scalar("description") };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function findSkillFiles(directory: string): Promise<string[]> {
|
|
42
|
-
try {
|
|
43
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
44
|
-
const files: string[] = [];
|
|
45
|
-
const subdirectories: Promise<string[]>[] = [];
|
|
46
|
-
for (const entry of entries) {
|
|
47
|
-
const entryPath = path.join(directory, entry.name);
|
|
48
|
-
if (entry.isFile() && entry.name === "SKILL.md") files.push(entryPath);
|
|
49
|
-
else if (entry.isDirectory()) subdirectories.push(findSkillFiles(entryPath));
|
|
50
|
-
}
|
|
51
|
-
const nested = await Promise.all(subdirectories);
|
|
52
|
-
return [...files, ...nested.flat()];
|
|
53
|
-
} catch {
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function skillID(name: string): string {
|
|
59
|
-
return (
|
|
60
|
-
name
|
|
61
|
-
.toLowerCase()
|
|
62
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
63
|
-
.replace(/^-+|-+$/g, "") || "mate-skill"
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function loadCompanionSkills(companionPath: string): Promise<CompanionSkill[]> {
|
|
68
|
-
const files = (await Promise.all(companionSkillPaths(companionPath).map(findSkillFiles))).flat();
|
|
69
|
-
const skills = await Promise.all(
|
|
70
|
-
files.map(async (filePath): Promise<CompanionSkill | null> => {
|
|
71
|
-
try {
|
|
72
|
-
const content = await fs.readFile(filePath, "utf8");
|
|
73
|
-
const metadata = parseFrontmatter(content);
|
|
74
|
-
const name = metadata.name || path.basename(path.dirname(filePath));
|
|
75
|
-
return {
|
|
76
|
-
id: skillID(name),
|
|
77
|
-
name,
|
|
78
|
-
...(metadata.description ? { description: metadata.description } : {}),
|
|
79
|
-
path: filePath,
|
|
80
|
-
content,
|
|
81
|
-
};
|
|
82
|
-
} catch {
|
|
83
|
-
/** An unreadable companion skill must not prevent the session from loading. */
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}),
|
|
87
|
-
);
|
|
88
|
-
return skills.filter((skill) => skill !== null);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
19
|
/**
|
|
92
20
|
* Grants the session access to the companion and registers its skills. The
|
|
93
21
|
* permission hook only widens access for companion resources; every other
|
|
94
22
|
* evaluation keeps OpenCode's own verdict.
|
|
95
23
|
*/
|
|
96
|
-
export async function registerCompanionAccess(
|
|
24
|
+
export async function registerCompanionAccess(
|
|
25
|
+
api: Context,
|
|
26
|
+
companionPath: string,
|
|
27
|
+
extraRoots: readonly string[] = [],
|
|
28
|
+
): Promise<void> {
|
|
97
29
|
const permissionRoot = path.resolve(companionPath);
|
|
30
|
+
/** An audience layer's files are as readable as the companion's, for this launch only. */
|
|
31
|
+
const allowedRoots = [permissionRoot, ...extraRoots.map((root) => path.resolve(root))];
|
|
98
32
|
|
|
99
33
|
await api.permission.hook("evaluate", (event) => {
|
|
100
|
-
if (event.resources.some((resource) => isWithin(
|
|
34
|
+
if (event.resources.some((resource) => allowedRoots.some((root) => isWithin(root, resource)))) {
|
|
101
35
|
event.effect = "allow";
|
|
102
36
|
delete event.message;
|
|
103
37
|
}
|
|
104
38
|
});
|
|
105
39
|
|
|
106
|
-
const skills = await
|
|
40
|
+
const skills = await loadSkillInfos(companionSkillPaths(permissionRoot));
|
|
107
41
|
if (skills.length === 0) return;
|
|
108
42
|
|
|
109
43
|
await api.skill.transform((editor) => {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
2
|
+
|
|
3
|
+
import { repositoryWrappedForOpenCode } from "../../src/opencode/companion-hooks";
|
|
4
|
+
import { currentAudienceLayer } from "../../src/runtime/audience-layer";
|
|
5
|
+
import { MATE_ENV } from "../../src/runtime/env";
|
|
6
|
+
import { parseGuidanceContent } from "../../src/runtime/guidance";
|
|
7
|
+
import {
|
|
8
|
+
readOpenCodeAudienceLayer,
|
|
9
|
+
type OpenCodeLayerContents,
|
|
10
|
+
} from "../../src/runtime/opencode-audience-layer";
|
|
11
|
+
import { readWrapStateSync } from "../../src/runtime/wrap-state";
|
|
12
|
+
import type { LaunchEnvironment } from "./launch-environment";
|
|
13
|
+
|
|
14
|
+
export interface TrustedAudienceLayer {
|
|
15
|
+
root: string;
|
|
16
|
+
contents: OpenCodeLayerContents;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The audience layer named by a payload the plugin trusts. `env` is the
|
|
21
|
+
* launch environment `pluginLaunchEnvironment` decided on, so a masked
|
|
22
|
+
* payload, a bare session and the shared service all read as `{}` here and
|
|
23
|
+
* never apply a layer.
|
|
24
|
+
*/
|
|
25
|
+
export async function readTrustedAudienceLayer(
|
|
26
|
+
env: LaunchEnvironment,
|
|
27
|
+
): Promise<TrustedAudienceLayer | null> {
|
|
28
|
+
const payload = env[MATE_ENV.guidanceJson];
|
|
29
|
+
if (!payload?.trim()) return null;
|
|
30
|
+
const root = parseGuidanceContent(payload).guidance.audienceLayer;
|
|
31
|
+
if (!root) return null;
|
|
32
|
+
return { root, contents: await readOpenCodeAudienceLayer(root) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The layer of the audience the session's checkout is wrapped for. The wrap is
|
|
37
|
+
* resolved from the session's own directory, so the shared service serves each
|
|
38
|
+
* checkout its own audience; the environment never selects one.
|
|
39
|
+
*/
|
|
40
|
+
export async function readWrappedAudienceLayer(
|
|
41
|
+
directory: string,
|
|
42
|
+
companionPath: string,
|
|
43
|
+
): Promise<TrustedAudienceLayer | null> {
|
|
44
|
+
const repoRoot = repositoryWrappedForOpenCode(directory);
|
|
45
|
+
if (!repoRoot) return null;
|
|
46
|
+
const audience = readWrapStateSync(repoRoot).audience;
|
|
47
|
+
if (!audience) return null;
|
|
48
|
+
const root = currentAudienceLayer(companionPath, audience);
|
|
49
|
+
return root ? { root, contents: await readOpenCodeAudienceLayer(root) } : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Applies a layer's MCP servers, skills and agents through the plugin API. A
|
|
54
|
+
* new agent id is created by updating it; its prompt goes in `system`.
|
|
55
|
+
*/
|
|
56
|
+
export async function registerAudienceLayer(
|
|
57
|
+
api: Context,
|
|
58
|
+
layer: OpenCodeLayerContents,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
if (layer.mcpServers.length > 0) {
|
|
61
|
+
await api.mcp.transform((editor) => {
|
|
62
|
+
for (const server of layer.mcpServers) editor.set(server.name, server.config as never);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (layer.skills.length > 0) {
|
|
67
|
+
await api.skill.transform((editor) => {
|
|
68
|
+
for (const skill of layer.skills) {
|
|
69
|
+
if (editor.get(skill.id)) {
|
|
70
|
+
editor.update(skill.id, (current) => Object.assign(current, skill));
|
|
71
|
+
} else {
|
|
72
|
+
editor.add(skill as never);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (layer.agents.length > 0) {
|
|
79
|
+
await api.agent.transform((editor) => {
|
|
80
|
+
for (const agent of layer.agents) {
|
|
81
|
+
editor.update(agent.id, (current) => {
|
|
82
|
+
const target = current as Record<string, unknown>;
|
|
83
|
+
target.name ??= agent.id;
|
|
84
|
+
if (agent.description) target.description = agent.description;
|
|
85
|
+
target.mode = agent.mode;
|
|
86
|
+
target.system = agent.system;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
@@ -5,15 +6,38 @@ import { MATE_ENV } from "../../src/runtime/env";
|
|
|
5
6
|
import { COMPANION_POLICY_MARKER, type MateGuidanceFile } from "../../src/runtime/guidance";
|
|
6
7
|
|
|
7
8
|
import type { CompanionContext } from "../../src/opencode/companion-policy";
|
|
9
|
+
import type { LaunchEnvironment } from "./launch-environment";
|
|
8
10
|
import { resolveOpenCodeGuidance } from "../../src/opencode/projected-guidance";
|
|
11
|
+
import {
|
|
12
|
+
runtimeDirectoryPath,
|
|
13
|
+
runtimeMateBin,
|
|
14
|
+
runtimeWrapperBinPath,
|
|
15
|
+
} from "../../src/runtime/runtime-directory";
|
|
9
16
|
|
|
10
17
|
function prependPathEntry(pathValue: string | undefined, entry: string): string {
|
|
11
18
|
const entries = (pathValue ?? "").split(path.delimiter).filter(Boolean);
|
|
12
19
|
return [entry, ...entries.filter((value) => value !== entry)].join(path.delimiter);
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The launch environment first, then the runtime directory every `mate` run
|
|
24
|
+
* keeps current. `null` when neither exists: guidance naming an unresolved
|
|
25
|
+
* placeholder would send the model to a path that is not there.
|
|
26
|
+
*/
|
|
27
|
+
function resolveWrapperBinPath(env: LaunchEnvironment): string | null {
|
|
28
|
+
if (env.MATE_WRAPPER_BIN_PATH) return env.MATE_WRAPPER_BIN_PATH;
|
|
29
|
+
const runtime = runtimeWrapperBinPath();
|
|
30
|
+
return fs.existsSync(runtime) ? runtime : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The runtime-directory launcher for the Mate CLI; `null` until a `mate` run wrote it. */
|
|
34
|
+
export function resolveMateBin(): string | null {
|
|
35
|
+
const launcher = runtimeMateBin();
|
|
36
|
+
return fs.existsSync(launcher) ? launcher : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function missingRuntimeDirectoryMessage(): string {
|
|
40
|
+
return `Mate runtime directory ${runtimeDirectoryPath()} is missing; run \`mate\` once to repair it.`;
|
|
17
41
|
}
|
|
18
42
|
|
|
19
43
|
function buildStartupError(details: string[]): Error {
|
|
@@ -31,14 +55,17 @@ function buildStartupError(details: string[]): Error {
|
|
|
31
55
|
* an Unmanaged Session in a wrapped repository builds the same payload from the
|
|
32
56
|
* companion the projection names, with the Capability names read live.
|
|
33
57
|
*/
|
|
34
|
-
function loadGuidance(directory: string): MateGuidanceFile | null {
|
|
35
|
-
const { guidance, errors } = resolveOpenCodeGuidance(
|
|
58
|
+
function loadGuidance(env: LaunchEnvironment, directory: string): MateGuidanceFile | null {
|
|
59
|
+
const { guidance, errors } = resolveOpenCodeGuidance(env, directory);
|
|
36
60
|
if (errors.length > 0) throw buildStartupError(errors);
|
|
37
61
|
return guidance;
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
function materializeCompanionGuidance(
|
|
41
|
-
|
|
64
|
+
function materializeCompanionGuidance(
|
|
65
|
+
guidance: string,
|
|
66
|
+
context: CompanionContext,
|
|
67
|
+
wrapperBinPath: string,
|
|
68
|
+
): string {
|
|
42
69
|
return guidance
|
|
43
70
|
.replaceAll("$MATE_REPO_PATH", context.repositoryPath)
|
|
44
71
|
.replaceAll("$MATE_ARTIFACT_PATH", context.companionPath)
|
|
@@ -46,8 +73,13 @@ function materializeCompanionGuidance(guidance: string, context: CompanionContex
|
|
|
46
73
|
.replaceAll("$MATE_REPO_ID", context.repositoryId);
|
|
47
74
|
}
|
|
48
75
|
|
|
49
|
-
function buildSystemPrompt(
|
|
50
|
-
|
|
76
|
+
function buildSystemPrompt(
|
|
77
|
+
context: CompanionContext,
|
|
78
|
+
guidance: MateGuidanceFile,
|
|
79
|
+
wrapperBinPath: string,
|
|
80
|
+
layerGuidance: string,
|
|
81
|
+
): string[] {
|
|
82
|
+
const lines = [materializeCompanionGuidance(guidance.companionGuidance, context, wrapperBinPath)];
|
|
51
83
|
|
|
52
84
|
if (guidance.codebaseExplorationGuidance.trim()) {
|
|
53
85
|
lines.push("", guidance.codebaseExplorationGuidance);
|
|
@@ -57,6 +89,11 @@ function buildSystemPrompt(context: CompanionContext, guidance: MateGuidanceFile
|
|
|
57
89
|
lines.push("", `<agents.md>${context.agentsMd.trim()}</agents.md>`);
|
|
58
90
|
}
|
|
59
91
|
|
|
92
|
+
/** The audience's guidance follows the companion's, never replaces it. */
|
|
93
|
+
if (layerGuidance.trim()) {
|
|
94
|
+
lines.push("", layerGuidance.trim());
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
return lines;
|
|
61
98
|
}
|
|
62
99
|
|
|
@@ -70,10 +107,11 @@ function readSystemText(part: unknown): string {
|
|
|
70
107
|
function applyCompanionShellEnv(
|
|
71
108
|
context: CompanionContext,
|
|
72
109
|
env: Record<string, string | undefined>,
|
|
110
|
+
wrapperBinPath: string,
|
|
111
|
+
launchEnv: LaunchEnvironment,
|
|
73
112
|
): void {
|
|
74
|
-
const wrapperBinPath = resolveWrapperBinPath();
|
|
75
113
|
env.MATE_NAME = context.frameworkName;
|
|
76
|
-
env.MATE_VERSION =
|
|
114
|
+
env.MATE_VERSION = launchEnv.MATE_VERSION ?? "unknown";
|
|
77
115
|
env.MATE_ARTIFACT_PATH = context.companionPath;
|
|
78
116
|
env.MATE_WRAPPER_BIN_PATH = wrapperBinPath;
|
|
79
117
|
if (context.repositoryPath) {
|
|
@@ -95,8 +133,7 @@ function applyCompanionShellEnv(
|
|
|
95
133
|
env.PATH = prependPathEntry(process.env.PATH, wrapperBinPath);
|
|
96
134
|
}
|
|
97
135
|
|
|
98
|
-
function companionPathsResult(context: CompanionContext) {
|
|
99
|
-
const wrapperBinPath = resolveWrapperBinPath();
|
|
136
|
+
function companionPathsResult(context: CompanionContext, wrapperBinPath: string) {
|
|
100
137
|
return {
|
|
101
138
|
content: JSON.stringify(
|
|
102
139
|
{
|
|
@@ -142,12 +179,27 @@ function mergeCompanionSystem(system: unknown[], companion: string): string[] {
|
|
|
142
179
|
return parts.length > 0 ? [parts.join("\n\n")] : [];
|
|
143
180
|
}
|
|
144
181
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Registers guidance, compaction context, shell env, and `companion_paths`.
|
|
184
|
+
* `env` is the launch environment the caller decided to trust; `{}` resolves
|
|
185
|
+
* everything from the Projection Root.
|
|
186
|
+
*/
|
|
187
|
+
export async function registerCompanion(
|
|
188
|
+
api: Context,
|
|
189
|
+
context: CompanionContext,
|
|
190
|
+
env: LaunchEnvironment = process.env,
|
|
191
|
+
layerGuidance = "",
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
const guidance = loadGuidance(env, api.location.directory);
|
|
148
194
|
if (!guidance) return;
|
|
195
|
+
/** Reported, not thrown: the guardrails registered before this must stay live. */
|
|
196
|
+
const wrapperBinPath = resolveWrapperBinPath(env);
|
|
197
|
+
if (!wrapperBinPath) {
|
|
198
|
+
console.error(`mate: ${missingRuntimeDirectoryMessage()}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
149
201
|
|
|
150
|
-
const companion = buildSystemPrompt(context, guidance).join("\n");
|
|
202
|
+
const companion = buildSystemPrompt(context, guidance, wrapperBinPath, layerGuidance).join("\n");
|
|
151
203
|
|
|
152
204
|
await api.session.hook("context", (event) => {
|
|
153
205
|
const merged = mergeCompanionSystem(event.system, companion);
|
|
@@ -164,7 +216,7 @@ export async function registerCompanion(api: Context, context: CompanionContext)
|
|
|
164
216
|
});
|
|
165
217
|
|
|
166
218
|
await api.shell.hook("create.before", (event) => {
|
|
167
|
-
applyCompanionShellEnv(context, event.env);
|
|
219
|
+
applyCompanionShellEnv(context, event.env, wrapperBinPath, env);
|
|
168
220
|
});
|
|
169
221
|
|
|
170
222
|
await api.tool.transform((editor) => {
|
|
@@ -173,7 +225,7 @@ export async function registerCompanion(api: Context, context: CompanionContext)
|
|
|
173
225
|
description: `Return active ${context.frameworkName} companion framework and working repository paths.`,
|
|
174
226
|
input: { type: "object", properties: {}, additionalProperties: false },
|
|
175
227
|
async execute() {
|
|
176
|
-
return companionPathsResult(context);
|
|
228
|
+
return companionPathsResult(context, wrapperBinPath);
|
|
177
229
|
},
|
|
178
230
|
});
|
|
179
231
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MATE_ENV } from "../../src/runtime/env";
|
|
2
|
+
|
|
3
|
+
export type LaunchEnvironment = Record<string, string | undefined>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True inside OpenCode's shared background service (`opencode serve --service`),
|
|
7
|
+
* which hosts plugins in-process for every bare `opencode` session. Its
|
|
8
|
+
* environment belongs to whichever process started it, not to any session.
|
|
9
|
+
*/
|
|
10
|
+
export function runsInSharedService(argv: readonly string[]): boolean {
|
|
11
|
+
return argv.includes("serve") && argv.includes("--service");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The environment the plugin may treat as a Mate launch environment: `env`
|
|
16
|
+
* itself for a managed launch, `{}` otherwise so every reader falls back to
|
|
17
|
+
* the Projection Root.
|
|
18
|
+
*
|
|
19
|
+
* Inherited `MATE_*` variables prove nothing — every shell under a Mate session
|
|
20
|
+
* carries them. The guidance payload does: only `mate opencode` sets it, and
|
|
21
|
+
* the plugin masks it in every shell it configures.
|
|
22
|
+
*/
|
|
23
|
+
export function pluginLaunchEnvironment(
|
|
24
|
+
env: LaunchEnvironment,
|
|
25
|
+
argv: readonly string[],
|
|
26
|
+
): LaunchEnvironment {
|
|
27
|
+
const managed = Boolean(env[MATE_ENV.guidanceJson]?.trim()) && !runsInSharedService(argv);
|
|
28
|
+
return managed ? env : {};
|
|
29
|
+
}
|
|
@@ -1,25 +1,77 @@
|
|
|
1
1
|
import { Plugin } from "@opencode/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
3
|
+
import {
|
|
4
|
+
registerCompanionHooks,
|
|
5
|
+
repositoryWrappedForOpenCode,
|
|
6
|
+
} from "../../src/opencode/companion-hooks";
|
|
3
7
|
import { readContext } from "../../src/opencode/companion-policy";
|
|
8
|
+
import { registerSessionPrepare } from "../../src/opencode/session-prepare";
|
|
9
|
+
import { hasLaunchEnvironment } from "../../src/runtime/env";
|
|
10
|
+
import { spawnPrepareRunner } from "../../src/runtime/session-prepare-runner";
|
|
11
|
+
import { readWrapStateSync } from "../../src/runtime/wrap-state";
|
|
4
12
|
|
|
5
13
|
import { registerCompanionAccess } from "./add-dir";
|
|
6
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
readTrustedAudienceLayer,
|
|
16
|
+
readWrappedAudienceLayer,
|
|
17
|
+
registerAudienceLayer,
|
|
18
|
+
} from "./audience-layer";
|
|
19
|
+
import { missingRuntimeDirectoryMessage, registerCompanion, resolveMateBin } from "./companion";
|
|
20
|
+
import { pluginLaunchEnvironment, type LaunchEnvironment } from "./launch-environment";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Only a location in a repository wrapped for OpenCode prepares, and only
|
|
24
|
+
* without a launch environment: `mate opencode` has already prepared the
|
|
25
|
+
* session it starts.
|
|
26
|
+
*/
|
|
27
|
+
async function registerPrepare(api: Context, companionPath: string, env: LaunchEnvironment) {
|
|
28
|
+
if (hasLaunchEnvironment(env)) return undefined;
|
|
29
|
+
const repoRoot = repositoryWrappedForOpenCode(api.location.directory);
|
|
30
|
+
if (!repoRoot) return undefined;
|
|
31
|
+
const mateBin = resolveMateBin();
|
|
32
|
+
if (!mateBin) {
|
|
33
|
+
console.error(`mate: ${missingRuntimeDirectoryMessage()}`);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return registerSessionPrepare(api, {
|
|
37
|
+
directory: api.location.directory,
|
|
38
|
+
repoPath: repoRoot,
|
|
39
|
+
companionPath,
|
|
40
|
+
mateServers: readWrapStateSync(repoRoot).openCodeMcpServers,
|
|
41
|
+
run: spawnPrepareRunner(mateBin, repoRoot),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
7
44
|
|
|
8
45
|
/**
|
|
9
46
|
* Mate OpenCode server plugin (OpenCode 2.x). Loaded through the package's
|
|
10
47
|
* `./server` export; stays inert when no companion resolves from the launch
|
|
11
|
-
* environment or a Projection Root above the session directory.
|
|
48
|
+
* environment or a Projection Root above the session directory. The launch
|
|
49
|
+
* environment is decided once here: outside a managed launch every reader
|
|
50
|
+
* gets `{}`, so an environment inherited by the shared service never counts.
|
|
12
51
|
*/
|
|
13
52
|
export const MateOpenCodePlugin = Plugin.define({
|
|
14
53
|
id: "mate-opencode-plugin",
|
|
15
54
|
async setup(api) {
|
|
16
|
-
const
|
|
55
|
+
const env = pluginLaunchEnvironment(process.env, process.argv);
|
|
56
|
+
const context = readContext(env, api.location.directory);
|
|
17
57
|
if (!context.companionPath) return;
|
|
18
58
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
59
|
+
/**
|
|
60
|
+
* A layer comes from a payload this launch trusts, else from the wrap of
|
|
61
|
+
* the session's checkout; an inherited environment never names one.
|
|
62
|
+
*/
|
|
63
|
+
const layer =
|
|
64
|
+
(await readTrustedAudienceLayer(env)) ??
|
|
65
|
+
(await readWrappedAudienceLayer(api.location.directory, context.companionPath));
|
|
66
|
+
await registerCompanionAccess(api, context.companionPath, layer ? [layer.root] : []);
|
|
67
|
+
await registerCompanionHooks(api, context, env);
|
|
68
|
+
await registerCompanion(api, context, env, layer?.contents.guidance);
|
|
69
|
+
if (layer) await registerAudienceLayer(api, layer.contents);
|
|
70
|
+
/** Last: the guardrails above are live while the local phase holds `setup`. */
|
|
71
|
+
const prepareCleanup = await registerPrepare(api, context.companionPath, env);
|
|
72
|
+
return async () => {
|
|
73
|
+
await prepareCleanup?.();
|
|
74
|
+
};
|
|
23
75
|
},
|
|
24
76
|
});
|
|
25
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqbit/mate-core",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
4
4
|
"description": "Core framework and plugin APIs for Mate.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"typecheck": "bunx tsc --noEmit -p ../../tsconfig.json",
|
|
36
36
|
"lint": "oxlint src/ opencode-plugin/",
|
|
37
37
|
"lint:fix": "oxlint src/ opencode-plugin/ --fix",
|
|
38
|
-
"format": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\"
|
|
39
|
-
"format:check": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\"
|
|
38
|
+
"format": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\"",
|
|
39
|
+
"format:check": "oxfmt src/ opencode-plugin/ \"!**/*.generated.*\" --check"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@opencode/plugin": "^2.0.23",
|
|
@@ -356,7 +356,7 @@ export async function resolveDriftedSpecs(
|
|
|
356
356
|
message: `mate: not a canonical spec: ${outside.join(", ")}. Only specs under ${SPECS_RELATIVE_DIR}/ are spec publication targets.`,
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
|
-
const driftedPaths = new Set(drifted);
|
|
359
|
+
const driftedPaths = new Set(drifted.map(withoutTrailingSlash));
|
|
360
360
|
const notDrifted = normalized.filter((candidate) => !driftedPaths.has(candidate));
|
|
361
361
|
if (notDrifted.length > 0) {
|
|
362
362
|
return {
|