@uniqbit/mate-core 0.18.3 → 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/opencode-plugin/src/add-dir.ts +10 -76
- package/opencode-plugin/src/audience-layer.ts +91 -0
- package/opencode-plugin/src/companion.ts +28 -11
- package/opencode-plugin/src/launch-environment.ts +29 -0
- package/opencode-plugin/src/server.ts +25 -8
- package/package.json +1 -1
- package/src/cli/commands/cap/index-cmd.ts +2 -11
- package/src/cli/commands/doctor.ts +86 -4
- package/src/cli/commands/install.ts +8 -0
- package/src/cli/commands/studio/terminal-launch.ts +16 -7
- package/src/cli/commands/wrap.ts +29 -4
- package/src/cli/usage.ts +2 -1
- package/src/hooks/session-guidance.ts +20 -2
- package/src/lib/orchestrator/adapters/base.ts +3 -2
- package/src/lib/orchestrator/adapters/claude.ts +130 -20
- package/src/lib/orchestrator/adapters/opencode.ts +55 -15
- package/src/lib/orchestrator/config-store.ts +74 -1
- package/src/lib/orchestrator/openspec-store.ts +202 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +88 -35
- package/src/lib/orchestrator/projection-entries.ts +46 -0
- package/src/lib/orchestrator/projection-openspec-store.ts +164 -0
- package/src/lib/orchestrator/projection-types.ts +9 -1
- package/src/lib/orchestrator/session-prepare.ts +82 -4
- package/src/lib/orchestrator/working-repo-projection.ts +2 -1
- package/src/lib/orchestrator/wrap-pass.ts +92 -1
- package/src/opencode/companion-hooks.ts +7 -4
- package/src/opencode/projected-guidance.ts +6 -13
- package/src/runtime/audience-layer.ts +84 -0
- 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-document-manifest.ts +17 -0
- package/src/runtime/wrap-state.ts +4 -0
- 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/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 +7 -4
- package/src/tools/setup/engine.ts +141 -10
- package/src/tools/setup/isolation-diagnostics.ts +139 -0
- package/src/tools/setup/plugin.ts +19 -4
- package/src/tools/setup/plugins/gitignore.ts +7 -1
- package/src/tools/setup/providers/claude.ts +27 -2
- package/src/tools/setup/providers/opencode.ts +10 -0
- package/src/tools/setup/registry.ts +24 -2
- 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 +9 -1
- package/src/tools/setup.ts +9 -6
- package/wrappers/bin/openspec +42 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
6
|
+
import { removeManagedBlocksForPlugin } from "./context-services";
|
|
7
|
+
import {
|
|
8
|
+
audiencePluginDeclarations,
|
|
9
|
+
declaredAudiences,
|
|
10
|
+
effectivePluginDeclarations,
|
|
11
|
+
readFrameworkRaw,
|
|
12
|
+
} from "./dynamic-plugins/audiences";
|
|
13
|
+
import { validateDeclaration } from "./dynamic-plugins/declarations";
|
|
14
|
+
import { loadDynamicPlugin, type DynamicPluginLoadDeps } from "./dynamic-plugins/loader";
|
|
15
|
+
import type { CapabilityContributionInput, CapabilityPlugin, Plugin, SetupContext } from "./plugin";
|
|
16
|
+
import type { NormalizedRegistration } from "./registry";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Upgrade Sweep: removes what audience plugins leaked into a companion before
|
|
20
|
+
* the contribution ledger existed. Runs once, when a persisted pass finds no
|
|
21
|
+
* ledger, and again only for the packages it could not load then.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The companion files Mate's managed guidance blocks live in. */
|
|
25
|
+
const GUIDANCE_FILES = ["CLAUDE.md", "AGENTS.md", path.join(".opencode", "AGENTS.md")];
|
|
26
|
+
|
|
27
|
+
const MANAGED_BLOCK_START = new RegExp(
|
|
28
|
+
`<!-- ${FRAMEWORK_NAME}:managed:([^\\n]+):[0-9a-f]+ start -->`,
|
|
29
|
+
"g",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/** Plugin ids owning a managed guidance block in `text`. */
|
|
33
|
+
export function managedBlockOwners(text: string): string[] {
|
|
34
|
+
return [...new Set([...text.matchAll(MANAGED_BLOCK_START)].map((match) => match[1]!))];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `relative path → owners` for every managed guidance block under `root`. */
|
|
38
|
+
export async function managedGuidanceOwners(root: string): Promise<Map<string, string[]>> {
|
|
39
|
+
const owners = new Map<string, string[]>();
|
|
40
|
+
for (const file of GUIDANCE_FILES) {
|
|
41
|
+
const text = await fs.readFile(path.join(root, file), "utf8").catch(() => null);
|
|
42
|
+
if (text === null) continue;
|
|
43
|
+
const found = managedBlockOwners(text);
|
|
44
|
+
if (found.length > 0) owners.set(file, found);
|
|
45
|
+
}
|
|
46
|
+
return owners;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Guidance blocks name their owner, so this needs no plugin loaded: every
|
|
51
|
+
* block whose owner is not a base registration goes.
|
|
52
|
+
*/
|
|
53
|
+
export async function removeForeignGuidanceBlocks(
|
|
54
|
+
root: string,
|
|
55
|
+
baseIds: ReadonlySet<string>,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
for (const [file, owners] of await managedGuidanceOwners(root)) {
|
|
58
|
+
for (const owner of owners) {
|
|
59
|
+
if (baseIds.has(owner)) continue;
|
|
60
|
+
await removeManagedBlocksForPlugin(path.join(root, file), FRAMEWORK_NAME, owner);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AudienceSweep {
|
|
66
|
+
/** Disabled inputs for every swept plugin, per runtime. */
|
|
67
|
+
teardown: Map<string, CapabilityContributionInput[]>;
|
|
68
|
+
/** Packages that could not be loaded; their non-guidance entries stay until a retry. */
|
|
69
|
+
unswept: string[];
|
|
70
|
+
warnings: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AudienceSweepOptions {
|
|
74
|
+
/** Every registration of the process; an audience plugin already loaded is reused, not reloaded. */
|
|
75
|
+
registrations: readonly NormalizedRegistration[];
|
|
76
|
+
/** Restrict the sweep to these packages; every audience declaration when absent. */
|
|
77
|
+
packages?: ReadonlySet<string>;
|
|
78
|
+
deps?: DynamicPluginLoadDeps;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One distinct package per audience declaration, base packages excluded. */
|
|
82
|
+
function audiencePackages(raw: unknown): Map<string, ReturnType<typeof validateDeclaration>> {
|
|
83
|
+
const basePackages = new Set(
|
|
84
|
+
effectivePluginDeclarations(raw, null).flatMap((entry) => {
|
|
85
|
+
const { declaration } = validateDeclaration(entry);
|
|
86
|
+
return declaration ? [declaration.package] : [];
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
const found = new Map<string, ReturnType<typeof validateDeclaration>>();
|
|
90
|
+
for (const audience of declaredAudiences(raw)) {
|
|
91
|
+
for (const entry of audiencePluginDeclarations(raw, audience)) {
|
|
92
|
+
const validated = validateDeclaration(entry);
|
|
93
|
+
const name = validated.declaration?.package;
|
|
94
|
+
if (!name || basePackages.has(name) || found.has(name)) continue;
|
|
95
|
+
found.set(name, validated);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Collects the declarations of every audience plugin as disabled inputs:
|
|
103
|
+
* loaded for its declarations only — never activated, no lifecycle hook run.
|
|
104
|
+
* An unloadable plugin is reported, never fatal.
|
|
105
|
+
*/
|
|
106
|
+
export async function sweepAudienceDeclarations(
|
|
107
|
+
ctx: SetupContext,
|
|
108
|
+
options: AudienceSweepOptions,
|
|
109
|
+
): Promise<AudienceSweep> {
|
|
110
|
+
const sweep: AudienceSweep = { teardown: new Map(), unswept: [], warnings: [] };
|
|
111
|
+
const raw = await readFrameworkRaw(ctx.companionPath);
|
|
112
|
+
|
|
113
|
+
for (const [name, { declaration }] of audiencePackages(raw)) {
|
|
114
|
+
if (options.packages && !options.packages.has(name)) continue;
|
|
115
|
+
let plugin: Plugin | undefined = options.registrations.find(
|
|
116
|
+
(entry) => entry.packageName === name,
|
|
117
|
+
)?.plugin;
|
|
118
|
+
if (!plugin) {
|
|
119
|
+
const loaded = await loadDynamicPlugin(ctx.companionPath, declaration!, options.deps);
|
|
120
|
+
if (!loaded.ok) {
|
|
121
|
+
sweep.unswept.push(name);
|
|
122
|
+
sweep.warnings.push(
|
|
123
|
+
`audience plugin "${name}" was not swept from the companion: ${loaded.warning} Its entries stay until a pass can load it.`,
|
|
124
|
+
);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
plugin = loaded.plugin;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const capability = plugin as CapabilityPlugin;
|
|
131
|
+
if (plugin.kind !== "capability" || !capability.getRuntimeContributions) continue;
|
|
132
|
+
try {
|
|
133
|
+
const byRuntime = await capability.getRuntimeContributions(ctx);
|
|
134
|
+
for (const [runtimeId, contributions] of Object.entries(byRuntime)) {
|
|
135
|
+
if (!contributions) continue;
|
|
136
|
+
const inputs = sweep.teardown.get(runtimeId) ?? [];
|
|
137
|
+
inputs.push({
|
|
138
|
+
pluginId: plugin.id,
|
|
139
|
+
enabled: false,
|
|
140
|
+
capabilityEnabled: false,
|
|
141
|
+
contributions,
|
|
142
|
+
});
|
|
143
|
+
sweep.teardown.set(runtimeId, inputs);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
sweep.unswept.push(name);
|
|
147
|
+
sweep.warnings.push(
|
|
148
|
+
`audience plugin "${name}" was not swept from the companion: ${error instanceof Error ? error.message : String(error)}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return sweep;
|
|
153
|
+
}
|
|
@@ -18,6 +18,7 @@ const REPORTED_AS: Partial<
|
|
|
18
18
|
> = {
|
|
19
19
|
"projection-root": { as: "removed", label: ".mate" },
|
|
20
20
|
"git-excludes": { as: "removed", label: "git-excludes" },
|
|
21
|
+
"openspec-store-pointer": { as: "removed", label: "openspec/config.yaml" },
|
|
21
22
|
"claude-working-settings": { as: "updated", label: "claude-settings" },
|
|
22
23
|
"legacy-tokensave-claude-md": { as: "updated", label: "claude-settings" },
|
|
23
24
|
};
|
|
@@ -36,6 +37,7 @@ export async function cleanupWorkingRepository(
|
|
|
36
37
|
retained.push(entry.id);
|
|
37
38
|
continue;
|
|
38
39
|
}
|
|
40
|
+
if (entry.retains && !retained.includes(entry.retains)) retained.push(entry.retains);
|
|
39
41
|
if (entry.state !== "removed") continue;
|
|
40
42
|
const reported = REPORTED_AS[entry.id];
|
|
41
43
|
if (!reported) continue;
|
|
@@ -118,8 +118,16 @@ export async function reconcileWorkingRepoCapabilityExcludes(
|
|
|
118
118
|
if (!excludePath) return false;
|
|
119
119
|
const existing = await readExclude(excludePath);
|
|
120
120
|
const managed = new Set(managedEntries);
|
|
121
|
+
const desired = new Set(desiredEntries);
|
|
122
|
+
/**
|
|
123
|
+
* A desired line stays where it stands: re-appending it would trade places
|
|
124
|
+
* with a block written after it, like the companion copies', on every pass.
|
|
125
|
+
*/
|
|
121
126
|
const lines = trimTrailingEmptyLines(
|
|
122
|
-
contentLines(existing).filter((line) =>
|
|
127
|
+
contentLines(existing).filter((line) => {
|
|
128
|
+
const entry = line.trim();
|
|
129
|
+
return desired.has(entry) || !managed.has(entry);
|
|
130
|
+
}),
|
|
123
131
|
);
|
|
124
132
|
for (const entry of desiredEntries) {
|
|
125
133
|
if (!lines.some((line) => line.trim() === entry)) lines.push(entry);
|
package/src/tools/setup.ts
CHANGED
|
@@ -61,7 +61,7 @@ export async function applySetupCompatibilities(
|
|
|
61
61
|
companionPath: string,
|
|
62
62
|
config: FrameworkConfig,
|
|
63
63
|
mode: "setup" | "sync",
|
|
64
|
-
plugins: PluginRegistration[] = getActiveDistribution().registry.
|
|
64
|
+
plugins: PluginRegistration[] = getActiveDistribution().registry.baseRegistrations(),
|
|
65
65
|
repoPath?: string,
|
|
66
66
|
scope: SetupScope = config.type === "hub" ? "hub" : "companion",
|
|
67
67
|
): Promise<SetupInstallationOutcome> {
|
|
@@ -73,14 +73,15 @@ export async function applySetupCompatibilities(
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* The active Agent Runtimes' documents for a Working Repository, rendered
|
|
76
|
-
* without a write. Runs no plugin phase: the working target reconciles
|
|
76
|
+
* without a write: base, plus the audience the checkout is wrapped for. Runs no plugin phase: the working target reconciles
|
|
77
77
|
* declarations only, so nothing is written to the companion on its behalf.
|
|
78
78
|
*/
|
|
79
79
|
export async function renderWorkingRuntimeDocuments(
|
|
80
80
|
companionPath: string,
|
|
81
81
|
config: FrameworkConfig,
|
|
82
82
|
repoPath: string,
|
|
83
|
-
plugins: PluginRegistration[] = getActiveDistribution().registry.
|
|
83
|
+
plugins: PluginRegistration[] = getActiveDistribution().registry.baseRegistrations(),
|
|
84
|
+
wrapped?: { audience: string; registrations: PluginRegistration[] },
|
|
84
85
|
): Promise<RenderedRuntimeDocuments> {
|
|
85
86
|
const plan = buildSetupInstallationPlan(config, plugins);
|
|
86
87
|
const ctx: SetupContext = {
|
|
@@ -91,7 +92,7 @@ export async function renderWorkingRuntimeDocuments(
|
|
|
91
92
|
repoPath,
|
|
92
93
|
target: "working",
|
|
93
94
|
};
|
|
94
|
-
return renderRuntimeDocuments(ctx, plugins, plan);
|
|
95
|
+
return renderRuntimeDocuments(ctx, plugins, plan, wrapped);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
export async function updateProjectGitignore(
|
|
@@ -99,7 +100,9 @@ export async function updateProjectGitignore(
|
|
|
99
100
|
config: FrameworkConfig,
|
|
100
101
|
): Promise<void> {
|
|
101
102
|
const ctx: SetupContext = { companionPath, config, mode: "sync", activeProviders: [] };
|
|
102
|
-
const plugins = getActiveDistribution()
|
|
103
|
+
const plugins = getActiveDistribution()
|
|
104
|
+
.registry.baseRegistrations()
|
|
105
|
+
.map((entry) => entry.plugin);
|
|
103
106
|
const entries = collectManagedGitignoreEntries(ctx, plugins);
|
|
104
107
|
await writeManagedGitignoreBlock(path.join(companionPath, ".gitignore"), FRAMEWORK_NAME, entries);
|
|
105
108
|
}
|
|
@@ -113,7 +116,7 @@ export async function syncCompanionFiles(
|
|
|
113
116
|
companionPath,
|
|
114
117
|
config,
|
|
115
118
|
"sync",
|
|
116
|
-
getActiveDistribution().registry.
|
|
119
|
+
getActiveDistribution().registry.baseRegistrations(),
|
|
117
120
|
repoPath,
|
|
118
121
|
);
|
|
119
122
|
}
|
package/wrappers/bin/openspec
CHANGED
|
@@ -30,10 +30,12 @@ _mate_same_dir() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
_companion="$MATE_ARTIFACT_PATH"
|
|
33
|
+
_repo="$MATE_REPO_PATH"
|
|
33
34
|
if [ -z "$_companion" ]; then
|
|
34
35
|
_projection="$(_mate_projection_file)"
|
|
35
36
|
if [ -n "$_projection" ]; then
|
|
36
37
|
_companion="$(_mate_projected "$_projection" MATE_ARTIFACT_PATH)"
|
|
38
|
+
_repo="$(_mate_projected "$_projection" MATE_REPO_PATH)"
|
|
37
39
|
_projected_wrapper_dir="$(_mate_projected "$_projection" MATE_WRAPPER_BIN_PATH)"
|
|
38
40
|
if [ -n "$_companion" ] && ! _mate_same_dir "$_projected_wrapper_dir" "$_this_dir"; then
|
|
39
41
|
{
|
|
@@ -92,4 +94,43 @@ if [ "$command" = "templates" ]; then
|
|
|
92
94
|
fi
|
|
93
95
|
fi
|
|
94
96
|
fi
|
|
95
|
-
|
|
97
|
+
|
|
98
|
+
_mate_is_under() {
|
|
99
|
+
case "$1/" in "$2"/*) return 0 ;; esac
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Moves _cwd to the Working Repository root when the caller is inside that
|
|
104
|
+
# repository, outside the companion, and the root carries Mate's pointer.
|
|
105
|
+
_mate_use_store_pointer() {
|
|
106
|
+
[ -n "$_repo" ] && [ -d "$_repo" ] || return 0
|
|
107
|
+
local repo_root here companion_root pointer first
|
|
108
|
+
repo_root="$(cd "$_repo" && pwd -P)"
|
|
109
|
+
here="$(pwd -P)"
|
|
110
|
+
companion_root="$(cd "$_companion" 2>/dev/null && pwd -P)"
|
|
111
|
+
_mate_is_under "$here" "$repo_root" || return 0
|
|
112
|
+
[ -n "$companion_root" ] && _mate_is_under "$here" "$companion_root" && return 0
|
|
113
|
+
pointer="$repo_root/openspec/config.yaml"
|
|
114
|
+
if [ -f "$pointer" ]; then
|
|
115
|
+
IFS= read -r first < "$pointer" || true
|
|
116
|
+
case "$first" in
|
|
117
|
+
"# generated by "*" — do not edit") _cwd="$repo_root" ;;
|
|
118
|
+
esac
|
|
119
|
+
elif [ ! -f "$repo_root/openspec/config.yml" ]; then
|
|
120
|
+
echo "openspec: no OpenSpec store pointer in $repo_root, so edits are scoped to the companion — run \`mate wrap\` to enable Working Repository edit roots" >&2
|
|
121
|
+
fi
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Commands that resolve an OpenSpec root run from the Working Repository root
|
|
125
|
+
# when it carries Mate's store pointer: OpenSpec then plans in the companion's
|
|
126
|
+
# store and names the repository as the edit root. The root, not the caller's
|
|
127
|
+
# cwd, so a nested openspec/ cannot win. Every other case — including
|
|
128
|
+
# templates, init and update, which do not resolve stores — stays on the
|
|
129
|
+
# companion, exactly as before.
|
|
130
|
+
_cwd="$_companion"
|
|
131
|
+
case "$command" in
|
|
132
|
+
new|status|instructions|list|show|validate|archive|doctor|context|schemas|view)
|
|
133
|
+
_mate_use_store_pointer
|
|
134
|
+
;;
|
|
135
|
+
esac
|
|
136
|
+
exec env -C "$_cwd" "$_real" "$@"
|