@jstn-sdk/ma 0.1.12 → 0.14.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 (198) hide show
  1. package/.codex/agents/Architect.toml +1 -2
  2. package/.codex/agents/Auditor.toml +1 -2
  3. package/.codex/agents/Builder.toml +1 -2
  4. package/.codex/agents/Flow.toml +1 -2
  5. package/.codex/agents/Sage.toml +1 -2
  6. package/.codex/agents/Vibe.toml +1 -2
  7. package/.codex/hooks.json +24 -13
  8. package/.codex/prompts/enforcement.md +4 -0
  9. package/.codex/prompts/onboarding.md +6 -1
  10. package/.codex/prompts/release-rules.md +0 -0
  11. package/.codex/prompts/skill-contract.md +4 -0
  12. package/COVERAGE.md +211 -0
  13. package/DEMO.md +274 -0
  14. package/LICENSE +0 -0
  15. package/README.md +132 -656
  16. package/bin/ma.js +748 -55
  17. package/data/clone-data.ledger.json +41 -0
  18. package/data/clone-data.proof.json +50 -0
  19. package/data/clone-data.rvf +37 -0
  20. package/docs/README.md +10 -1
  21. package/docs/autonomous-tasks.md +46 -0
  22. package/docs/codex-integration.md +16 -0
  23. package/docs/getting-started.md +62 -22
  24. package/docs/mcp-setup.md +64 -0
  25. package/docs/qa/release-issue-gates-0.14.0.json +644 -0
  26. package/docs/qa/release-readiness-0.1.5.md +1 -1
  27. package/docs/qa/{release-readiness-0.1.12.md → release-readiness-0.14.0.md} +25 -9
  28. package/docs/quality.md +24 -0
  29. package/docs/reference/native-security-playbooks.md +6 -0
  30. package/docs/reference/native-source-selection.md +6 -0
  31. package/docs/release-spec.md +39 -10
  32. package/docs/skills.md +14 -1
  33. package/index.js +295 -2
  34. package/mcp/collections.json +23 -6
  35. package/mcp/fallback.json +0 -0
  36. package/mcp/local/code-intel.js +179 -41
  37. package/mcp/local/context.js +180 -0
  38. package/mcp/local/memory.js +17 -6
  39. package/mcp/local/quality.js +39 -0
  40. package/mcp/local/state.js +7 -3
  41. package/mcp/local/team-run.js +56 -4
  42. package/mcp/local-capabilities.json +9 -0
  43. package/mcp/servers.json +24 -0
  44. package/package.json +29 -4
  45. package/plugins/meta-architect/.app.json +1 -1
  46. package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
  47. package/plugins/meta-architect/.mcp.json +1 -1
  48. package/plugins/meta-architect/README.md +8 -1
  49. package/plugins/meta-architect/obsidian/main.js +534 -0
  50. package/plugins/meta-architect/obsidian/manifest.json +9 -0
  51. package/plugins/meta-architect/obsidian/styles.css +7 -0
  52. package/plugins/meta-architect/skills/arch/SKILL.md +2 -0
  53. package/plugins/meta-architect/skills/build/SKILL.md +3 -0
  54. package/plugins/meta-architect/skills/flow/SKILL.md +2 -0
  55. package/plugins/meta-architect/skills/sage/SKILL.md +2 -0
  56. package/plugins/meta-architect/skills/sage/references/source-selection.md +8 -0
  57. package/plugins/meta-architect/skills/vet/SKILL.md +2 -0
  58. package/plugins/meta-architect/skills/vet/references/security-playbooks.md +7 -0
  59. package/plugins/meta-architect/skills/vibe/SKILL.md +2 -0
  60. package/schemas/autonomous-task-queue.schema.json +56 -0
  61. package/schemas/handoff-packet.schema.json +36 -0
  62. package/schemas/setup-receipt.schema.json +28 -0
  63. package/schemas/skill-frontmatter.schema.json +20 -0
  64. package/schemas/support-bundle.schema.json +37 -0
  65. package/schemas/task-contract.schema.json +31 -0
  66. package/scripts/active-autonomy-hook.mjs +226 -0
  67. package/scripts/build-linux-packages.mjs +7 -3
  68. package/scripts/cleanup-test-fixtures.sh +55 -0
  69. package/scripts/context-hydration-hook.mjs +89 -0
  70. package/scripts/demo-smoke.js +285 -0
  71. package/scripts/doctor.js +27 -12
  72. package/scripts/install.sh +57 -0
  73. package/scripts/install.sh.sha256 +1 -0
  74. package/scripts/linux-package-smoke.mjs +2 -2
  75. package/scripts/package-size-check.mjs +26 -0
  76. package/scripts/plugin-sync.js +83 -17
  77. package/scripts/postinstall.js +17 -10
  78. package/scripts/prepack.js +9 -0
  79. package/scripts/ralph/prompt.md +35 -0
  80. package/scripts/release-sync.js +72 -7
  81. package/scripts/release-verify.js +260 -2
  82. package/scripts/setup-npmrc.js +48 -23
  83. package/scripts/skills-install.js +9 -2
  84. package/scripts/skills-manifest.js +2 -21
  85. package/scripts/skills-pack.js +2 -2
  86. package/scripts/skills-validate.js +5 -55
  87. package/skills/arch/SKILL.md +2 -0
  88. package/skills/build/SKILL.md +3 -0
  89. package/skills/flow/SKILL.md +2 -0
  90. package/skills/index.json +0 -0
  91. package/skills/sage/SKILL.md +2 -0
  92. package/skills/sage/references/source-selection.md +8 -0
  93. package/skills/vet/SKILL.md +2 -0
  94. package/skills/vet/references/security-playbooks.md +7 -0
  95. package/skills/vibe/SKILL.md +2 -0
  96. package/sprint/00-idea.md +0 -0
  97. package/sprint/01-architecture.md +0 -0
  98. package/sprint/02-oss-evidence.md +0 -0
  99. package/sprint/03-logic.md +0 -0
  100. package/sprint/04-security.md +0 -0
  101. package/sprint/05-dx-ux.md +0 -0
  102. package/sprint/06-build-plan.md +0 -0
  103. package/sprint/07-release.md +0 -0
  104. package/src/agents.js +279 -0
  105. package/src/bootstrap.js +268 -62
  106. package/src/build-gate.js +2 -2
  107. package/src/codex-app-server.js +291 -0
  108. package/src/decision-log.js +0 -0
  109. package/src/fs-utils.js +175 -4
  110. package/src/launcher.js +28 -21
  111. package/src/mcp-config.js +9 -1
  112. package/src/mcp-live-client.js +422 -3
  113. package/src/paths.js +10 -1
  114. package/src/policy.js +3 -3
  115. package/src/prelaunch.js +189 -0
  116. package/src/quality/ai-quality-orchestrator.js +328 -0
  117. package/src/release-issue-gates.js +252 -0
  118. package/src/release-operations.js +62 -0
  119. package/src/release-state.js +0 -0
  120. package/src/runtime/active-autonomy-core.js +208 -0
  121. package/src/runtime/agent-compat.js +31 -0
  122. package/src/runtime/alignment-sentinel.js +165 -0
  123. package/src/runtime/architect-review.js +326 -0
  124. package/src/runtime/autonomous-tasks.js +408 -0
  125. package/src/runtime/build-readiness.js +28 -4
  126. package/src/runtime/code-graph-rehearse.js +113 -0
  127. package/src/runtime/codeburn-core.js +112 -0
  128. package/src/runtime/context-authority.js +40 -0
  129. package/src/runtime/context-economy-core.js +316 -0
  130. package/src/runtime/continuity-graph.js +201 -0
  131. package/src/runtime/continuity-notes.js +71 -26
  132. package/src/runtime/core-source-ingest.js +379 -0
  133. package/src/runtime/detached-provider.js +142 -0
  134. package/src/runtime/doctor-report.js +18 -0
  135. package/src/runtime/environment-awareness-core.js +460 -0
  136. package/src/runtime/exposure-catalog.js +276 -0
  137. package/src/runtime/graphify-core.js +167 -0
  138. package/src/runtime/guidance-stack.js +9 -2
  139. package/src/runtime/handoff-packets.js +92 -0
  140. package/src/runtime/headroom-core.js +104 -0
  141. package/src/runtime/helper-orchestration-core.js +307 -0
  142. package/src/runtime/hook-profiles.js +20 -0
  143. package/src/runtime/learning-loop-core.js +344 -0
  144. package/src/runtime/live-agent-verification.js +122 -0
  145. package/src/runtime/maestro-events.js +18 -0
  146. package/src/runtime/maestro-manager.js +19 -0
  147. package/src/runtime/maestro-output.js +58 -0
  148. package/src/runtime/maestro-state.js +125 -0
  149. package/src/runtime/managed-markdown.js +29 -0
  150. package/src/runtime/mcp-authority.js +147 -0
  151. package/src/runtime/mcp-policy.js +192 -0
  152. package/src/runtime/obsidian-integration-core.js +1089 -0
  153. package/src/runtime/obsidian-plugin-bridge.js +1045 -0
  154. package/src/runtime/pi-maestro-core.js +89 -0
  155. package/src/runtime/preferences.js +79 -0
  156. package/src/runtime/project-context.js +636 -0
  157. package/src/runtime/prompt-strategy-core.js +230 -0
  158. package/src/runtime/quorum-review.js +90 -0
  159. package/src/runtime/ralph-execution-core.js +217 -0
  160. package/src/runtime/redaction-gateway.js +373 -0
  161. package/src/runtime/runtime-state.js +935 -2
  162. package/src/runtime/schema-migrations.js +162 -0
  163. package/src/runtime/semantic-recording-core.js +147 -0
  164. package/src/runtime/signal-hooks.js +28 -0
  165. package/src/runtime/skills-registry-export.js +754 -0
  166. package/src/runtime/task-contracts.js +83 -0
  167. package/src/runtime/universal-plugin-broker-core.js +898 -0
  168. package/src/runtime/workspace-intelligence-runtime.js +674 -0
  169. package/src/runtime/workspace-virtualizer.js +102 -0
  170. package/src/runtime-artifacts.js +225 -62
  171. package/src/setup-lifecycle.js +359 -0
  172. package/src/skill-frontmatter.js +78 -0
  173. package/src/skill-installer.js +226 -71
  174. package/src/skills.js +1166 -96
  175. package/src/state-sync.js +0 -0
  176. package/src/test-fixtures.js +543 -0
  177. package/src/tui/grid.js +67 -0
  178. package/src/tui/status-grid.js +23 -0
  179. package/support-bundle.json +114 -0
  180. package/templates/AGENTS.md +6 -2
  181. package/templates/catalog-manifest.json +0 -0
  182. package/templates/model-instructions/core.md +0 -0
  183. package/templates/model-instructions/release.md +0 -0
  184. package/templates/model-instructions/security.md +0 -0
  185. package/templates/quality/ai-quality-rules.yml +37 -0
  186. package/docs/assets/image/Screenshot(1).png +0 -0
  187. package/docs/assets/image/Screenshot(2).png +0 -0
  188. package/docs/assets/image/Screenshot(3).png +0 -0
  189. package/docs/assets/image/Screenshot(4).png +0 -0
  190. package/docs/assets/image/Screenshot(5).png +0 -0
  191. package/docs/assets/image/Screenshot(6).png +0 -0
  192. package/docs/assets/image/Screenshot(7).png +0 -0
  193. package/docs/assets/image/Screenshot(8).png +0 -0
  194. package/docs/assets/image/Screenshot(9).png +0 -0
  195. package/docs/assets/meta-architect-logo.png +0 -0
  196. package/docs/assets/meta-architect-logo.svg +0 -8
  197. package/docs/onboarding.md +0 -65
  198. package/docs/skills-publishing.md +0 -255
@@ -0,0 +1,898 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { ensureDir, readJson, writeFileIfMissing, writeJson } from "../fs-utils.js";
6
+ import { getRuntimeSubsystemPath } from "../paths.js";
7
+ import { agentRegistry, sanitizeSkillName } from "./skills-registry-export.js";
8
+
9
+ export const universalPluginBrokerSchemaVersion = "0.1.0";
10
+ export const universalPluginManifestSchemaVersion = "0.1.0";
11
+
12
+ const mcpInjectorHosts = ["claude-code", "antigravity", "cursor", "codex"];
13
+
14
+ function asArray(value) {
15
+ return Array.isArray(value) ? value : [];
16
+ }
17
+
18
+ function quoteTomlString(value) {
19
+ return JSON.stringify(`${value}`);
20
+ }
21
+
22
+ function quotePosixShell(value) {
23
+ const text = `${value}`;
24
+ if (text.includes("\0")) {
25
+ throw new Error("plugin command and args must not contain NUL bytes");
26
+ }
27
+ return `'${text.replaceAll("'", "'\\''")}'`;
28
+ }
29
+
30
+ function dedupe(values) {
31
+ return [...new Set(values)];
32
+ }
33
+
34
+ function mcpServerNameForPlugin(pluginName) {
35
+ const safeName = sanitizeSkillName(pluginName);
36
+ const baseName = safeName.startsWith("ma-") ? safeName.slice(3) : safeName;
37
+ return `ma-plugin-${baseName}`;
38
+ }
39
+
40
+ async function pathExists(targetPath) {
41
+ try {
42
+ await fs.access(targetPath);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ async function directoryExists(targetPath) {
50
+ try {
51
+ return (await fs.stat(targetPath)).isDirectory();
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ async function readJsonOrDefault(filePath, fallback) {
58
+ try {
59
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
60
+ } catch (error) {
61
+ if (error?.code === "ENOENT") return structuredClone(fallback);
62
+ throw new Error(`Invalid JSON configuration: ${filePath}`, { cause: error });
63
+ }
64
+ }
65
+
66
+ async function readTextOrEmpty(filePath) {
67
+ try {
68
+ return await fs.readFile(filePath, "utf8");
69
+ } catch (error) {
70
+ if (error?.code === "ENOENT") return "";
71
+ throw new Error(`Unable to read vendor configuration: ${filePath}`, { cause: error });
72
+ }
73
+ }
74
+
75
+ async function writeJsonConfig(filePath, config) {
76
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
77
+ await fs.writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`);
78
+ }
79
+
80
+ async function fileHash(filePath) {
81
+ try {
82
+ if ((await fs.lstat(filePath)).isSymbolicLink()) {
83
+ throw new Error(`Refusing to hash symlinked vendor path: ${filePath}`);
84
+ }
85
+ return crypto
86
+ .createHash("sha256")
87
+ .update(await fs.readFile(filePath))
88
+ .digest("hex");
89
+ } catch (error) {
90
+ if (error?.code === "ENOENT") return null;
91
+ throw new Error(`Unable to read vendor configuration: ${filePath}`, { cause: error });
92
+ }
93
+ }
94
+
95
+ async function pathHash(target) {
96
+ try {
97
+ const stat = await fs.lstat(target);
98
+ if (stat.isSymbolicLink()) throw new Error(`Refusing to hash symlinked path: ${target}`);
99
+ if (stat.isDirectory()) {
100
+ const entries = (await fs.readdir(target)).sort();
101
+ const parts = [];
102
+ for (const entry of entries)
103
+ parts.push(`${entry}\0${await pathHash(path.join(target, entry))}`);
104
+ return crypto
105
+ .createHash("sha256")
106
+ .update(`dir\0${parts.join("\0")}`)
107
+ .digest("hex");
108
+ }
109
+ return crypto
110
+ .createHash("sha256")
111
+ .update(await fs.readFile(target))
112
+ .digest("hex");
113
+ } catch (error) {
114
+ if (error?.code === "ENOENT") return null;
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ async function assertNoSymlinkPath(target) {
120
+ if (!target) return;
121
+ let current = path.parse(path.resolve(target)).root;
122
+ for (const part of path.relative(current, path.resolve(target)).split(path.sep).filter(Boolean)) {
123
+ current = path.join(current, part);
124
+ const stat = await fs.lstat(current).catch((error) => {
125
+ if (error?.code === "ENOENT") return null;
126
+ throw error;
127
+ });
128
+ if (stat?.isSymbolicLink()) throw new Error(`Refusing symlinked vendor path: ${current}`);
129
+ }
130
+ }
131
+
132
+ function hostConfigPath({ host, home, platform, env }) {
133
+ if (host === "claude-code") return path.join(home, ".claude.json");
134
+ if (host === "antigravity") return path.join(home, ".antigravity", "config.toml");
135
+ if (host === "cursor") return cursorStoragePath({ home, platform, env });
136
+ if (host === "codex") return path.join(home, ".codex", "config.toml");
137
+ return null;
138
+ }
139
+
140
+ async function snapshotHostConfig({ host, home, platform, env, backupRoot }) {
141
+ const configPath = hostConfigPath({ host, home, platform, env });
142
+ if (!configPath) return { host, path: null, backupPath: null, beforeHash: null };
143
+ await assertNoSymlinkPath(configPath);
144
+ const beforeHash = await fileHash(configPath);
145
+ const backupPath = path.join(backupRoot, `${host}.bak`);
146
+ if (beforeHash) {
147
+ await fs.mkdir(backupRoot, { recursive: true });
148
+ await fs.copyFile(configPath, backupPath);
149
+ }
150
+ return { host, path: configPath, backupPath: beforeHash ? backupPath : null, beforeHash };
151
+ }
152
+
153
+ function cursorStoragePath({
154
+ home = os.homedir(),
155
+ platform = process.platform,
156
+ env = process.env,
157
+ } = {}) {
158
+ if (platform === "win32") {
159
+ return path.join(
160
+ env.APPDATA || path.join(home, "AppData", "Roaming"),
161
+ "Cursor",
162
+ "User",
163
+ "globalStorage",
164
+ "storage.json",
165
+ );
166
+ }
167
+ if (platform === "darwin") {
168
+ return path.join(
169
+ home,
170
+ "Library",
171
+ "Application Support",
172
+ "Cursor",
173
+ "User",
174
+ "globalStorage",
175
+ "storage.json",
176
+ );
177
+ }
178
+ return path.join(home, ".config", "Cursor", "User", "globalStorage", "storage.json");
179
+ }
180
+
181
+ function createMcpDefinition({ manifest, pluginDir, wrapperPath, useWrapper = true }) {
182
+ const manifestArgs = asArray(manifest.mcp?.args).map((arg) =>
183
+ `${arg}`.replaceAll("{{PLUGIN_DIR}}", pluginDir),
184
+ );
185
+ if (useWrapper) {
186
+ return {
187
+ command: wrapperPath,
188
+ args: [],
189
+ };
190
+ }
191
+ return {
192
+ command: manifest.mcp.command,
193
+ args: manifestArgs,
194
+ };
195
+ }
196
+
197
+ export function renderWrapperScript({ manifest, pluginDir }) {
198
+ const args = asArray(manifest.mcp?.args).map((arg) =>
199
+ `${arg}`.replaceAll("{{PLUGIN_DIR}}", pluginDir),
200
+ );
201
+ if (manifest.mcp.command === "node" && args.length > 0) {
202
+ const [entrypoint, ...rest] = args;
203
+ const quotedRest = rest.map(quotePosixShell).join(" ");
204
+ return `#!/usr/bin/env sh\nexec ${quotePosixShell("node")} ${quotePosixShell(entrypoint)}${quotedRest ? ` ${quotedRest}` : ""} "$@"\n`;
205
+ }
206
+ const quotedArgs = args.map(quotePosixShell).join(" ");
207
+ return `#!/usr/bin/env sh\nexec ${quotePosixShell(manifest.mcp.command)}${quotedArgs ? ` ${quotedArgs}` : ""} "$@"\n`;
208
+ }
209
+
210
+ export function getUniversalPluginBrokerCorePath() {
211
+ return getRuntimeSubsystemPath("context", "universal-plugin-broker-core.json");
212
+ }
213
+
214
+ export function createDefaultUniversalPluginBrokerCore() {
215
+ const supportedAgents = Object.keys(agentRegistry).sort();
216
+ return {
217
+ schemaVersion: universalPluginBrokerSchemaVersion,
218
+ product: "Meta-Architect",
219
+ purpose:
220
+ "Defines the MA-owned hybrid plugin contract: universal MCP tooling plus vendor-agnostic MA skill context payloads across supported host agents.",
221
+ architecture: {
222
+ tooling_layer: {
223
+ protocol: "MCP stdio JSON-RPC",
224
+ server_contract: "ma-manifest.json#mcp",
225
+ usable_by: mcpInjectorHosts,
226
+ },
227
+ context_layer: {
228
+ canonical_skill_dir: ".agents/skills",
229
+ prompt_contract: "ma-manifest.json#ma_roles",
230
+ usable_by: supportedAgents,
231
+ },
232
+ },
233
+ lifecycle: ["resolve", "isolate", "generate", "mutate"],
234
+ mutation_policy: {
235
+ may_write_vendor_configs: true,
236
+ writes_only_detected_or_requested_hosts: true,
237
+ may_mutate_release_state: false,
238
+ may_mutate_decisions: false,
239
+ records_as: "plugin_compatibility_configuration",
240
+ never_records_as: "build_evidence",
241
+ requires_session_reload_for: ["claude-code", "antigravity", "codex"],
242
+ },
243
+ supported_agents: supportedAgents,
244
+ mcp_injector_hosts: mcpInjectorHosts,
245
+ };
246
+ }
247
+
248
+ export function validateUniversalPluginBrokerCore(value) {
249
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
250
+ throw new Error("universal plugin broker core must be an object");
251
+ }
252
+ if (value.schemaVersion !== universalPluginBrokerSchemaVersion) {
253
+ throw new Error(`Unsupported universal plugin broker schemaVersion: ${value.schemaVersion}`);
254
+ }
255
+ if (value.mutation_policy?.may_mutate_release_state !== false) {
256
+ throw new Error("plugin broker must not mutate release state");
257
+ }
258
+ if (value.mutation_policy?.may_mutate_decisions !== false) {
259
+ throw new Error("plugin broker must not mutate decisions");
260
+ }
261
+ if (value.mutation_policy?.records_as !== "plugin_compatibility_configuration") {
262
+ throw new Error("plugin broker must record as plugin_compatibility_configuration");
263
+ }
264
+ if (value.mutation_policy?.never_records_as !== "build_evidence") {
265
+ throw new Error("plugin broker must never record as build_evidence");
266
+ }
267
+ for (const host of mcpInjectorHosts) {
268
+ if (!value.mcp_injector_hosts?.includes(host)) {
269
+ throw new Error(`plugin broker missing MCP injector host: ${host}`);
270
+ }
271
+ }
272
+ return value;
273
+ }
274
+
275
+ export async function seedUniversalPluginBrokerCoreArtifacts() {
276
+ await ensureDir(getRuntimeSubsystemPath("context"));
277
+ await writeFileIfMissing(
278
+ getUniversalPluginBrokerCorePath(),
279
+ `${JSON.stringify(createDefaultUniversalPluginBrokerCore(), null, 2)}\n`,
280
+ );
281
+ }
282
+
283
+ export async function loadUniversalPluginBrokerCore() {
284
+ return validateUniversalPluginBrokerCore(await readJson(getUniversalPluginBrokerCorePath()));
285
+ }
286
+
287
+ export function createUniversalPluginManifest({
288
+ name,
289
+ version = "0.1.0",
290
+ description = "",
291
+ entrypoint,
292
+ mcp = null,
293
+ ma_roles: maRoles = {},
294
+ }) {
295
+ const safeName = sanitizeSkillName(name);
296
+ if (!safeName) {
297
+ throw new Error("plugin manifest requires name");
298
+ }
299
+ const normalizedMcp = mcp ?? {
300
+ command: "node",
301
+ args: ["{{PLUGIN_DIR}}/dist/index.js"],
302
+ };
303
+ return validateUniversalPluginManifest({
304
+ schemaVersion: universalPluginManifestSchemaVersion,
305
+ name: safeName,
306
+ version,
307
+ description,
308
+ entrypoint,
309
+ mcp: {
310
+ command: normalizedMcp.command,
311
+ args: asArray(normalizedMcp.args),
312
+ },
313
+ ma_roles: {
314
+ bind: dedupe(asArray(maRoles.bind)),
315
+ prompt_injection_file: maRoles.prompt_injection_file ?? null,
316
+ },
317
+ });
318
+ }
319
+
320
+ export function validateUniversalPluginManifest(value) {
321
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
322
+ throw new Error("plugin manifest must be an object");
323
+ }
324
+ const name = sanitizeSkillName(value.name);
325
+ if (!name) {
326
+ throw new Error("plugin manifest requires name");
327
+ }
328
+ if (typeof value.version !== "string" || value.version.trim() === "") {
329
+ throw new Error("plugin manifest requires version");
330
+ }
331
+ if (typeof value.entrypoint !== "string" || value.entrypoint.trim() === "") {
332
+ throw new Error("plugin manifest requires entrypoint");
333
+ }
334
+ if (typeof value.mcp?.command !== "string" || value.mcp.command.trim() === "") {
335
+ throw new Error("plugin manifest requires mcp.command");
336
+ }
337
+ if (!Array.isArray(value.mcp?.args)) {
338
+ throw new Error("plugin manifest requires mcp.args array");
339
+ }
340
+ const roleBindings = asArray(value.ma_roles?.bind);
341
+ for (const role of roleBindings) {
342
+ if (typeof role !== "string" || !role.startsWith("$")) {
343
+ throw new Error("plugin manifest role bindings must use MA trigger names such as $vet");
344
+ }
345
+ }
346
+ return {
347
+ schemaVersion: value.schemaVersion ?? universalPluginManifestSchemaVersion,
348
+ name,
349
+ version: value.version,
350
+ description: value.description ?? "",
351
+ entrypoint: value.entrypoint,
352
+ mcp: {
353
+ command: value.mcp.command,
354
+ args: value.mcp.args.map((arg) => `${arg}`),
355
+ },
356
+ ma_roles: {
357
+ bind: dedupe(roleBindings),
358
+ prompt_injection_file: value.ma_roles?.prompt_injection_file ?? null,
359
+ },
360
+ };
361
+ }
362
+
363
+ export async function loadUniversalPluginManifest(sourceDir) {
364
+ return validateUniversalPluginManifest(await readJson(path.join(sourceDir, "ma-manifest.json")));
365
+ }
366
+
367
+ export function renderUniversalMcpServerTemplate({
368
+ serverName = "ma-plugin-universal-executor",
369
+ toolName = "execute_plugin_action",
370
+ } = {}) {
371
+ return `import { Server } from "@modelcontextprotocol/sdk/server/index.js";
372
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
373
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
374
+
375
+ const server = new Server({ name: ${JSON.stringify(serverName)}, version: "1.0.0" }, { capabilities: { tools: {} } });
376
+
377
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
378
+ tools: [{
379
+ name: ${JSON.stringify(toolName)},
380
+ description: "Executes plugin logic through MA-compatible MCP stdio.",
381
+ inputSchema: {
382
+ type: "object",
383
+ properties: {
384
+ targetFilePath: { type: "string" },
385
+ payload: { type: "string" }
386
+ },
387
+ required: ["targetFilePath", "payload"]
388
+ }
389
+ }]
390
+ }));
391
+
392
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
393
+ if (request.params.name !== ${JSON.stringify(toolName)}) throw new Error("Tool not found");
394
+ const { targetFilePath, payload } = request.params.arguments;
395
+ return { content: [{ type: "text", text: JSON.stringify({ status: "success", targetFilePath, payload }) }] };
396
+ });
397
+
398
+ await server.connect(new StdioServerTransport());
399
+ `;
400
+ }
401
+
402
+ export function renderPluginContextSkillMd(manifest) {
403
+ const plugin = validateUniversalPluginManifest(manifest);
404
+ const serverName = mcpServerNameForPlugin(plugin.name);
405
+ const roles = plugin.ma_roles.bind.length > 0 ? plugin.ma_roles.bind : ["$maestro"];
406
+ return [
407
+ "---",
408
+ `name: ${serverName}`,
409
+ `description: ${plugin.description || `MA universal plugin context payload for ${plugin.name}`}`,
410
+ "---",
411
+ "",
412
+ `# ${serverName}`,
413
+ "",
414
+ "This is a Meta-Architect universal plugin context payload.",
415
+ "",
416
+ "## Role Binding",
417
+ "",
418
+ ...roles.map((role) => `- ${role}`),
419
+ "",
420
+ "## Execution Contract",
421
+ "",
422
+ `- MCP server name: \`${serverName}\``,
423
+ "- Use the MCP tool only when the active MA lane selects this plugin as task-relevant.",
424
+ "- Route authoritative state changes through `$maestro` or the owning lane.",
425
+ "- Do not mutate `.ma/release.json`, `.ma/decisions.json`, `.ma/plans/`, or `.ma/specs/` directly.",
426
+ "- Record plugin output as `plugin_compatibility_configuration` or lane-scoped context, never as `build_evidence`.",
427
+ "- If the host MCP client was already running, report that `/reload-mcp` or a session restart may be required.",
428
+ "",
429
+ ].join("\n");
430
+ }
431
+
432
+ export async function writePluginContextSkill({ manifest, home = os.homedir() }) {
433
+ const plugin = validateUniversalPluginManifest(manifest);
434
+ const serverName = mcpServerNameForPlugin(plugin.name);
435
+ const skillDir = path.join(home, ".agents", "skills", serverName);
436
+ await assertNoSymlinkPath(skillDir);
437
+ for (const fileName of ["SKILL.md", "ma-plugin-manifest.json", "ma-plugin-broker-receipt.json"]) {
438
+ await assertNoSymlinkPath(path.join(skillDir, fileName));
439
+ }
440
+ await fs.mkdir(skillDir, { recursive: true });
441
+ await fs.writeFile(path.join(skillDir, "SKILL.md"), renderPluginContextSkillMd(plugin));
442
+ await writeJson(path.join(skillDir, "ma-plugin-manifest.json"), plugin);
443
+ const receipt = {
444
+ schemaVersion: universalPluginBrokerSchemaVersion,
445
+ record_type: "plugin_context_skill_receipt",
446
+ plugin: plugin.name,
447
+ skill: serverName,
448
+ path: skillDir,
449
+ records_as: "plugin_compatibility_configuration",
450
+ build_evidence: false,
451
+ };
452
+ await writeJson(path.join(skillDir, "ma-plugin-broker-receipt.json"), receipt);
453
+ return receipt;
454
+ }
455
+
456
+ export async function detectInstalledPluginHosts({
457
+ home = os.homedir(),
458
+ platform = process.platform,
459
+ env = process.env,
460
+ } = {}) {
461
+ const hosts = [];
462
+ if (
463
+ (await pathExists(path.join(home, ".claude.json"))) ||
464
+ (await directoryExists(path.join(home, ".claude")))
465
+ ) {
466
+ hosts.push("claude-code");
467
+ }
468
+ if (await directoryExists(path.join(home, ".antigravity"))) {
469
+ hosts.push("antigravity");
470
+ }
471
+ if (
472
+ (await pathExists(cursorStoragePath({ home, platform, env }))) ||
473
+ (await directoryExists(path.dirname(cursorStoragePath({ home, platform, env }))))
474
+ ) {
475
+ hosts.push("cursor");
476
+ }
477
+ if (await directoryExists(path.join(home, ".codex"))) {
478
+ hosts.push("codex");
479
+ }
480
+ return hosts;
481
+ }
482
+
483
+ export async function injectClaudeCodeMcpServer({
484
+ home = os.homedir(),
485
+ serverName,
486
+ command,
487
+ args,
488
+ }) {
489
+ const claudeJsonPath = path.join(home, ".claude.json");
490
+ await assertNoSymlinkPath(claudeJsonPath);
491
+ if (!(await pathExists(claudeJsonPath)) && !(await directoryExists(path.join(home, ".claude")))) {
492
+ return { host: "claude-code", status: "skipped", reason: "Claude Code config not detected" };
493
+ }
494
+ const config = await readJsonOrDefault(claudeJsonPath, {});
495
+ config.mcpServers = config.mcpServers ?? {};
496
+ config.mcpServers[serverName] = { command, args };
497
+ await writeJsonConfig(claudeJsonPath, config);
498
+ return { host: "claude-code", status: "configured", path: claudeJsonPath };
499
+ }
500
+
501
+ export async function injectAntigravityMcpServer({
502
+ home = os.homedir(),
503
+ serverName,
504
+ command,
505
+ args,
506
+ }) {
507
+ const antigravityRoot = path.join(home, ".antigravity");
508
+ if (!(await directoryExists(antigravityRoot))) {
509
+ return {
510
+ host: "antigravity",
511
+ status: "skipped",
512
+ reason: "Antigravity config root not detected",
513
+ };
514
+ }
515
+ const configPath = path.join(antigravityRoot, "config.toml");
516
+ await assertNoSymlinkPath(configPath);
517
+ const existing = (await readTextOrEmpty(configPath)).trimEnd();
518
+ const marker = `name = ${quoteTomlString(serverName)}`;
519
+ if (existing.includes(marker)) {
520
+ return { host: "antigravity", status: "configured", path: configPath, idempotent: true };
521
+ }
522
+ const argList = `[${args.map((arg) => quoteTomlString(arg)).join(", ")}]`;
523
+ const block = [
524
+ "",
525
+ "[[mcp_servers]]",
526
+ `name = ${quoteTomlString(serverName)}`,
527
+ `command = ${quoteTomlString(command)}`,
528
+ `args = ${argList}`,
529
+ 'env = ["NODE_ENV=production"]',
530
+ "",
531
+ ].join("\n");
532
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
533
+ await fs.writeFile(configPath, `${existing}${block}`);
534
+ return { host: "antigravity", status: "configured", path: configPath };
535
+ }
536
+
537
+ export async function injectCursorMcpServer({
538
+ home = os.homedir(),
539
+ platform = process.platform,
540
+ env = process.env,
541
+ serverName,
542
+ command,
543
+ args,
544
+ }) {
545
+ const settingsPath = cursorStoragePath({ home, platform, env });
546
+ await assertNoSymlinkPath(settingsPath);
547
+ if (!(await pathExists(settingsPath)) && !(await directoryExists(path.dirname(settingsPath)))) {
548
+ return { host: "cursor", status: "skipped", reason: "Cursor storage not detected" };
549
+ }
550
+ const storage = await readJsonOrDefault(settingsPath, {});
551
+ storage["mcp.mcpServers"] = storage["mcp.mcpServers"] ?? {};
552
+ storage["mcp.mcpServers"][serverName] = {
553
+ name: serverName,
554
+ type: "command",
555
+ command: [command, ...args.map((arg) => `"${arg.replaceAll('"', '\\"')}"`)].join(" ").trim(),
556
+ enabled: true,
557
+ };
558
+ await writeJsonConfig(settingsPath, storage);
559
+ return { host: "cursor", status: "configured", path: settingsPath };
560
+ }
561
+
562
+ export async function injectCodexMcpServer({ home = os.homedir(), serverName, command, args }) {
563
+ const codexRoot = path.join(home, ".codex");
564
+ if (!(await directoryExists(codexRoot))) {
565
+ return { host: "codex", status: "skipped", reason: "Codex config root not detected" };
566
+ }
567
+ const configPath = path.join(codexRoot, "config.toml");
568
+ await assertNoSymlinkPath(configPath);
569
+ const existing = (await readTextOrEmpty(configPath)).trimEnd();
570
+ const header = `[mcp_servers.${quoteTomlString(serverName)}]`;
571
+ if (existing.includes(header)) {
572
+ return { host: "codex", status: "configured", path: configPath, idempotent: true };
573
+ }
574
+ const argList = `[${args.map((arg) => quoteTomlString(arg)).join(", ")}]`;
575
+ const block = ["", header, `command = ${quoteTomlString(command)}`, `args = ${argList}`, ""].join(
576
+ "\n",
577
+ );
578
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
579
+ await fs.writeFile(configPath, `${existing}${block}`);
580
+ return { host: "codex", status: "configured", path: configPath };
581
+ }
582
+
583
+ export async function injectPluginToVendors({
584
+ manifest,
585
+ pluginDir,
586
+ wrapperPath,
587
+ home = os.homedir(),
588
+ targets = null,
589
+ platform = process.platform,
590
+ env = process.env,
591
+ useWrapper = true,
592
+ backupRoot = null,
593
+ } = {}) {
594
+ const plugin = validateUniversalPluginManifest(manifest);
595
+ const selectedTargets = targets ?? (await detectInstalledPluginHosts({ home, platform, env }));
596
+ const serverName = mcpServerNameForPlugin(plugin.name);
597
+ const definition = createMcpDefinition({ manifest: plugin, pluginDir, wrapperPath, useWrapper });
598
+ const receipts = [];
599
+ const snapshots = new Map();
600
+ try {
601
+ for (const target of selectedTargets) {
602
+ const snapshot = backupRoot
603
+ ? await snapshotHostConfig({ host: target, home, platform, env, backupRoot })
604
+ : null;
605
+ if (snapshot) snapshots.set(target, snapshot);
606
+ if (target === "claude-code") {
607
+ receipts.push(await injectClaudeCodeMcpServer({ home, serverName, ...definition }));
608
+ } else if (target === "antigravity") {
609
+ receipts.push(await injectAntigravityMcpServer({ home, serverName, ...definition }));
610
+ } else if (target === "cursor") {
611
+ receipts.push(
612
+ await injectCursorMcpServer({ home, platform, env, serverName, ...definition }),
613
+ );
614
+ } else if (target === "codex") {
615
+ receipts.push(await injectCodexMcpServer({ home, serverName, ...definition }));
616
+ } else {
617
+ receipts.push({ host: target, status: "skipped", reason: "No MA MCP injector for host" });
618
+ }
619
+ }
620
+ } catch (error) {
621
+ const rollbackErrors = [];
622
+ for (const snapshot of snapshots.values()) {
623
+ try {
624
+ if (snapshot.backupPath) await fs.copyFile(snapshot.backupPath, snapshot.path);
625
+ else await fs.rm(snapshot.path, { force: true });
626
+ } catch (rollbackError) {
627
+ rollbackErrors.push(rollbackError);
628
+ }
629
+ }
630
+ if (rollbackErrors.length > 0) {
631
+ throw new AggregateError([error, ...rollbackErrors], "Plugin vendor rollback failed");
632
+ }
633
+ throw error;
634
+ }
635
+ for (const receipt of receipts) {
636
+ const snapshot = backupRoot ? snapshots.get(receipt.host) : null;
637
+ if (snapshot) {
638
+ receipt.backupPath = snapshot.backupPath;
639
+ receipt.beforeHash = snapshot.beforeHash;
640
+ receipt.afterHash = await fileHash(snapshot.path);
641
+ }
642
+ }
643
+ return {
644
+ record_type: "vendor_manifest_injection",
645
+ plugin: plugin.name,
646
+ serverName,
647
+ mcp: definition,
648
+ receipts,
649
+ configured_hosts: receipts
650
+ .filter((receipt) => receipt.status === "configured")
651
+ .map((receipt) => receipt.host),
652
+ records_as: "plugin_compatibility_configuration",
653
+ build_evidence: false,
654
+ };
655
+ }
656
+
657
+ export async function installUniversalPlugin({
658
+ sourceDir,
659
+ home = os.homedir(),
660
+ targets = null,
661
+ platform = process.platform,
662
+ env = process.env,
663
+ useWrapper = true,
664
+ dryRun = false,
665
+ } = {}) {
666
+ if (!sourceDir) {
667
+ throw new Error("installUniversalPlugin requires sourceDir");
668
+ }
669
+ const manifest = await loadUniversalPluginManifest(sourceDir);
670
+ const sourceEntrypoint = path.join(sourceDir, manifest.entrypoint);
671
+ if (!(await pathExists(sourceEntrypoint))) {
672
+ throw new Error(`Plugin entrypoint missing: ${manifest.entrypoint}`);
673
+ }
674
+ const selectedTargets = targets ?? (await detectInstalledPluginHosts({ home, platform, env }));
675
+ if (dryRun) {
676
+ return {
677
+ schemaVersion: universalPluginBrokerSchemaVersion,
678
+ record_type: "universal_plugin_broker_dry_run",
679
+ plugin: manifest.name,
680
+ targets: selectedTargets,
681
+ files: selectedTargets
682
+ .map((host) => hostConfigPath({ host, home, platform, env }))
683
+ .filter(Boolean),
684
+ records_as: "plugin_compatibility_configuration",
685
+ build_evidence: false,
686
+ };
687
+ }
688
+ await preflightVendorConfigs({ selectedTargets, home, platform, env });
689
+ const pluginDir = path.join(home, ".ma", "plugins", manifest.name);
690
+ const wrapperPath = path.join(home, ".ma", "bin", mcpServerNameForPlugin(manifest.name));
691
+ const backupRoot = path.join(home, ".ma", "backups", "plugins", manifest.name, `${Date.now()}`);
692
+ let previousPaths;
693
+ let contextSkillReceipt;
694
+ let vendorInjection;
695
+ let installedPaths;
696
+ try {
697
+ const contextSkillPath = path.join(
698
+ home,
699
+ ".agents",
700
+ "skills",
701
+ mcpServerNameForPlugin(manifest.name),
702
+ );
703
+ await assertNoSymlinkPath(backupRoot);
704
+ await assertNoSymlinkPath(pluginDir);
705
+ await assertNoSymlinkPath(wrapperPath);
706
+ await assertNoSymlinkPath(contextSkillPath);
707
+ await fs.mkdir(backupRoot, { recursive: true });
708
+ previousPaths = {
709
+ pluginDir: await backupPathIfPresent(pluginDir, path.join(backupRoot, "previous-plugin")),
710
+ wrapperPath: await backupPathIfPresent(
711
+ wrapperPath,
712
+ path.join(backupRoot, "previous-wrapper"),
713
+ ),
714
+ contextSkill: await backupPathIfPresent(
715
+ contextSkillPath,
716
+ path.join(backupRoot, "previous-skill"),
717
+ ),
718
+ };
719
+ await fs.mkdir(path.dirname(pluginDir), { recursive: true });
720
+ await fs.cp(sourceDir, pluginDir, { recursive: true });
721
+ await fs.mkdir(path.dirname(wrapperPath), { recursive: true });
722
+ await fs.writeFile(wrapperPath, renderWrapperScript({ manifest, pluginDir }));
723
+ await fs.chmod(wrapperPath, 0o755);
724
+ contextSkillReceipt = await writePluginContextSkill({ manifest, home });
725
+ vendorInjection = await injectPluginToVendors({
726
+ manifest,
727
+ pluginDir,
728
+ wrapperPath,
729
+ home,
730
+ targets: selectedTargets,
731
+ platform,
732
+ env,
733
+ useWrapper,
734
+ backupRoot,
735
+ });
736
+ installedPaths = {
737
+ pluginDir: await pathHash(pluginDir),
738
+ wrapperPath: await pathHash(wrapperPath),
739
+ contextSkill: await pathHash(contextSkillReceipt.path),
740
+ };
741
+ const receipt = {
742
+ schemaVersion: universalPluginBrokerSchemaVersion,
743
+ record_type: "universal_plugin_broker_install_receipt",
744
+ plugin: manifest.name,
745
+ pluginDir,
746
+ wrapperPath,
747
+ contextSkillReceipt,
748
+ previousPaths,
749
+ installedPaths,
750
+ vendorInjection,
751
+ backupRoot,
752
+ activation_notice:
753
+ "Plugin configured. Run /reload-mcp where supported or restart active agent sessions to activate newly injected MCP servers.",
754
+ records_as: "plugin_compatibility_configuration",
755
+ build_evidence: false,
756
+ };
757
+ await writeJson(path.join(backupRoot, "receipt.json"), receipt);
758
+ return receipt;
759
+ } catch (error) {
760
+ if (vendorInjection && previousPaths) {
761
+ try {
762
+ await rollbackUniversalPluginInstall({
763
+ pluginDir,
764
+ wrapperPath,
765
+ contextSkillReceipt,
766
+ previousPaths,
767
+ installedPaths,
768
+ vendorInjection,
769
+ backupRoot,
770
+ });
771
+ } catch (rollbackError) {
772
+ const failure = error instanceof Error ? error : new Error(String(error));
773
+ failure.cause = rollbackError;
774
+ throw failure;
775
+ }
776
+ } else {
777
+ if (previousPaths) {
778
+ await restorePath(pluginDir, previousPaths.pluginDir);
779
+ await restorePath(wrapperPath, previousPaths.wrapperPath);
780
+ await restorePath(
781
+ path.join(home, ".agents", "skills", mcpServerNameForPlugin(manifest.name)),
782
+ previousPaths.contextSkill,
783
+ );
784
+ }
785
+ await fs.rm(backupRoot, { recursive: true, force: true });
786
+ }
787
+ throw error;
788
+ }
789
+ }
790
+
791
+ async function preflightVendorConfigs({ selectedTargets, home, platform, env }) {
792
+ for (const target of selectedTargets) {
793
+ if (target === "claude-code") {
794
+ const configPath = path.join(home, ".claude.json");
795
+ if (await pathExists(configPath)) await readJsonOrDefault(configPath, {});
796
+ }
797
+ if (target === "cursor") {
798
+ const configPath = cursorStoragePath({ home, platform, env });
799
+ if (await pathExists(configPath)) await readJsonOrDefault(configPath, {});
800
+ }
801
+ if (target === "antigravity" || target === "codex") {
802
+ const configPath = hostConfigPath({ host: target, home, platform, env });
803
+ if (configPath) await readTextOrEmpty(configPath);
804
+ }
805
+ }
806
+ }
807
+
808
+ async function backupPathIfPresent(source, backup) {
809
+ if (!(await pathExists(source))) return null;
810
+ await fs.cp(source, backup, { recursive: true, force: true });
811
+ return backup;
812
+ }
813
+
814
+ async function restorePath(target, backup) {
815
+ if (!target) return;
816
+ await fs.rm(target, { recursive: true, force: true });
817
+ if (backup) await fs.cp(backup, target, { recursive: true, force: true });
818
+ }
819
+
820
+ export async function rollbackUniversalPluginInstall(receipt) {
821
+ if (receipt?.installedPaths) {
822
+ for (const [label, expectedHash] of Object.entries(receipt.installedPaths)) {
823
+ const target =
824
+ label === "pluginDir"
825
+ ? receipt.pluginDir
826
+ : label === "wrapperPath"
827
+ ? receipt.wrapperPath
828
+ : receipt.contextSkillReceipt?.path;
829
+ if ((await pathHash(target)) !== expectedHash) {
830
+ throw new Error(`Refusing rollback: installed ${label} changed after installation`);
831
+ }
832
+ }
833
+ }
834
+ const entries = receipt?.vendorInjection?.receipts ?? [];
835
+ const validated = [];
836
+ for (const entry of entries) {
837
+ if (!entry.path) continue;
838
+ const currentHash = await fileHash(entry.path);
839
+ const expectedHash = entry.afterHash ?? null;
840
+ if (currentHash !== expectedHash) {
841
+ throw new Error(`Refusing rollback for ${entry.host}: vendor config changed after injection`);
842
+ }
843
+ const backupExists = entry.backupPath && (await pathExists(entry.backupPath));
844
+ if (entry.status === "configured" && entry.beforeHash && !backupExists) {
845
+ throw new Error(`Refusing rollback for ${entry.host}: rollback source is missing`);
846
+ }
847
+ validated.push({
848
+ entry,
849
+ current: currentHash === null ? null : await fs.readFile(entry.path),
850
+ desired: backupExists ? await fs.readFile(entry.backupPath) : null,
851
+ });
852
+ }
853
+ try {
854
+ for (const { entry, desired } of validated) {
855
+ if (desired) await fs.writeFile(entry.path, desired);
856
+ else if (entry.status === "configured") await fs.rm(entry.path, { force: true });
857
+ }
858
+ } catch (error) {
859
+ const restoreErrors = [];
860
+ for (const { entry, current } of validated) {
861
+ try {
862
+ await assertNoSymlinkPath(entry.path);
863
+ if (current) await fs.writeFile(entry.path, current);
864
+ else await fs.rm(entry.path, { force: true });
865
+ } catch (restoreError) {
866
+ restoreErrors.push(restoreError);
867
+ }
868
+ }
869
+ if (restoreErrors.length > 0) {
870
+ throw new AggregateError(restoreErrors, "Plugin vendor rollback recovery failed");
871
+ }
872
+ throw new Error("Plugin rollback failed; prior vendor state was restored where possible", {
873
+ cause: error,
874
+ });
875
+ }
876
+ const pathRestoreErrors = [];
877
+ for (const [target, backup] of [
878
+ [receipt.pluginDir, receipt.previousPaths?.pluginDir],
879
+ [receipt.wrapperPath, receipt.previousPaths?.wrapperPath],
880
+ [receipt.contextSkillReceipt?.path, receipt.previousPaths?.contextSkill],
881
+ ]) {
882
+ try {
883
+ await assertNoSymlinkPath(target);
884
+ await restorePath(target, backup);
885
+ } catch (restoreError) {
886
+ pathRestoreErrors.push(restoreError);
887
+ }
888
+ }
889
+ if (pathRestoreErrors.length > 0) {
890
+ throw new AggregateError(pathRestoreErrors, "Plugin rollback recovery failed");
891
+ }
892
+ if (receipt.backupRoot) await fs.rm(receipt.backupRoot, { recursive: true, force: true });
893
+ return {
894
+ rolledBack: validated
895
+ .filter(({ entry }) => entry.status === "configured")
896
+ .map(({ entry }) => entry.host),
897
+ };
898
+ }