@jstn-sdk/ma 0.1.13 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/.agents/plugins/marketplace.json +0 -0
  2. package/.codex/agents/Architect.toml +1 -0
  3. package/.codex/agents/Auditor.toml +1 -0
  4. package/.codex/agents/Builder.toml +1 -0
  5. package/.codex/agents/Flow.toml +1 -0
  6. package/.codex/agents/Sage.toml +1 -0
  7. package/.codex/agents/Vibe.toml +1 -0
  8. package/.codex/hooks.json +12 -2
  9. package/.codex/prompts/enforcement.md +0 -0
  10. package/.codex/prompts/onboarding.md +6 -1
  11. package/.codex/prompts/release-rules.md +0 -0
  12. package/.codex/prompts/skill-contract.md +0 -0
  13. package/COVERAGE.md +9 -9
  14. package/DEMO.md +15 -6
  15. package/LICENSE +0 -0
  16. package/README.md +127 -784
  17. package/bin/ma.js +591 -105
  18. package/data/clone-data.ledger.json +0 -0
  19. package/data/clone-data.proof.json +0 -0
  20. package/data/clone-data.rvf +0 -0
  21. package/docs/README.md +8 -2
  22. package/docs/assets/banner.png +0 -0
  23. package/docs/autonomous-tasks.md +46 -0
  24. package/docs/codex-integration.md +16 -0
  25. package/docs/getting-started.md +32 -8
  26. package/docs/installed-sdk.md +0 -0
  27. package/docs/mcp-setup.md +30 -1
  28. package/docs/qa/{release-issue-gates-0.1.13.json → release-issue-gates-0.14.1.json} +77 -77
  29. package/docs/qa/release-readiness-0.1.5.md +0 -0
  30. package/docs/qa/{release-readiness-0.1.13.md → release-readiness-0.14.1.md} +12 -12
  31. package/docs/quality.md +24 -0
  32. package/docs/reference/native-engineering-patterns.md +0 -0
  33. package/docs/reference/native-security-playbooks.md +0 -0
  34. package/docs/reference/native-source-selection.md +0 -0
  35. package/docs/reference/native-style-and-deslop.md +0 -0
  36. package/docs/release-spec.md +27 -14
  37. package/docs/skills.md +1 -1
  38. package/docs/vendor-plugin-publishing.md +49 -0
  39. package/index.js +73 -0
  40. package/mcp/collections.json +0 -0
  41. package/mcp/fallback.json +0 -0
  42. package/mcp/local/code-intel.js +185 -41
  43. package/mcp/local/context.js +180 -0
  44. package/mcp/local/memory.js +17 -6
  45. package/mcp/local/playbooks.js +0 -0
  46. package/mcp/local/quality.js +39 -0
  47. package/mcp/local/state.js +7 -3
  48. package/mcp/local/team-run.js +8 -3
  49. package/mcp/local/trace.js +0 -0
  50. package/mcp/local-capabilities.json +9 -0
  51. package/mcp/native-playbooks.json +0 -0
  52. package/mcp/servers.json +0 -0
  53. package/package.json +37 -6
  54. package/plugins/meta-architect/.app.json +1 -1
  55. package/plugins/meta-architect/.claude-plugin/plugin.json +12 -0
  56. package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
  57. package/plugins/meta-architect/.mcp.json +1 -1
  58. package/plugins/meta-architect/README.md +14 -2
  59. package/plugins/meta-architect/obsidian/main.js +137 -4
  60. package/plugins/meta-architect/obsidian/manifest.json +1 -1
  61. package/plugins/meta-architect/obsidian/styles.css +0 -0
  62. package/plugins/meta-architect/skills/align/SKILL.md +0 -0
  63. package/plugins/meta-architect/skills/align/agents/openai.yaml +0 -0
  64. package/plugins/meta-architect/skills/align/references/shared-language.md +0 -0
  65. package/plugins/meta-architect/skills/arch/SKILL.md +0 -0
  66. package/plugins/meta-architect/skills/arch/agents/openai.yaml +0 -0
  67. package/plugins/meta-architect/skills/build/SKILL.md +0 -0
  68. package/plugins/meta-architect/skills/build/agents/openai.yaml +0 -0
  69. package/plugins/meta-architect/skills/cleanup/SKILL.md +0 -0
  70. package/plugins/meta-architect/skills/cleanup/agents/openai.yaml +0 -0
  71. package/plugins/meta-architect/skills/cleanup/references/style-and-deslop.md +0 -0
  72. package/plugins/meta-architect/skills/diagnose/SKILL.md +0 -0
  73. package/plugins/meta-architect/skills/diagnose/agents/openai.yaml +0 -0
  74. package/plugins/meta-architect/skills/flow/SKILL.md +0 -0
  75. package/plugins/meta-architect/skills/flow/agents/openai.yaml +0 -0
  76. package/plugins/meta-architect/skills/maestro/SKILL.md +17 -1
  77. package/plugins/meta-architect/skills/maestro/agents/openai.yaml +0 -0
  78. package/plugins/meta-architect/skills/maestro/references/core-release-rules.md +2 -2
  79. package/plugins/meta-architect/skills/maestro/references/native-ingest-map.md +0 -0
  80. package/plugins/meta-architect/skills/sage/SKILL.md +0 -0
  81. package/plugins/meta-architect/skills/sage/agents/openai.yaml +0 -0
  82. package/plugins/meta-architect/skills/sage/references/source-selection.md +0 -0
  83. package/plugins/meta-architect/skills/tdd/SKILL.md +0 -0
  84. package/plugins/meta-architect/skills/tdd/agents/openai.yaml +0 -0
  85. package/plugins/meta-architect/skills/vet/SKILL.md +0 -0
  86. package/plugins/meta-architect/skills/vet/agents/openai.yaml +0 -0
  87. package/plugins/meta-architect/skills/vet/references/security-playbooks.md +0 -0
  88. package/plugins/meta-architect/skills/vibe/SKILL.md +0 -0
  89. package/plugins/meta-architect/skills/vibe/agents/openai.yaml +0 -0
  90. package/schemas/autonomous-task-queue.schema.json +58 -0
  91. package/schemas/handoff-packet.schema.json +36 -0
  92. package/schemas/setup-receipt.schema.json +28 -0
  93. package/schemas/skill-frontmatter.schema.json +20 -0
  94. package/schemas/support-bundle.schema.json +37 -0
  95. package/schemas/task-contract.schema.json +31 -0
  96. package/scripts/active-autonomy-hook.mjs +141 -11
  97. package/scripts/biome-staged.js +0 -0
  98. package/scripts/build-linux-packages.mjs +7 -3
  99. package/scripts/cleanup-test-fixtures.sh +55 -0
  100. package/scripts/context-hydration-hook.mjs +89 -0
  101. package/scripts/demo-smoke.js +285 -0
  102. package/scripts/doctor.js +10 -8
  103. package/scripts/install.sh +33 -4
  104. package/scripts/install.sh.sha256 +1 -0
  105. package/scripts/linux-package-lib.mjs +0 -0
  106. package/scripts/linux-package-smoke.mjs +2 -2
  107. package/scripts/package-size-check.mjs +26 -0
  108. package/scripts/plugin-build.js +314 -0
  109. package/scripts/plugin-publish.js +77 -0
  110. package/scripts/plugin-sync.js +83 -17
  111. package/scripts/prepack.js +9 -0
  112. package/scripts/ralph/prompt.md +0 -0
  113. package/scripts/release-metadata.js +0 -0
  114. package/scripts/release-sync.js +83 -7
  115. package/scripts/release-verify.js +131 -7
  116. package/scripts/setup-npmrc.js +48 -23
  117. package/scripts/skills-install.js +9 -2
  118. package/scripts/skills-manifest.js +2 -21
  119. package/scripts/skills-pack.js +2 -2
  120. package/scripts/skills-validate.js +5 -55
  121. package/skills/align/SKILL.md +0 -0
  122. package/skills/align/agents/openai.yaml +0 -0
  123. package/skills/align/references/shared-language.md +0 -0
  124. package/skills/arch/SKILL.md +0 -0
  125. package/skills/arch/agents/openai.yaml +0 -0
  126. package/skills/build/SKILL.md +0 -0
  127. package/skills/build/agents/openai.yaml +0 -0
  128. package/skills/cleanup/SKILL.md +0 -0
  129. package/skills/cleanup/agents/openai.yaml +0 -0
  130. package/skills/cleanup/references/style-and-deslop.md +0 -0
  131. package/skills/diagnose/SKILL.md +0 -0
  132. package/skills/diagnose/agents/openai.yaml +0 -0
  133. package/skills/flow/SKILL.md +0 -0
  134. package/skills/flow/agents/openai.yaml +0 -0
  135. package/skills/index.json +0 -0
  136. package/skills/maestro/SKILL.md +17 -1
  137. package/skills/maestro/agents/openai.yaml +0 -0
  138. package/skills/maestro/references/core-release-rules.md +2 -2
  139. package/skills/maestro/references/native-ingest-map.md +0 -0
  140. package/skills/sage/SKILL.md +0 -0
  141. package/skills/sage/agents/openai.yaml +0 -0
  142. package/skills/sage/references/source-selection.md +0 -0
  143. package/skills/tdd/SKILL.md +0 -0
  144. package/skills/tdd/agents/openai.yaml +0 -0
  145. package/skills/vet/SKILL.md +0 -0
  146. package/skills/vet/agents/openai.yaml +0 -0
  147. package/skills/vet/references/security-playbooks.md +0 -0
  148. package/skills/vibe/SKILL.md +0 -0
  149. package/skills/vibe/agents/openai.yaml +0 -0
  150. package/sprint/00-idea.md +0 -0
  151. package/sprint/01-architecture.md +0 -0
  152. package/sprint/02-oss-evidence.md +0 -0
  153. package/sprint/03-logic.md +0 -0
  154. package/sprint/04-security.md +0 -0
  155. package/sprint/05-dx-ux.md +0 -0
  156. package/sprint/06-build-plan.md +0 -0
  157. package/sprint/07-release.md +2 -2
  158. package/src/agents.js +281 -0
  159. package/src/bootstrap.js +272 -65
  160. package/src/build-gate.js +0 -0
  161. package/src/codex-app-server.js +291 -0
  162. package/src/decision-log.js +0 -0
  163. package/src/fs-utils.js +175 -4
  164. package/src/launcher.js +26 -23
  165. package/src/mcp-config.js +9 -1
  166. package/src/mcp-live-client.js +141 -8
  167. package/src/paths.js +6 -1
  168. package/src/policy.js +2 -2
  169. package/src/prelaunch.js +223 -0
  170. package/src/process-utils.js +62 -0
  171. package/src/quality/ai-quality-orchestrator.js +328 -0
  172. package/src/release-issue-gates.js +62 -0
  173. package/src/release-operations.js +72 -0
  174. package/src/release-state.js +0 -0
  175. package/src/runtime/active-autonomy-core.js +0 -0
  176. package/src/runtime/agent-compat.js +31 -0
  177. package/src/runtime/alignment-sentinel.js +0 -0
  178. package/src/runtime/architect-review.js +249 -11
  179. package/src/runtime/autonomous-tasks.js +585 -0
  180. package/src/runtime/build-readiness.js +0 -0
  181. package/src/runtime/code-graph-rehearse.js +0 -0
  182. package/src/runtime/codeburn-core.js +118 -0
  183. package/src/runtime/context-authority.js +40 -0
  184. package/src/runtime/context-economy-core.js +40 -0
  185. package/src/runtime/continuity-graph.js +201 -0
  186. package/src/runtime/continuity-notes.js +71 -26
  187. package/src/runtime/core-source-ingest.js +2 -2
  188. package/src/runtime/detached-provider.js +90 -13
  189. package/src/runtime/doctor-report.js +18 -0
  190. package/src/runtime/environment-awareness-core.js +0 -0
  191. package/src/runtime/exposure-catalog.js +0 -0
  192. package/src/runtime/graphify-core.js +167 -0
  193. package/src/runtime/guidance-stack.js +9 -2
  194. package/src/runtime/handoff-packets.js +92 -0
  195. package/src/runtime/headroom-core.js +104 -0
  196. package/src/runtime/helper-orchestration-core.js +0 -0
  197. package/src/runtime/hook-profiles.js +20 -0
  198. package/src/runtime/learning-loop-core.js +110 -4
  199. package/src/runtime/live-agent-verification.js +122 -0
  200. package/src/runtime/maestro-events.js +0 -0
  201. package/src/runtime/maestro-manager.js +0 -0
  202. package/src/runtime/maestro-output.js +58 -0
  203. package/src/runtime/maestro-state.js +0 -0
  204. package/src/runtime/managed-markdown.js +29 -0
  205. package/src/runtime/mcp-authority.js +147 -0
  206. package/src/runtime/mcp-policy.js +0 -0
  207. package/src/runtime/obsidian-integration-core.js +255 -17
  208. package/src/runtime/obsidian-plugin-bridge.js +344 -38
  209. package/src/runtime/orchestrator.js +0 -0
  210. package/src/runtime/pi-maestro-core.js +89 -0
  211. package/src/runtime/preferences.js +79 -0
  212. package/src/runtime/project-context.js +636 -0
  213. package/src/runtime/prompt-strategy-core.js +0 -0
  214. package/src/runtime/quorum-review.js +0 -0
  215. package/src/runtime/ralph-execution-core.js +0 -0
  216. package/src/runtime/redaction-gateway.js +218 -19
  217. package/src/runtime/runtime-state.js +94 -2
  218. package/src/runtime/schema-migrations.js +162 -0
  219. package/src/runtime/semantic-recording-core.js +0 -0
  220. package/src/runtime/signal-hooks.js +13 -0
  221. package/src/runtime/skills-registry-export.js +96 -8
  222. package/src/runtime/startup-path.js +0 -0
  223. package/src/runtime/task-contracts.js +83 -0
  224. package/src/runtime/universal-plugin-broker-core.js +373 -50
  225. package/src/runtime/workspace-intelligence-runtime.js +0 -0
  226. package/src/runtime/workspace-virtualizer.js +0 -0
  227. package/src/runtime/workspaces.js +0 -0
  228. package/src/runtime-artifacts.js +0 -0
  229. package/src/setup-lifecycle.js +359 -0
  230. package/src/skill-frontmatter.js +78 -0
  231. package/src/skill-installer.js +226 -71
  232. package/src/skills.js +309 -31
  233. package/src/state-sync.js +0 -0
  234. package/src/test-fixtures.js +543 -0
  235. package/src/tui/grid.js +67 -0
  236. package/src/tui/status-grid.js +23 -0
  237. package/support-bundle.json +114 -0
  238. package/templates/AGENTS.md +6 -2
  239. package/templates/catalog-manifest.json +0 -0
  240. package/templates/model-instructions/core.md +0 -0
  241. package/templates/model-instructions/release.md +0 -0
  242. package/templates/model-instructions/security.md +0 -0
  243. package/templates/quality/ai-quality-rules.yml +37 -0
  244. package/docs/assets/image/Screenshot(1).png +0 -0
  245. package/docs/assets/image/Screenshot(2).png +0 -0
  246. package/docs/assets/image/Screenshot(3).png +0 -0
  247. package/docs/assets/image/Screenshot(4).png +0 -0
  248. package/docs/assets/image/Screenshot(5).png +0 -0
  249. package/docs/assets/image/Screenshot(6).png +0 -0
  250. package/docs/assets/image/Screenshot(7).png +0 -0
  251. package/docs/assets/image/Screenshot(8).png +0 -0
  252. package/docs/assets/image/Screenshot(9).png +0 -0
  253. package/docs/assets/meta-architect-logo.png +0 -0
  254. package/docs/assets/meta-architect-logo.svg +0 -8
  255. package/docs/onboarding.md +0 -81
  256. package/docs/prompt-guidance-contract.md +0 -17
  257. package/docs/prompt-guidance-fragments/active-autonomy-core.md +0 -7
  258. package/docs/skills-publishing.md +0 -255
  259. package/scripts/postinstall.js +0 -23
@@ -1,3 +1,4 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs/promises";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
@@ -18,6 +19,14 @@ function quoteTomlString(value) {
18
19
  return JSON.stringify(`${value}`);
19
20
  }
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
+
21
30
  function dedupe(values) {
22
31
  return [...new Set(values)];
23
32
  }
@@ -48,8 +57,18 @@ async function directoryExists(targetPath) {
48
57
  async function readJsonOrDefault(filePath, fallback) {
49
58
  try {
50
59
  return JSON.parse(await fs.readFile(filePath, "utf8"));
51
- } catch {
52
- return structuredClone(fallback);
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 });
53
72
  }
54
73
  }
55
74
 
@@ -58,6 +77,79 @@ async function writeJsonConfig(filePath, config) {
58
77
  await fs.writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`);
59
78
  }
60
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
+
61
153
  function cursorStoragePath({
62
154
  home = os.homedir(),
63
155
  platform = process.platform,
@@ -102,17 +194,17 @@ function createMcpDefinition({ manifest, pluginDir, wrapperPath, useWrapper = tr
102
194
  };
103
195
  }
104
196
 
105
- function renderWrapperScript({ manifest, pluginDir }) {
197
+ export function renderWrapperScript({ manifest, pluginDir }) {
106
198
  const args = asArray(manifest.mcp?.args).map((arg) =>
107
199
  `${arg}`.replaceAll("{{PLUGIN_DIR}}", pluginDir),
108
200
  );
109
201
  if (manifest.mcp.command === "node" && args.length > 0) {
110
202
  const [entrypoint, ...rest] = args;
111
- const quotedRest = rest.map((arg) => `"${arg.replaceAll('"', '\\"')}"`).join(" ");
112
- return `#!/usr/bin/env sh\nexec node "${entrypoint.replaceAll('"', '\\"')}" ${quotedRest} "$@"\n`;
203
+ const quotedRest = rest.map(quotePosixShell).join(" ");
204
+ return `#!/usr/bin/env sh\nexec ${quotePosixShell("node")} ${quotePosixShell(entrypoint)}${quotedRest ? ` ${quotedRest}` : ""} "$@"\n`;
113
205
  }
114
- const quotedArgs = args.map((arg) => `"${arg.replaceAll('"', '\\"')}"`).join(" ");
115
- return `#!/usr/bin/env sh\nexec "${manifest.mcp.command.replaceAll('"', '\\"')}" ${quotedArgs} "$@"\n`;
206
+ const quotedArgs = args.map(quotePosixShell).join(" ");
207
+ return `#!/usr/bin/env sh\nexec ${quotePosixShell(manifest.mcp.command)}${quotedArgs ? ` ${quotedArgs}` : ""} "$@"\n`;
116
208
  }
117
209
 
118
210
  export function getUniversalPluginBrokerCorePath() {
@@ -341,6 +433,10 @@ export async function writePluginContextSkill({ manifest, home = os.homedir() })
341
433
  const plugin = validateUniversalPluginManifest(manifest);
342
434
  const serverName = mcpServerNameForPlugin(plugin.name);
343
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
+ }
344
440
  await fs.mkdir(skillDir, { recursive: true });
345
441
  await fs.writeFile(path.join(skillDir, "SKILL.md"), renderPluginContextSkillMd(plugin));
346
442
  await writeJson(path.join(skillDir, "ma-plugin-manifest.json"), plugin);
@@ -391,6 +487,7 @@ export async function injectClaudeCodeMcpServer({
391
487
  args,
392
488
  }) {
393
489
  const claudeJsonPath = path.join(home, ".claude.json");
490
+ await assertNoSymlinkPath(claudeJsonPath);
394
491
  if (!(await pathExists(claudeJsonPath)) && !(await directoryExists(path.join(home, ".claude")))) {
395
492
  return { host: "claude-code", status: "skipped", reason: "Claude Code config not detected" };
396
493
  }
@@ -416,7 +513,8 @@ export async function injectAntigravityMcpServer({
416
513
  };
417
514
  }
418
515
  const configPath = path.join(antigravityRoot, "config.toml");
419
- const existing = (await fs.readFile(configPath, "utf8").catch(() => "")).trimEnd();
516
+ await assertNoSymlinkPath(configPath);
517
+ const existing = (await readTextOrEmpty(configPath)).trimEnd();
420
518
  const marker = `name = ${quoteTomlString(serverName)}`;
421
519
  if (existing.includes(marker)) {
422
520
  return { host: "antigravity", status: "configured", path: configPath, idempotent: true };
@@ -445,6 +543,7 @@ export async function injectCursorMcpServer({
445
543
  args,
446
544
  }) {
447
545
  const settingsPath = cursorStoragePath({ home, platform, env });
546
+ await assertNoSymlinkPath(settingsPath);
448
547
  if (!(await pathExists(settingsPath)) && !(await directoryExists(path.dirname(settingsPath)))) {
449
548
  return { host: "cursor", status: "skipped", reason: "Cursor storage not detected" };
450
549
  }
@@ -466,7 +565,8 @@ export async function injectCodexMcpServer({ home = os.homedir(), serverName, co
466
565
  return { host: "codex", status: "skipped", reason: "Codex config root not detected" };
467
566
  }
468
567
  const configPath = path.join(codexRoot, "config.toml");
469
- const existing = (await fs.readFile(configPath, "utf8").catch(() => "")).trimEnd();
568
+ await assertNoSymlinkPath(configPath);
569
+ const existing = (await readTextOrEmpty(configPath)).trimEnd();
470
570
  const header = `[mcp_servers.${quoteTomlString(serverName)}]`;
471
571
  if (existing.includes(header)) {
472
572
  return { host: "codex", status: "configured", path: configPath, idempotent: true };
@@ -489,25 +589,55 @@ export async function injectPluginToVendors({
489
589
  platform = process.platform,
490
590
  env = process.env,
491
591
  useWrapper = true,
592
+ backupRoot = null,
492
593
  } = {}) {
493
594
  const plugin = validateUniversalPluginManifest(manifest);
494
595
  const selectedTargets = targets ?? (await detectInstalledPluginHosts({ home, platform, env }));
495
596
  const serverName = mcpServerNameForPlugin(plugin.name);
496
597
  const definition = createMcpDefinition({ manifest: plugin, pluginDir, wrapperPath, useWrapper });
497
598
  const receipts = [];
498
- for (const target of selectedTargets) {
499
- if (target === "claude-code") {
500
- receipts.push(await injectClaudeCodeMcpServer({ home, serverName, ...definition }));
501
- } else if (target === "antigravity") {
502
- receipts.push(await injectAntigravityMcpServer({ home, serverName, ...definition }));
503
- } else if (target === "cursor") {
504
- receipts.push(
505
- await injectCursorMcpServer({ home, platform, env, serverName, ...definition }),
506
- );
507
- } else if (target === "codex") {
508
- receipts.push(await injectCodexMcpServer({ home, serverName, ...definition }));
509
- } else {
510
- receipts.push({ host: target, status: "skipped", reason: "No MA MCP injector for host" });
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);
511
641
  }
512
642
  }
513
643
  return {
@@ -531,6 +661,7 @@ export async function installUniversalPlugin({
531
661
  platform = process.platform,
532
662
  env = process.env,
533
663
  useWrapper = true,
664
+ dryRun = false,
534
665
  } = {}) {
535
666
  if (!sourceDir) {
536
667
  throw new Error("installUniversalPlugin requires sourceDir");
@@ -540,36 +671,228 @@ export async function installUniversalPlugin({
540
671
  if (!(await pathExists(sourceEntrypoint))) {
541
672
  throw new Error(`Plugin entrypoint missing: ${manifest.entrypoint}`);
542
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 });
543
689
  const pluginDir = path.join(home, ".ma", "plugins", manifest.name);
544
690
  const wrapperPath = path.join(home, ".ma", "bin", mcpServerNameForPlugin(manifest.name));
545
- await fs.mkdir(path.dirname(pluginDir), { recursive: true });
546
- await fs.rm(pluginDir, { recursive: true, force: true });
547
- await fs.cp(sourceDir, pluginDir, { recursive: true });
548
- await fs.mkdir(path.dirname(wrapperPath), { recursive: true });
549
- await fs.writeFile(wrapperPath, renderWrapperScript({ manifest, pluginDir }));
550
- await fs.chmod(wrapperPath, 0o755);
551
- const contextSkillReceipt = await writePluginContextSkill({ manifest, home });
552
- const vendorInjection = await injectPluginToVendors({
553
- manifest,
554
- pluginDir,
555
- wrapperPath,
556
- home,
557
- targets,
558
- platform,
559
- env,
560
- useWrapper,
561
- });
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 });
562
893
  return {
563
- schemaVersion: universalPluginBrokerSchemaVersion,
564
- record_type: "universal_plugin_broker_install_receipt",
565
- plugin: manifest.name,
566
- pluginDir,
567
- wrapperPath,
568
- contextSkillReceipt,
569
- vendorInjection,
570
- activation_notice:
571
- "Plugin configured. Run /reload-mcp where supported or restart active agent sessions to activate newly injected MCP servers.",
572
- records_as: "plugin_compatibility_configuration",
573
- build_evidence: false,
894
+ rolledBack: validated
895
+ .filter(({ entry }) => entry.status === "configured")
896
+ .map(({ entry }) => entry.host),
574
897
  };
575
898
  }
File without changes
File without changes
File without changes
File without changes