@uniqbit/mate-core 0.18.4 → 1.0.0-canary.0

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 (231) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/opencode-plugin/src/add-dir.ts +76 -10
  4. package/opencode-plugin/src/companion.ts +1 -8
  5. package/opencode-plugin/src/server.ts +2 -15
  6. package/package.json +10 -2
  7. package/src/brand/art.generated.ts +69 -0
  8. package/src/brand/index.ts +12 -0
  9. package/src/brand/palette.ts +19 -0
  10. package/src/brand/svg.generated.ts +8 -0
  11. package/src/brand/terminal.ts +83 -0
  12. package/src/brand/types.ts +33 -0
  13. package/src/cli/commands/artifact/artifact.ts +8 -0
  14. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  15. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  16. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  17. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  18. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  19. package/src/cli/commands/artifact/finish/command.ts +33 -3
  20. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  21. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  22. package/src/cli/commands/artifact/finish/openspec.ts +200 -129
  23. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  24. package/src/cli/commands/artifact/pending/command.ts +31 -3
  25. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  26. package/src/cli/commands/cap/graphify.ts +52 -3
  27. package/src/cli/commands/cap/index-cmd.ts +8 -1
  28. package/src/cli/commands/cap/openspec.ts +13 -25
  29. package/src/cli/commands/companion/open.ts +56 -39
  30. package/src/cli/commands/companion/update.ts +75 -42
  31. package/src/cli/commands/doctor.ts +257 -78
  32. package/src/cli/commands/install.ts +0 -8
  33. package/src/cli/commands/plugin/install.ts +9 -4
  34. package/src/cli/commands/plugin/plugin.ts +3 -2
  35. package/src/cli/commands/report/adapter.ts +11 -2
  36. package/src/cli/commands/report/contract.ts +21 -0
  37. package/src/cli/commands/report/highlight.ts +34 -4
  38. package/src/cli/commands/report/markdown.ts +110 -0
  39. package/src/cli/commands/report/palette.ts +118 -0
  40. package/src/cli/commands/report/renderer.ts +456 -153
  41. package/src/cli/commands/report/types.ts +16 -0
  42. package/src/cli/commands/setup.ts +7 -15
  43. package/src/cli/commands/studio/client-asset.ts +16 -0
  44. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  45. package/src/cli/commands/studio/routes.ts +2 -0
  46. package/src/cli/commands/studio/selection.ts +19 -1
  47. package/src/cli/commands/studio/server.ts +42 -1
  48. package/src/cli/commands/studio/terminal-launch.ts +7 -16
  49. package/src/cli/commands/studio/views/client.ts +6 -616
  50. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  51. package/src/cli/commands/studio/views/document.tsx +137 -69
  52. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  53. package/src/cli/commands/studio/views/palette.ts +134 -0
  54. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  55. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  56. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  57. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  58. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  59. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  60. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  61. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  62. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  63. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  64. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  65. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  66. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  67. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  68. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  69. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  70. package/src/cli/commands/studio/views/styles.ts +217 -206
  71. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  72. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  73. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  74. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  75. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  76. package/src/cli/commands/wrap.ts +9 -42
  77. package/src/cli/main.ts +4 -1
  78. package/src/cli/parse-flags.ts +25 -7
  79. package/src/cli/setup-selector.tsx +38 -162
  80. package/src/cli/usage.ts +5 -2
  81. package/src/hooks/companion-instructions.ts +190 -0
  82. package/src/hooks/hook-io.ts +63 -0
  83. package/src/hooks/session-banner.ts +21 -29
  84. package/src/hooks/session-guidance.ts +10 -36
  85. package/src/hooks/session-prepare.ts +115 -28
  86. package/src/hooks/validate-artifact-path.ts +82 -24
  87. package/src/index.ts +4 -0
  88. package/src/lib/change-layout.ts +154 -0
  89. package/src/lib/components/wizard-header.tsx +3 -1
  90. package/src/lib/context-mode-package.ts +24 -11
  91. package/src/lib/delivery/archive-change.ts +154 -0
  92. package/src/lib/delivery/branches.ts +108 -0
  93. package/src/lib/delivery/environment.ts +29 -0
  94. package/src/lib/delivery/forge.ts +279 -0
  95. package/src/lib/delivery/overlap.ts +202 -0
  96. package/src/lib/delivery/record-delivery.ts +196 -0
  97. package/src/lib/delivery/record.ts +343 -0
  98. package/src/lib/delivery/refresh.ts +174 -0
  99. package/src/lib/delivery/repository-access.ts +97 -0
  100. package/src/lib/delivery/stages.ts +111 -0
  101. package/src/lib/delivery/token.ts +58 -0
  102. package/src/lib/frontmatter.ts +18 -0
  103. package/src/lib/opencode-plugin-package.ts +5 -5
  104. package/src/lib/openspec-cli.ts +116 -0
  105. package/src/lib/openspec-scopes.ts +86 -0
  106. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  107. package/src/lib/orchestrator/adapters/claude.ts +60 -162
  108. package/src/lib/orchestrator/adapters/opencode.ts +9 -24
  109. package/src/lib/orchestrator/config-store.ts +2 -70
  110. package/src/lib/orchestrator/editor.ts +9 -9
  111. package/src/lib/orchestrator/openspec-store.ts +14 -54
  112. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  113. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  114. package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
  115. package/src/lib/orchestrator/projection-entries.ts +45 -15
  116. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  117. package/src/lib/orchestrator/projection-types.ts +3 -2
  118. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  119. package/src/lib/orchestrator/session-prepare.ts +49 -87
  120. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  121. package/src/lib/orchestrator/types.ts +2 -2
  122. package/src/lib/orchestrator/wrap-pass.ts +22 -88
  123. package/src/lib/package-paths.ts +8 -0
  124. package/src/opencode/companion-hooks.ts +64 -5
  125. package/src/opencode/companion-policy.ts +37 -18
  126. package/src/opencode/projected-guidance.ts +4 -1
  127. package/src/opencode/session-prepare.ts +2 -1
  128. package/src/opencode/tui.tsx +4 -2
  129. package/src/runtime/artifact-refusal.ts +63 -0
  130. package/src/runtime/claude-delivery-plan.ts +89 -7
  131. package/src/runtime/commit-attribution-guard.ts +181 -0
  132. package/src/runtime/companion-guidance.ts +15 -3
  133. package/src/runtime/companion-instructions.ts +340 -0
  134. package/src/runtime/error-message.ts +4 -0
  135. package/src/runtime/guidance.ts +0 -17
  136. package/src/runtime/index.ts +5 -0
  137. package/src/runtime/managed-block.ts +117 -0
  138. package/src/runtime/markdown-scan.ts +266 -0
  139. package/src/runtime/policy.ts +16 -15
  140. package/src/runtime/projected-flag.ts +7 -0
  141. package/src/runtime/projected-guidance.ts +1 -0
  142. package/src/runtime/runtime-document-manifest.ts +62 -28
  143. package/src/runtime/secret-guard.ts +148 -0
  144. package/src/runtime/secret-patterns.ts +164 -0
  145. package/src/runtime/secret-scan.ts +90 -0
  146. package/src/runtime/session-kind.ts +13 -0
  147. package/src/runtime/shell-heredoc.ts +81 -0
  148. package/src/runtime/wrap-state.ts +36 -6
  149. package/src/runtime/write-atomic.ts +64 -0
  150. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  151. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  152. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  153. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  154. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  155. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  156. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  157. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  158. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  159. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  160. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  161. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  162. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  163. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  164. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  165. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  166. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  167. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  168. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  169. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  170. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  171. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  172. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  173. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  174. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  175. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  176. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  177. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  178. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  179. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  180. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  181. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  182. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  183. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  184. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  185. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  186. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  187. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  188. package/src/templates/mate-skills/openspec-references.ts +140 -0
  189. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  190. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  191. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  192. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  193. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  194. package/src/tools/setup/capabilities/graphify.ts +12 -0
  195. package/src/tools/setup/capabilities/openspec.ts +273 -270
  196. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  197. package/src/tools/setup/context-services.ts +9 -17
  198. package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
  199. package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
  200. package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
  201. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  202. package/src/tools/setup/engine.ts +93 -148
  203. package/src/tools/setup/managed-output.ts +51 -0
  204. package/src/tools/setup/mate.ts +86 -11
  205. package/src/tools/setup/plugin.ts +37 -17
  206. package/src/tools/setup/plugins/gitignore.ts +1 -7
  207. package/src/tools/setup/plugins/guidance.ts +7 -4
  208. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  209. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  210. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  211. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  212. package/src/tools/setup/providers/claude.ts +285 -34
  213. package/src/tools/setup/providers/opencode.ts +180 -11
  214. package/src/tools/setup/registry.ts +2 -24
  215. package/src/tools/setup/surface-target.ts +4 -13
  216. package/src/tools/setup/utils.ts +14 -4
  217. package/src/tools/setup.ts +131 -18
  218. package/wrappers/bin/graphify +52 -4
  219. package/wrappers/bin/openspec +33 -9
  220. package/wrappers/lib/graphify-gate.mjs +8 -0
  221. package/opencode-plugin/src/audience-layer.ts +0 -91
  222. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  223. package/src/runtime/audience-layer.ts +0 -84
  224. package/src/runtime/opencode-audience-layer.ts +0 -176
  225. package/src/tools/setup/audience-isolation.fixture.ts +0 -325
  226. package/src/tools/setup/audience-layer.ts +0 -207
  227. package/src/tools/setup/audience-registrations.ts +0 -52
  228. package/src/tools/setup/contribution-ledger.ts +0 -207
  229. package/src/tools/setup/isolation-diagnostics.ts +0 -139
  230. package/src/tools/setup/runtime-documents.ts +0 -291
  231. package/src/tools/setup/upgrade-sweep.ts +0 -153
@@ -1,207 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- import { FRAMEWORK_NAME } from "../../framework";
5
- import { writeAtomic } from "../../runtime/write-atomic";
6
- import { instructionBlockKey } from "./context-services";
7
- import type { CapabilityContributionInput, RuntimeContributions } from "./plugin";
8
-
9
- /**
10
- * Contribution Ledger: which plugin owns each contribution a persisted pass
11
- * applied to the companion, per Agent Runtime. Committed with the companion,
12
- * because it describes committed files: a clone that never loaded a plugin
13
- * must still be able to remove what another clone committed. It records
14
- * identities only (names, block keys, markers, entries), never a value, so it
15
- * carries no secret.
16
- */
17
-
18
- export const CONTRIBUTION_LEDGER_VERSION = 1;
19
-
20
- /** The identities one owner applied on one runtime. Absent kinds are none. */
21
- export interface LedgerIdentities {
22
- mcpServers?: string[];
23
- guidanceKeys?: string[];
24
- skillTrees?: string[];
25
- agentDefinitions?: string[];
26
- hookMarkers?: string[];
27
- permissionEntries?: string[];
28
- pluginReferences?: string[];
29
- }
30
-
31
- export interface ContributionLedger {
32
- version: number;
33
- /** Runtime id → owning plugin id → identities. */
34
- runtimes: Record<string, Record<string, LedgerIdentities>>;
35
- /** Audience plugin packages the upgrade sweep could not load; retried by every pass. */
36
- unswept?: string[];
37
- }
38
-
39
- export function contributionLedgerPath(companionPath: string): string {
40
- return path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "contribution-ledger.json");
41
- }
42
-
43
- function isRecord(value: unknown): value is Record<string, unknown> {
44
- return typeof value === "object" && value !== null && !Array.isArray(value);
45
- }
46
-
47
- function stringList(value: unknown): string[] {
48
- return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
49
- }
50
-
51
- const IDENTITY_KINDS = [
52
- "agentDefinitions",
53
- "guidanceKeys",
54
- "hookMarkers",
55
- "mcpServers",
56
- "permissionEntries",
57
- "pluginReferences",
58
- "skillTrees",
59
- ] as const satisfies readonly (keyof LedgerIdentities)[];
60
-
61
- function sortedUnique(values: Iterable<string>): string[] {
62
- return [...new Set(values)].toSorted();
63
- }
64
-
65
- /** Sorted kinds and values, empty kinds dropped; `null` when nothing is left. */
66
- function normalizeIdentities(identities: LedgerIdentities): LedgerIdentities | null {
67
- const normalized: LedgerIdentities = {};
68
- for (const kind of IDENTITY_KINDS) {
69
- const values = sortedUnique(identities[kind] ?? []);
70
- if (values.length > 0) normalized[kind] = values;
71
- }
72
- return Object.keys(normalized).length > 0 ? normalized : null;
73
- }
74
-
75
- /** Sorted runtimes and owners; empty owners and runtimes dropped. */
76
- function normalizeLedger(ledger: ContributionLedger): ContributionLedger {
77
- const runtimes: ContributionLedger["runtimes"] = {};
78
- for (const runtimeId of Object.keys(ledger.runtimes).toSorted()) {
79
- const owners: Record<string, LedgerIdentities> = {};
80
- for (const owner of Object.keys(ledger.runtimes[runtimeId] ?? {}).toSorted()) {
81
- const identities = normalizeIdentities(ledger.runtimes[runtimeId]![owner]!);
82
- if (identities) owners[owner] = identities;
83
- }
84
- if (Object.keys(owners).length > 0) runtimes[runtimeId] = owners;
85
- }
86
- const unswept = sortedUnique(ledger.unswept ?? []);
87
- return {
88
- version: CONTRIBUTION_LEDGER_VERSION,
89
- runtimes,
90
- ...(unswept.length > 0 ? { unswept } : {}),
91
- };
92
- }
93
-
94
- /** The companion's ledger; `null` when absent or unreadable, which a pass treats as never recorded. */
95
- export async function readContributionLedger(
96
- companionPath: string,
97
- ): Promise<ContributionLedger | null> {
98
- let parsed: unknown;
99
- try {
100
- parsed = JSON.parse(await fs.readFile(contributionLedgerPath(companionPath), "utf8"));
101
- } catch {
102
- return null;
103
- }
104
- if (!isRecord(parsed) || !isRecord(parsed.runtimes)) return null;
105
- const runtimes: ContributionLedger["runtimes"] = {};
106
- for (const [runtimeId, owners] of Object.entries(parsed.runtimes)) {
107
- if (!isRecord(owners)) continue;
108
- runtimes[runtimeId] = {};
109
- for (const [owner, identities] of Object.entries(owners)) {
110
- if (!isRecord(identities)) continue;
111
- const read: LedgerIdentities = {};
112
- for (const kind of IDENTITY_KINDS) read[kind] = stringList(identities[kind]);
113
- runtimes[runtimeId][owner] = read;
114
- }
115
- }
116
- return normalizeLedger({
117
- version: CONTRIBUTION_LEDGER_VERSION,
118
- runtimes,
119
- unswept: stringList(parsed.unswept),
120
- });
121
- }
122
-
123
- /** Writes the ledger in its stable form; an unchanged ledger leaves the file untouched. */
124
- export async function writeContributionLedger(
125
- companionPath: string,
126
- ledger: ContributionLedger,
127
- ): Promise<void> {
128
- const target = contributionLedgerPath(companionPath);
129
- const text = `${JSON.stringify(normalizeLedger(ledger), null, 2)}\n`;
130
- const existing = await fs.readFile(target, "utf8").catch(() => null);
131
- if (existing === text) return;
132
- await fs.mkdir(path.dirname(target), { recursive: true });
133
- await writeAtomic(target, text);
134
- }
135
-
136
- /** The identities a contribution set applies for `pluginId`. */
137
- export function identitiesOf(
138
- pluginId: string,
139
- contributions: RuntimeContributions,
140
- ): LedgerIdentities {
141
- return {
142
- mcpServers: (contributions.mcpServers ?? []).map((server) => server.name),
143
- guidanceKeys: (contributions.guidanceSections ?? []).map((section) =>
144
- instructionBlockKey(pluginId, section.content),
145
- ),
146
- skillTrees: (contributions.skillTrees ?? []).map((tree) => tree.name),
147
- agentDefinitions: (contributions.agentDefinitions ?? []).map((agent) => agent.name),
148
- hookMarkers: (contributions.hookGroups ?? []).map((hook) => hook.marker),
149
- permissionEntries: [...(contributions.permissionEntries ?? [])],
150
- pluginReferences: (contributions.pluginReferences ?? []).map((entry) => entry.reference),
151
- };
152
- }
153
-
154
- /** The ledger a pass leaves: what its enabled inputs applied, per runtime and owner. */
155
- export function ledgerFromInputs(
156
- inputsByRuntime: ReadonlyMap<string, readonly CapabilityContributionInput[]>,
157
- unswept: readonly string[] = [],
158
- ): ContributionLedger {
159
- const runtimes: ContributionLedger["runtimes"] = {};
160
- for (const [runtimeId, inputs] of inputsByRuntime) {
161
- const owners: Record<string, LedgerIdentities> = {};
162
- for (const input of inputs) {
163
- if (!input.enabled) continue;
164
- const identities = identitiesOf(input.pluginId, input.contributions);
165
- const merged = owners[input.pluginId] ?? {};
166
- for (const kind of IDENTITY_KINDS) {
167
- merged[kind] = [...(merged[kind] ?? []), ...(identities[kind] ?? [])];
168
- }
169
- owners[input.pluginId] = merged;
170
- }
171
- runtimes[runtimeId] = owners;
172
- }
173
- return normalizeLedger({ version: CONTRIBUTION_LEDGER_VERSION, runtimes, unswept: [...unswept] });
174
- }
175
-
176
- /**
177
- * A disabled input naming every recorded identity of `owner`, so each Runtime
178
- * Surface removes them through its existing disabled-input path. Removal keys
179
- * on identity only, so no value is needed: guidance is stripped by owner, a
180
- * hook group by its marker, a plugin reference by its exact entry.
181
- */
182
- export function ledgerTeardownInput(
183
- owner: string,
184
- identities: LedgerIdentities,
185
- ): CapabilityContributionInput {
186
- return {
187
- pluginId: owner,
188
- enabled: false,
189
- capabilityEnabled: false,
190
- contributions: {
191
- mcpServers: (identities.mcpServers ?? []).map((name) => ({ name })),
192
- guidanceSections: (identities.guidanceKeys ?? []).length > 0 ? [{ content: "" }] : [],
193
- skillTrees: (identities.skillTrees ?? []).map((name) => ({ name, sourceDir: "" })),
194
- agentDefinitions: (identities.agentDefinitions ?? []).map((name) => ({ name, content: "" })),
195
- hookGroups: (identities.hookMarkers ?? []).map((marker) => ({
196
- event: "",
197
- marker,
198
- group: {},
199
- })),
200
- permissionEntries: [...(identities.permissionEntries ?? [])],
201
- pluginReferences: (identities.pluginReferences ?? []).map((reference) => ({
202
- reference,
203
- isManagedReference: (entry: unknown) => entry === reference,
204
- })),
205
- },
206
- };
207
- }
@@ -1,139 +0,0 @@
1
- // oxlint-disable no-await-in-loop
2
- import path from "node:path";
3
-
4
- import type { FrameworkConfig } from "../../lib/orchestrator/types";
5
- import {
6
- readContributionLedger,
7
- type ContributionLedger,
8
- type LedgerIdentities,
9
- } from "./contribution-ledger";
10
- import type { CapabilityPlugin, SetupContext } from "./plugin";
11
- import type { PluginRegistry } from "./registry";
12
- import { managedGuidanceOwners } from "./upgrade-sweep";
13
-
14
- /**
15
- * What `mate doctor` reports about audience isolation: contributions on the
16
- * companion's persisted surfaces that no base plugin owns, audience plugins
17
- * the upgrade sweep could not load, and the active audience's plugins whose
18
- * imperative parts a launch layer cannot deliver.
19
- */
20
-
21
- export interface LeakedContribution {
22
- /** Companion-relative file or directory. */
23
- surface: string;
24
- kind: string;
25
- /** The entry's name; absent where the surface entry is the owner's block. */
26
- identity?: string;
27
- owner: string;
28
- }
29
-
30
- export interface UndeliveredAudiencePlugin {
31
- pluginId: string;
32
- /** `apply`, `teardown`, `forProvider` or `pluginReferences`. */
33
- parts: string[];
34
- }
35
-
36
- export interface AudienceIsolationReport {
37
- leaked: LeakedContribution[];
38
- unswept: string[];
39
- undelivered: UndeliveredAudiencePlugin[];
40
- }
41
-
42
- /** Ledger kinds by where each runtime keeps them; guidance is read from the files themselves. */
43
- const LEDGER_SURFACES: Record<string, Partial<Record<keyof LedgerIdentities, string>>> = {
44
- claude: {
45
- mcpServers: ".mcp.json",
46
- skillTrees: path.join(".claude", "skills"),
47
- agentDefinitions: path.join(".claude", "agents"),
48
- hookMarkers: path.join(".claude", "settings.local.json"),
49
- permissionEntries: path.join(".claude", "settings.local.json"),
50
- },
51
- opencode: {
52
- mcpServers: path.join(".opencode", "opencode.json"),
53
- pluginReferences: path.join(".opencode", "opencode.json"),
54
- skillTrees: path.join(".agents", "skills"),
55
- agentDefinitions: path.join(".opencode", "agents"),
56
- },
57
- };
58
-
59
- function leakedLedgerContributions(
60
- ledger: ContributionLedger | null,
61
- baseIds: ReadonlySet<string>,
62
- ): LeakedContribution[] {
63
- const leaked: LeakedContribution[] = [];
64
- for (const [runtimeId, owners] of Object.entries(ledger?.runtimes ?? {})) {
65
- for (const [owner, identities] of Object.entries(owners)) {
66
- if (baseIds.has(owner)) continue;
67
- for (const [kind, surface] of Object.entries(LEDGER_SURFACES[runtimeId] ?? {})) {
68
- for (const identity of identities[kind as keyof LedgerIdentities] ?? []) {
69
- leaked.push({ surface: surface!, kind, identity, owner });
70
- }
71
- }
72
- }
73
- }
74
- return leaked;
75
- }
76
-
77
- /**
78
- * Whether a lifecycle hook does anything. `apply` and `teardown` are required
79
- * members, so a declarative plugin carries empty ones; only a body counts.
80
- */
81
- function hasBody(hook: unknown): boolean {
82
- if (typeof hook !== "function") return false;
83
- const source = Function.prototype.toString.call(hook);
84
- const open = source.indexOf("{");
85
- const close = source.lastIndexOf("}");
86
- if (open === -1 || close <= open) return true;
87
- const body = source
88
- .slice(open + 1, close)
89
- .replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "")
90
- .trim();
91
- return body.length > 0;
92
- }
93
-
94
- async function undeliveredParts(plugin: CapabilityPlugin, ctx: SetupContext): Promise<string[]> {
95
- const parts: string[] = [];
96
- if (hasBody(plugin.apply)) parts.push("apply");
97
- if (hasBody(plugin.teardown)) parts.push("teardown");
98
- if (plugin.forProvider && Object.keys(plugin.forProvider).length > 0) parts.push("forProvider");
99
- try {
100
- const byRuntime = (await plugin.getRuntimeContributions?.(ctx)) ?? {};
101
- if (Object.values(byRuntime).some((runtime) => (runtime?.pluginReferences ?? []).length > 0)) {
102
- parts.push("pluginReferences");
103
- }
104
- } catch {
105
- /** A plugin whose declarations fail is the plugin checks' finding, not this one. */
106
- }
107
- return parts;
108
- }
109
-
110
- export async function inspectAudienceIsolation(
111
- companionPath: string,
112
- config: FrameworkConfig,
113
- registry: PluginRegistry,
114
- activeAudience: string | null,
115
- ): Promise<AudienceIsolationReport> {
116
- const baseIds = new Set(registry.baseRegistrations().map((entry) => entry.plugin.id));
117
- const leaked: LeakedContribution[] = [];
118
-
119
- for (const [surface, owners] of await managedGuidanceOwners(companionPath)) {
120
- for (const owner of owners) {
121
- if (!baseIds.has(owner)) leaked.push({ surface, kind: "guidance", owner });
122
- }
123
- }
124
-
125
- const ledger = await readContributionLedger(companionPath);
126
- leaked.push(...leakedLedgerContributions(ledger, baseIds));
127
-
128
- const undelivered: UndeliveredAudiencePlugin[] = [];
129
- if (activeAudience) {
130
- const ctx: SetupContext = { companionPath, config, mode: "sync", activeProviders: [] };
131
- for (const { plugin } of registry.audienceRegistrations(activeAudience)) {
132
- if (plugin.kind !== "capability") continue;
133
- const parts = await undeliveredParts(plugin as CapabilityPlugin, ctx);
134
- if (parts.length > 0) undelivered.push({ pluginId: plugin.id, parts });
135
- }
136
- }
137
-
138
- return { leaked, unswept: ledger?.unswept ?? [], undelivered };
139
- }
@@ -1,291 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { runtimeOpenCodePluginRoot } from "../../runtime/runtime-directory";
4
- import {
5
- CLAUDE_LOCAL_CONFIG_DOCUMENT,
6
- CLAUDE_SETTINGS_DOCUMENT,
7
- OPENCODE_CONFIG_DOCUMENT,
8
- OPENCODE_MCP_SERVERS_AT,
9
- type ManagedRegion,
10
- type RenderedRuntimeDocument,
11
- } from "../../runtime/runtime-document-manifest";
12
- import type { CapabilityContributionInput, SetupContext } from "./plugin";
13
- import {
14
- mateBannerHookGroups,
15
- mateGuardHookGroups,
16
- mateGuidanceHookGroups,
17
- mateSessionPrepareHookGroups,
18
- } from "./providers/claude-plugin-hooks";
19
- import {
20
- renderManagedClaudeHookGroups,
21
- renderManagedClaudeMcpServers,
22
- renderManagedClaudePermissionEntries,
23
- } from "./providers/claude";
24
- import {
25
- renderCompanionExternalDirectoryPermissions,
26
- renderManagedOpenCodeMcpServers,
27
- } from "./providers/opencode";
28
- import { isRecord, readOpenCodeConfig } from "./providers/opencode-format";
29
- import { projectionRepoRoot } from "./surface-target";
30
-
31
- /**
32
- * The working target's documents, rendered as values. Each is a document the
33
- * runtime finds by walking up from the current directory, and every path inside
34
- * one still names the Companion Repository — only where the document is placed
35
- * differs from the companion target.
36
- */
37
-
38
- export { CLAUDE_LOCAL_CONFIG_DOCUMENT };
39
-
40
- function nonEmpty(region: ManagedRegion): boolean {
41
- if (region.kind === "list") return region.values.length > 0;
42
- if (region.kind === "map") return Object.keys(region.entries).length > 0;
43
- return true;
44
- }
45
-
46
- function document(documentPath: string, regions: ManagedRegion[]): RenderedRuntimeDocument[] {
47
- const kept = regions.filter(nonEmpty);
48
- return kept.length > 0 ? [{ path: documentPath, regions: kept }] : [];
49
- }
50
-
51
- /**
52
- * The Mate plugin root is what makes an Unmanaged OpenCode session load the
53
- * companion guidance at all: OpenCode has no config channel for a generated
54
- * system prompt, so the plugin is the only injection point. It is named through
55
- * the runtime directory, so a reinstall does not strand a wrapped repository.
56
- * The managed launch carries no overlay of its own, so this document is the one
57
- * place every OpenCode session in the repository loads it from.
58
- */
59
-
60
- /**
61
- * Loaded by a managed session beside the launch's plugin. Mate hooks here run
62
- * twice there, inertly: guard is input-pure, guidance/prepare yield under a
63
- * launch env, banner is `--projected`. Capability groups run once: the launch
64
- * omits them from `--settings`. A launch passes no `--add-dir`; it relies on
65
- * `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD` for companion `CLAUDE.md`.
66
- */
67
- function claudeSettingsRegions(
68
- ctx: SetupContext,
69
- inputs: CapabilityContributionInput[],
70
- ): ManagedRegion[] {
71
- const capabilityHooks = renderManagedClaudeHookGroups(inputs);
72
- const mateHooks: Record<string, unknown[]> = {
73
- PreToolUse: mateGuardHookGroups(),
74
- SessionStart: [
75
- ...mateBannerHookGroups(),
76
- ...mateGuidanceHookGroups(),
77
- ...mateSessionPrepareHookGroups(),
78
- ],
79
- };
80
- const events = [...new Set([...Object.keys(mateHooks), ...Object.keys(capabilityHooks)])];
81
- return [
82
- ...events.map((event): ManagedRegion => ({
83
- at: ["hooks", event],
84
- kind: "list",
85
- values: [...(mateHooks[event] ?? []), ...(capabilityHooks[event] ?? [])],
86
- })),
87
- /**
88
- * `stripPrefix` heals a repository an older Mate wrapped. That version wrote
89
- * the whole launch environment here, and Claude Code applies `env` to the
90
- * session, so every hook in a bare session inherited `MATE_*` and read the
91
- * repository as a Managed Session — silently, since looking managed is
92
- * exactly what makes the guidance hook emit nothing. Guidance must be the
93
- * projection's to give in an Unmanaged Session, so no `MATE_*` key may
94
- * survive here.
95
- */
96
- {
97
- at: ["env"],
98
- kind: "map",
99
- entries: { CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1" },
100
- stripPrefix: "MATE_",
101
- },
102
- {
103
- at: ["permissions", "allow"],
104
- kind: "list",
105
- values: renderManagedClaudePermissionEntries(ctx.companionPath, inputs),
106
- },
107
- ];
108
- }
109
-
110
- export function renderDocumentsForTarget(
111
- ctx: SetupContext,
112
- inputsByRuntime: Map<string, CapabilityContributionInput[]>,
113
- ): RenderedRuntimeDocument[] {
114
- const repoRoot = projectionRepoRoot(ctx);
115
-
116
- const documents: RenderedRuntimeDocument[] = [];
117
- if (ctx.activeProviders.includes("claude")) {
118
- const inputs = inputsByRuntime.get("claude") ?? [];
119
- documents.push(...document(CLAUDE_SETTINGS_DOCUMENT, claudeSettingsRegions(ctx, inputs)));
120
- /**
121
- * Local scope, not `.mcp.json`: a project-scoped server is inert until a
122
- * human approves it in a session, which an Unmanaged Session is exactly the
123
- * case that cannot rely on. The repo path is the key Claude Code files a
124
- * project under, resolved so a symlinked working directory still matches.
125
- *
126
- * Confirmed against a real `~/.claude.json`: the keys there are the
127
- * directories sessions started in, not repository roots — sibling
128
- * directories of one checkout each hold their own entry, as do temporary
129
- * directories. So this reaches exactly one session: the one started at the
130
- * repository root. A session started in a subdirectory of a wrapped
131
- * repository has its own key, finds no `mcpServers` under it, and sees none
132
- * of these servers — the Unmanaged Session case this document exists for,
133
- * missed. That limit is deliberate. The directories a session might start
134
- * in are unbounded, and every one of them would be a fresh entry in the
135
- * user's global configuration that nothing later reclaims; keying the
136
- * repository root only keeps the projection's footprint there to a single
137
- * key. A subdirectory session still gets the rest of the projection — the
138
- * documents inside the repository are found by the runtime's own upward
139
- * walk — it loses only the MCP servers.
140
- */
141
- documents.push(
142
- ...document(CLAUDE_LOCAL_CONFIG_DOCUMENT, [
143
- {
144
- at: ["projects", repoRoot, "mcpServers"],
145
- kind: "map",
146
- entries: renderManagedClaudeMcpServers(inputs),
147
- },
148
- ]),
149
- );
150
- }
151
- if (ctx.activeProviders.includes("opencode")) {
152
- const inputs = inputsByRuntime.get("opencode") ?? [];
153
- documents.push(
154
- ...document(OPENCODE_CONFIG_DOCUMENT, [
155
- {
156
- at: ["plugins"],
157
- kind: "list",
158
- values: [runtimeOpenCodePluginRoot()],
159
- },
160
- {
161
- at: [...OPENCODE_MCP_SERVERS_AT],
162
- kind: "map",
163
- entries: renderManagedOpenCodeMcpServers(inputs),
164
- },
165
- {
166
- at: ["permissions"],
167
- kind: "list",
168
- values: renderCompanionExternalDirectoryPermissions(ctx.companionPath),
169
- },
170
- {
171
- at: ["skills"],
172
- kind: "list",
173
- values: [path.join(ctx.companionPath, ".agents", "skills")],
174
- },
175
- { at: ["references"], kind: "map", entries: { mate: ctx.companionPath } },
176
- ]),
177
- );
178
- }
179
- return documents;
180
- }
181
-
182
- /** Keys Mate renders its own regions for; the companion's values join those regions. */
183
- const MATE_OPENCODE_KEYS = new Set(["plugins", "permissions", "skills", "mcp", "references"]);
184
-
185
- function relativePathValues(value: unknown, at: string[]): string[] {
186
- if (typeof value === "string") {
187
- return value.startsWith("./") || value.startsWith("../") ? [at.join(".")] : [];
188
- }
189
- if (Array.isArray(value)) {
190
- return value.flatMap((entry, index) => relativePathValues(entry, [...at, String(index)]));
191
- }
192
- if (isRecord(value)) {
193
- return Object.entries(value).flatMap(([key, entry]) => relativePathValues(entry, [...at, key]));
194
- }
195
- return [];
196
- }
197
-
198
- /** Replaces the region at `at` with what `merge` builds from it, or appends one. */
199
- function upsertRegion(
200
- regions: ManagedRegion[],
201
- at: string[],
202
- merge: (found: ManagedRegion | undefined) => ManagedRegion,
203
- ): void {
204
- const key = JSON.stringify(at);
205
- const index = regions.findIndex((region) => JSON.stringify(region.at) === key);
206
- if (index === -1) regions.push(merge(undefined));
207
- else regions[index] = merge(regions[index]);
208
- }
209
-
210
- /** Mate's own values stay; companion values it does not already list go first. */
211
- function mergeListRegion(regions: ManagedRegion[], at: string[], extra: unknown[]): void {
212
- upsertRegion(regions, at, (found) => {
213
- const own = found?.kind === "list" ? found.values : [];
214
- const seen = new Set(own.map((value) => JSON.stringify(value)));
215
- const values = [...extra.filter((value) => !seen.has(JSON.stringify(value))), ...own];
216
- return { at, kind: "list", values };
217
- });
218
- }
219
-
220
- /** Mate's own entries win over companion entries of the same name. */
221
- function mergeMapRegion(
222
- regions: ManagedRegion[],
223
- at: string[],
224
- extra: Record<string, unknown>,
225
- ): void {
226
- upsertRegion(regions, at, (found) => {
227
- const own = found?.kind === "map" ? found.entries : {};
228
- return { at, kind: "map", entries: { ...extra, ...own } };
229
- });
230
- }
231
-
232
- /** Relative reference paths resolved against the companion's `.opencode/`. */
233
- function resolveReferences(
234
- references: Record<string, unknown>,
235
- companionDir: string,
236
- ): Record<string, unknown> {
237
- return Object.fromEntries(
238
- Object.entries(references).map(([name, reference]) => [
239
- name,
240
- typeof reference === "string" && !path.isAbsolute(reference)
241
- ? path.resolve(companionDir, reference)
242
- : reference,
243
- ]),
244
- );
245
- }
246
-
247
- /**
248
- * Folds the companion's `.opencode/opencode.json` into the wrapped document.
249
- * Mate's regions beat companion values; other top-level keys yield to the
250
- * repository's. `references` resolve against the companion's `.opencode/`;
251
- * other relative paths are reported, not rewritten.
252
- */
253
- export async function mergeCompanionOpenCodeConfig(
254
- companionPath: string,
255
- documents: RenderedRuntimeDocument[],
256
- ): Promise<{ documents: RenderedRuntimeDocument[]; warnings: string[] }> {
257
- const index = documents.findIndex((document) => document.path === OPENCODE_CONFIG_DOCUMENT);
258
- const companionConfigPath = path.join(companionPath, ".opencode", "opencode.json");
259
- const companion = index === -1 ? null : await readOpenCodeConfig(companionConfigPath);
260
- if (!companion?.present) return { documents, warnings: [] };
261
-
262
- const regions = [...documents[index]!.regions] as ManagedRegion[];
263
- const warnings: string[] = [];
264
-
265
- const companionDir = path.dirname(companionConfigPath);
266
- for (const [key, value] of Object.entries(companion.config)) {
267
- if (key === "$schema") continue;
268
- if (key === "references" && isRecord(value)) {
269
- mergeMapRegion(regions, ["references"], resolveReferences(value, companionDir));
270
- continue;
271
- }
272
- for (const at of relativePathValues(value, [key])) {
273
- warnings.push(
274
- `${companionConfigPath}: ${at} is a relative path, which resolves against the working repository once merged into ${OPENCODE_CONFIG_DOCUMENT}; use an absolute path.`,
275
- );
276
- }
277
- if (MATE_OPENCODE_KEYS.has(key)) {
278
- if (key === "mcp" && isRecord(value) && isRecord(value.servers)) {
279
- mergeMapRegion(regions, [...OPENCODE_MCP_SERVERS_AT], value.servers);
280
- } else if (Array.isArray(value)) {
281
- mergeListRegion(regions, [key], value);
282
- }
283
- continue;
284
- }
285
- regions.push({ at: [key], kind: "value", value, yields: true });
286
- }
287
-
288
- const next = [...documents];
289
- next[index] = { path: OPENCODE_CONFIG_DOCUMENT, regions };
290
- return { documents: next, warnings };
291
- }