@jstn-sdk/ma 0.1.13 → 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 (253) 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 +7 -7
  14. package/DEMO.md +15 -6
  15. package/LICENSE +0 -0
  16. package/README.md +126 -793
  17. package/bin/ma.js +585 -97
  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/autonomous-tasks.md +46 -0
  23. package/docs/codex-integration.md +16 -0
  24. package/docs/getting-started.md +28 -4
  25. package/docs/installed-sdk.md +0 -0
  26. package/docs/mcp-setup.md +30 -1
  27. package/docs/qa/{release-issue-gates-0.1.13.json → release-issue-gates-0.14.0.json} +77 -77
  28. package/docs/qa/release-readiness-0.1.5.md +0 -0
  29. package/docs/qa/{release-readiness-0.1.13.md → release-readiness-0.14.0.md} +12 -12
  30. package/docs/quality.md +24 -0
  31. package/docs/reference/native-engineering-patterns.md +0 -0
  32. package/docs/reference/native-security-playbooks.md +0 -0
  33. package/docs/reference/native-source-selection.md +0 -0
  34. package/docs/reference/native-style-and-deslop.md +0 -0
  35. package/docs/release-spec.md +27 -14
  36. package/docs/skills.md +1 -1
  37. package/index.js +73 -0
  38. package/mcp/collections.json +0 -0
  39. package/mcp/fallback.json +0 -0
  40. package/mcp/local/code-intel.js +179 -41
  41. package/mcp/local/context.js +180 -0
  42. package/mcp/local/memory.js +17 -6
  43. package/mcp/local/playbooks.js +0 -0
  44. package/mcp/local/quality.js +39 -0
  45. package/mcp/local/state.js +7 -3
  46. package/mcp/local/team-run.js +8 -3
  47. package/mcp/local/trace.js +0 -0
  48. package/mcp/local-capabilities.json +9 -0
  49. package/mcp/native-playbooks.json +0 -0
  50. package/mcp/servers.json +0 -0
  51. package/package.json +26 -4
  52. package/plugins/meta-architect/.app.json +1 -1
  53. package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
  54. package/plugins/meta-architect/.mcp.json +1 -1
  55. package/plugins/meta-architect/README.md +2 -2
  56. package/plugins/meta-architect/obsidian/main.js +137 -4
  57. package/plugins/meta-architect/obsidian/manifest.json +1 -1
  58. package/plugins/meta-architect/obsidian/styles.css +0 -0
  59. package/plugins/meta-architect/skills/align/SKILL.md +0 -0
  60. package/plugins/meta-architect/skills/align/agents/openai.yaml +0 -0
  61. package/plugins/meta-architect/skills/align/references/shared-language.md +0 -0
  62. package/plugins/meta-architect/skills/arch/SKILL.md +0 -0
  63. package/plugins/meta-architect/skills/arch/agents/openai.yaml +0 -0
  64. package/plugins/meta-architect/skills/build/SKILL.md +0 -0
  65. package/plugins/meta-architect/skills/build/agents/openai.yaml +0 -0
  66. package/plugins/meta-architect/skills/cleanup/SKILL.md +0 -0
  67. package/plugins/meta-architect/skills/cleanup/agents/openai.yaml +0 -0
  68. package/plugins/meta-architect/skills/cleanup/references/style-and-deslop.md +0 -0
  69. package/plugins/meta-architect/skills/diagnose/SKILL.md +0 -0
  70. package/plugins/meta-architect/skills/diagnose/agents/openai.yaml +0 -0
  71. package/plugins/meta-architect/skills/flow/SKILL.md +0 -0
  72. package/plugins/meta-architect/skills/flow/agents/openai.yaml +0 -0
  73. package/plugins/meta-architect/skills/maestro/SKILL.md +0 -0
  74. package/plugins/meta-architect/skills/maestro/agents/openai.yaml +0 -0
  75. package/plugins/meta-architect/skills/maestro/references/core-release-rules.md +0 -0
  76. package/plugins/meta-architect/skills/maestro/references/native-ingest-map.md +0 -0
  77. package/plugins/meta-architect/skills/sage/SKILL.md +0 -0
  78. package/plugins/meta-architect/skills/sage/agents/openai.yaml +0 -0
  79. package/plugins/meta-architect/skills/sage/references/source-selection.md +0 -0
  80. package/plugins/meta-architect/skills/tdd/SKILL.md +0 -0
  81. package/plugins/meta-architect/skills/tdd/agents/openai.yaml +0 -0
  82. package/plugins/meta-architect/skills/vet/SKILL.md +0 -0
  83. package/plugins/meta-architect/skills/vet/agents/openai.yaml +0 -0
  84. package/plugins/meta-architect/skills/vet/references/security-playbooks.md +0 -0
  85. package/plugins/meta-architect/skills/vibe/SKILL.md +0 -0
  86. package/plugins/meta-architect/skills/vibe/agents/openai.yaml +0 -0
  87. package/schemas/autonomous-task-queue.schema.json +56 -0
  88. package/schemas/handoff-packet.schema.json +36 -0
  89. package/schemas/setup-receipt.schema.json +28 -0
  90. package/schemas/skill-frontmatter.schema.json +20 -0
  91. package/schemas/support-bundle.schema.json +37 -0
  92. package/schemas/task-contract.schema.json +31 -0
  93. package/scripts/active-autonomy-hook.mjs +141 -11
  94. package/scripts/biome-staged.js +0 -0
  95. package/scripts/build-linux-packages.mjs +7 -3
  96. package/scripts/cleanup-test-fixtures.sh +55 -0
  97. package/scripts/context-hydration-hook.mjs +89 -0
  98. package/scripts/demo-smoke.js +285 -0
  99. package/scripts/doctor.js +10 -8
  100. package/scripts/install.sh +33 -4
  101. package/scripts/install.sh.sha256 +1 -0
  102. package/scripts/linux-package-lib.mjs +0 -0
  103. package/scripts/linux-package-smoke.mjs +2 -2
  104. package/scripts/package-size-check.mjs +26 -0
  105. package/scripts/plugin-sync.js +83 -17
  106. package/scripts/postinstall.js +17 -10
  107. package/scripts/prepack.js +9 -0
  108. package/scripts/ralph/prompt.md +0 -0
  109. package/scripts/release-metadata.js +0 -0
  110. package/scripts/release-sync.js +72 -7
  111. package/scripts/release-verify.js +100 -7
  112. package/scripts/setup-npmrc.js +48 -23
  113. package/scripts/skills-install.js +9 -2
  114. package/scripts/skills-manifest.js +2 -21
  115. package/scripts/skills-pack.js +2 -2
  116. package/scripts/skills-validate.js +5 -55
  117. package/skills/align/SKILL.md +0 -0
  118. package/skills/align/agents/openai.yaml +0 -0
  119. package/skills/align/references/shared-language.md +0 -0
  120. package/skills/arch/SKILL.md +0 -0
  121. package/skills/arch/agents/openai.yaml +0 -0
  122. package/skills/build/SKILL.md +0 -0
  123. package/skills/build/agents/openai.yaml +0 -0
  124. package/skills/cleanup/SKILL.md +0 -0
  125. package/skills/cleanup/agents/openai.yaml +0 -0
  126. package/skills/cleanup/references/style-and-deslop.md +0 -0
  127. package/skills/diagnose/SKILL.md +0 -0
  128. package/skills/diagnose/agents/openai.yaml +0 -0
  129. package/skills/flow/SKILL.md +0 -0
  130. package/skills/flow/agents/openai.yaml +0 -0
  131. package/skills/index.json +0 -0
  132. package/skills/maestro/SKILL.md +0 -0
  133. package/skills/maestro/agents/openai.yaml +0 -0
  134. package/skills/maestro/references/core-release-rules.md +0 -0
  135. package/skills/maestro/references/native-ingest-map.md +0 -0
  136. package/skills/sage/SKILL.md +0 -0
  137. package/skills/sage/agents/openai.yaml +0 -0
  138. package/skills/sage/references/source-selection.md +0 -0
  139. package/skills/tdd/SKILL.md +0 -0
  140. package/skills/tdd/agents/openai.yaml +0 -0
  141. package/skills/vet/SKILL.md +0 -0
  142. package/skills/vet/agents/openai.yaml +0 -0
  143. package/skills/vet/references/security-playbooks.md +0 -0
  144. package/skills/vibe/SKILL.md +0 -0
  145. package/skills/vibe/agents/openai.yaml +0 -0
  146. package/sprint/00-idea.md +0 -0
  147. package/sprint/01-architecture.md +0 -0
  148. package/sprint/02-oss-evidence.md +0 -0
  149. package/sprint/03-logic.md +0 -0
  150. package/sprint/04-security.md +0 -0
  151. package/sprint/05-dx-ux.md +0 -0
  152. package/sprint/06-build-plan.md +0 -0
  153. package/sprint/07-release.md +0 -0
  154. package/src/agents.js +279 -0
  155. package/src/bootstrap.js +252 -63
  156. package/src/build-gate.js +0 -0
  157. package/src/codex-app-server.js +291 -0
  158. package/src/decision-log.js +0 -0
  159. package/src/fs-utils.js +175 -4
  160. package/src/launcher.js +24 -21
  161. package/src/mcp-config.js +9 -1
  162. package/src/mcp-live-client.js +140 -7
  163. package/src/paths.js +6 -1
  164. package/src/policy.js +2 -2
  165. package/src/prelaunch.js +189 -0
  166. package/src/quality/ai-quality-orchestrator.js +328 -0
  167. package/src/release-issue-gates.js +62 -0
  168. package/src/release-operations.js +62 -0
  169. package/src/release-state.js +0 -0
  170. package/src/runtime/active-autonomy-core.js +0 -0
  171. package/src/runtime/agent-compat.js +31 -0
  172. package/src/runtime/alignment-sentinel.js +0 -0
  173. package/src/runtime/architect-review.js +248 -10
  174. package/src/runtime/autonomous-tasks.js +408 -0
  175. package/src/runtime/build-readiness.js +0 -0
  176. package/src/runtime/code-graph-rehearse.js +0 -0
  177. package/src/runtime/codeburn-core.js +112 -0
  178. package/src/runtime/context-authority.js +40 -0
  179. package/src/runtime/context-economy-core.js +40 -0
  180. package/src/runtime/continuity-graph.js +201 -0
  181. package/src/runtime/continuity-notes.js +71 -26
  182. package/src/runtime/core-source-ingest.js +0 -0
  183. package/src/runtime/detached-provider.js +82 -14
  184. package/src/runtime/doctor-report.js +18 -0
  185. package/src/runtime/environment-awareness-core.js +0 -0
  186. package/src/runtime/exposure-catalog.js +0 -0
  187. package/src/runtime/graphify-core.js +167 -0
  188. package/src/runtime/guidance-stack.js +9 -2
  189. package/src/runtime/handoff-packets.js +92 -0
  190. package/src/runtime/headroom-core.js +104 -0
  191. package/src/runtime/helper-orchestration-core.js +0 -0
  192. package/src/runtime/hook-profiles.js +20 -0
  193. package/src/runtime/learning-loop-core.js +110 -4
  194. package/src/runtime/live-agent-verification.js +122 -0
  195. package/src/runtime/maestro-events.js +0 -0
  196. package/src/runtime/maestro-manager.js +0 -0
  197. package/src/runtime/maestro-output.js +58 -0
  198. package/src/runtime/maestro-state.js +0 -0
  199. package/src/runtime/managed-markdown.js +29 -0
  200. package/src/runtime/mcp-authority.js +147 -0
  201. package/src/runtime/mcp-policy.js +0 -0
  202. package/src/runtime/obsidian-integration-core.js +255 -17
  203. package/src/runtime/obsidian-plugin-bridge.js +344 -38
  204. package/src/runtime/orchestrator.js +0 -0
  205. package/src/runtime/pi-maestro-core.js +89 -0
  206. package/src/runtime/preferences.js +79 -0
  207. package/src/runtime/project-context.js +636 -0
  208. package/src/runtime/prompt-strategy-core.js +0 -0
  209. package/src/runtime/quorum-review.js +0 -0
  210. package/src/runtime/ralph-execution-core.js +0 -0
  211. package/src/runtime/redaction-gateway.js +218 -19
  212. package/src/runtime/runtime-state.js +94 -2
  213. package/src/runtime/schema-migrations.js +162 -0
  214. package/src/runtime/semantic-recording-core.js +0 -0
  215. package/src/runtime/signal-hooks.js +13 -0
  216. package/src/runtime/skills-registry-export.js +92 -8
  217. package/src/runtime/startup-path.js +0 -0
  218. package/src/runtime/task-contracts.js +83 -0
  219. package/src/runtime/universal-plugin-broker-core.js +373 -50
  220. package/src/runtime/workspace-intelligence-runtime.js +0 -0
  221. package/src/runtime/workspace-virtualizer.js +0 -0
  222. package/src/runtime/workspaces.js +0 -0
  223. package/src/runtime-artifacts.js +0 -0
  224. package/src/setup-lifecycle.js +359 -0
  225. package/src/skill-frontmatter.js +78 -0
  226. package/src/skill-installer.js +226 -71
  227. package/src/skills.js +297 -19
  228. package/src/state-sync.js +0 -0
  229. package/src/test-fixtures.js +543 -0
  230. package/src/tui/grid.js +67 -0
  231. package/src/tui/status-grid.js +23 -0
  232. package/support-bundle.json +114 -0
  233. package/templates/AGENTS.md +6 -2
  234. package/templates/catalog-manifest.json +0 -0
  235. package/templates/model-instructions/core.md +0 -0
  236. package/templates/model-instructions/release.md +0 -0
  237. package/templates/model-instructions/security.md +0 -0
  238. package/templates/quality/ai-quality-rules.yml +37 -0
  239. package/docs/assets/image/Screenshot(1).png +0 -0
  240. package/docs/assets/image/Screenshot(2).png +0 -0
  241. package/docs/assets/image/Screenshot(3).png +0 -0
  242. package/docs/assets/image/Screenshot(4).png +0 -0
  243. package/docs/assets/image/Screenshot(5).png +0 -0
  244. package/docs/assets/image/Screenshot(6).png +0 -0
  245. package/docs/assets/image/Screenshot(7).png +0 -0
  246. package/docs/assets/image/Screenshot(8).png +0 -0
  247. package/docs/assets/image/Screenshot(9).png +0 -0
  248. package/docs/assets/meta-architect-logo.png +0 -0
  249. package/docs/assets/meta-architect-logo.svg +0 -8
  250. package/docs/onboarding.md +0 -81
  251. package/docs/prompt-guidance-contract.md +0 -17
  252. package/docs/prompt-guidance-fragments/active-autonomy-core.md +0 -7
  253. package/docs/skills-publishing.md +0 -255
@@ -10,8 +10,37 @@ export const obsidianPluginQueuePath = "Meta-Architect/Plugin Requests/request-q
10
10
  export const obsidianPluginActiveContextPath =
11
11
  "Meta-Architect/Plugin Context/Active Note Context.md";
12
12
  export const obsidianPluginAttachmentDir = "Meta-Architect/Attachments";
13
+ const defaultAttachmentMaxBytes = 1_048_576;
14
+ const defaultAttachmentContentTypes = new Set([
15
+ "application/octet-stream",
16
+ "application/json",
17
+ "text/plain",
18
+ "text/markdown",
19
+ "image/png",
20
+ "image/jpeg",
21
+ "image/webp",
22
+ ]);
13
23
 
14
24
  const forbiddenMaTargets = [".ma/release.json", ".ma/decisions.json", ".ma/plans/", ".ma/specs/"];
25
+ const protocolMaxPayloadBytes = 64 * 1024;
26
+ const protocolActions = new Set(["queue", "capture", "open"]);
27
+ const protocolFields = new Set([
28
+ "action",
29
+ "open",
30
+ "path",
31
+ "note_path",
32
+ "from",
33
+ "to",
34
+ "content",
35
+ "data",
36
+ "content_type",
37
+ "overwrite",
38
+ "id",
39
+ "authority",
40
+ "records_as",
41
+ "protocol_token",
42
+ "protocol_token_hash",
43
+ ]);
15
44
 
16
45
  const pluginCapabilityNames = [
17
46
  "in_app_plugin_surface",
@@ -59,6 +88,13 @@ export async function installObsidianPlugin({
59
88
  if (!vaultPath || typeof vaultPath !== "string") {
60
89
  throw new Error("Obsidian plugin install requires vaultPath");
61
90
  }
91
+ if (
92
+ typeof pluginId !== "string" ||
93
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(pluginId) ||
94
+ path.isAbsolute(pluginId)
95
+ ) {
96
+ throw new Error(`Invalid Obsidian plugin id: ${pluginId}`);
97
+ }
62
98
  const resolvedVaultPath = path.resolve(vaultPath);
63
99
  const vaultStat = await fs.stat(resolvedVaultPath);
64
100
  if (!vaultStat.isDirectory()) {
@@ -67,28 +103,73 @@ export async function installObsidianPlugin({
67
103
 
68
104
  const sourceDir = path.join(packageRoot, "plugins", "meta-architect", "obsidian");
69
105
  const targetDir = path.join(resolvedVaultPath, ".obsidian", "plugins", pluginId);
70
- await fs.mkdir(targetDir, { recursive: true });
71
- await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
72
-
73
- const manifest = JSON.parse(await fs.readFile(path.join(targetDir, "manifest.json"), "utf8"));
74
- if (manifest.id !== pluginId) {
75
- throw new Error(`Installed Obsidian plugin manifest id mismatch: ${manifest.id}`);
106
+ const relativeTarget = path.relative(resolvedVaultPath, path.resolve(targetDir));
107
+ if (relativeTarget.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTarget)) {
108
+ throw new Error("Obsidian plugin target must remain inside the vault");
76
109
  }
77
- const enabledPlugins = enable
78
- ? await enableObsidianCommunityPlugin({ vaultPath: resolvedVaultPath, pluginId })
79
- : null;
110
+ await assertNoSymlinkComponents(resolvedVaultPath, targetDir);
111
+ await assertNoSymlinkComponents(
112
+ resolvedVaultPath,
113
+ path.join(resolvedVaultPath, ".obsidian", "community-plugins.json"),
114
+ );
115
+ const sourceMainPath = path.join(sourceDir, "main.js");
116
+ const sourceManifestPath = path.join(sourceDir, "manifest.json");
117
+ const configPath = path.join(resolvedVaultPath, ".obsidian", "community-plugins.json");
118
+ const originalConfig = await readOptionalFile(configPath);
119
+ if (enable) parseCommunityPlugins(originalConfig);
120
+ const stagingDir = `${targetDir}.staging-${Date.now()}`;
121
+ const backupDir = `${targetDir}.backup-${Date.now()}`;
122
+ let hadTarget = false;
123
+ let installed = false;
124
+ try {
125
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
126
+ await fs.cp(sourceDir, stagingDir, { recursive: true, force: true });
80
127
 
81
- return {
82
- record_type: "obsidian_plugin_install",
83
- records_as: "vault_context",
84
- build_evidence: false,
85
- enabled: enable,
86
- community_plugins: enabledPlugins,
87
- plugin_id: pluginId,
88
- vault_path: resolvedVaultPath,
89
- plugin_path: targetDir,
90
- installed_files: await listPluginFiles(targetDir),
91
- };
128
+ const manifest = JSON.parse(await fs.readFile(path.join(stagingDir, "manifest.json"), "utf8"));
129
+ if (manifest.id !== pluginId) {
130
+ throw new Error(`Installed Obsidian plugin manifest id mismatch: ${manifest.id}`);
131
+ }
132
+ const sourceHash = await hashFile(sourceMainPath);
133
+ const installedHash = await hashFile(path.join(stagingDir, "main.js"));
134
+ const sourceManifestHash = await hashFile(sourceManifestPath);
135
+ const installedManifestHash = await hashFile(path.join(stagingDir, "manifest.json"));
136
+ if (sourceHash !== installedHash || sourceManifestHash !== installedManifestHash) {
137
+ throw new Error("Installed Obsidian plugin integrity mismatch");
138
+ }
139
+
140
+ hadTarget = await pathExists(targetDir);
141
+ if (hadTarget) await fs.rename(targetDir, backupDir);
142
+ await fs.rename(stagingDir, targetDir);
143
+ installed = true;
144
+ const enabledPlugins = enable
145
+ ? await enableObsidianCommunityPlugin({ vaultPath: resolvedVaultPath, pluginId })
146
+ : null;
147
+
148
+ const installedFiles = await listPluginFiles(targetDir);
149
+ await fs.rm(backupDir, { recursive: true, force: true });
150
+ return {
151
+ record_type: "obsidian_plugin_install",
152
+ records_as: "vault_context",
153
+ build_evidence: false,
154
+ enabled: enable,
155
+ community_plugins: enabledPlugins,
156
+ plugin_id: pluginId,
157
+ vault_path: resolvedVaultPath,
158
+ plugin_path: targetDir,
159
+ source_hash: sourceHash,
160
+ installed_hash: installedHash,
161
+ source_manifest_hash: sourceManifestHash,
162
+ installed_manifest_hash: installedManifestHash,
163
+ installed_files: installedFiles,
164
+ };
165
+ } catch (error) {
166
+ await fs.rm(stagingDir, { recursive: true, force: true });
167
+ if (installed) await fs.rm(targetDir, { recursive: true, force: true });
168
+ if (hadTarget) await fs.rename(backupDir, targetDir);
169
+ if (originalConfig === null) await fs.rm(configPath, { force: true });
170
+ else await fs.writeFile(configPath, originalConfig, "utf8");
171
+ throw error;
172
+ }
92
173
  }
93
174
 
94
175
  export async function registerMetaArchitectObsidianPluginRuntime({
@@ -261,6 +342,7 @@ export async function applyObsidianFrontmatterAuthority({
261
342
  }
262
343
 
263
344
  export async function renameObsidianFileSafely({ app, file, newPath } = {}) {
345
+ assertSafeVaultPath(file?.path);
264
346
  assertSafeVaultPath(newPath);
265
347
  if (!app?.fileManager?.renameFile) {
266
348
  throw new Error("Obsidian FileManager.renameFile is required for link-safe rename/move");
@@ -302,17 +384,28 @@ export function registerObsidianProtocolHandlers({
302
384
  return { registered: false, handler: "ma", reason: "protocol_handler_unavailable" };
303
385
  }
304
386
  plugin.registerObsidianProtocolHandler("ma", async (params = {}) => {
305
- if (params.action === "queue") {
306
- await writeQueueRequestToVault({ app, request: params });
307
- return;
387
+ const request = validateProtocolRequest(params);
388
+ if (request.action === "queue") {
389
+ if (!hasProtocolAuthority(params, settings)) {
390
+ throw new Error("obsidian_protocol_queue_authority_required");
391
+ }
392
+ return writeQueueRequestToVault({ app, request, settings });
308
393
  }
309
- if (params.action === "capture" || params.open === "active") {
394
+ if (request.action === "capture") {
395
+ if (!hasProtocolAuthority(request, settings)) {
396
+ throw new Error("obsidian_protocol_authority_required");
397
+ }
310
398
  await captureActiveNoteToVault({ app, MarkdownView, settings });
311
- return;
399
+ return protocolReceipt("capture", request);
400
+ }
401
+ if (!hasProtocolAuthority(request, settings)) {
402
+ throw new Error("obsidian_protocol_authority_required");
312
403
  }
313
- const target = params.path ?? "Meta-Architect/Map of Content.md";
404
+ const target = request.path ?? "Meta-Architect/Map of Content.md";
405
+ assertSafeVaultPath(target);
314
406
  const file = app?.vault?.getAbstractFileByPath?.(target);
315
407
  if (file) await app?.workspace?.getLeaf?.(true)?.openFile?.(file);
408
+ return protocolReceipt("open", request);
316
409
  });
317
410
  return { registered: true, handler: "ma", supports: ["open", "queue", "capture"] };
318
411
  }
@@ -398,18 +491,39 @@ export async function writeObsidianAttachment({
398
491
  attachmentPath,
399
492
  data,
400
493
  contentType = "application/octet-stream",
494
+ overwrite = false,
495
+ maxBytes = defaultAttachmentMaxBytes,
496
+ allowedContentTypes = defaultAttachmentContentTypes,
497
+ requestId = null,
498
+ sourceAction = null,
401
499
  } = {}) {
402
500
  assertSafeVaultPath(attachmentPath);
501
+ assertMaOwnedPath(attachmentPath, "Meta-Architect/Attachments/");
403
502
  if (!attachmentPath) {
404
503
  throw new Error("Obsidian attachment write requires attachmentPath");
405
504
  }
505
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
506
+ throw new Error("attachment_max_bytes_invalid");
507
+ }
406
508
  const bytes = toUint8Array(data);
509
+ if (bytes.byteLength > maxBytes) {
510
+ throw new Error(`attachment_too_large:${attachmentPath}:${bytes.byteLength}`);
511
+ }
512
+ if (!isAllowedAttachmentContentType(contentType, allowedContentTypes)) {
513
+ throw new Error(`attachment_type_forbidden:${contentType}`);
514
+ }
407
515
  const parent = attachmentPath.includes("/")
408
516
  ? attachmentPath.slice(0, attachmentPath.lastIndexOf("/"))
409
517
  : "";
410
518
  if (parent) await ensureVaultFolder(app, parent);
519
+ const existing = app?.vault?.getAbstractFileByPath?.(attachmentPath);
520
+ if (existing && !overwrite) {
521
+ throw new Error(`attachment_exists:${attachmentPath}`);
522
+ }
411
523
 
412
- if (app?.vault?.createBinary) {
524
+ if (existing && app?.vault?.adapter?.writeBinary) {
525
+ await app.vault.adapter.writeBinary(attachmentPath, bytes);
526
+ } else if (app?.vault?.createBinary) {
413
527
  await app.vault.createBinary(
414
528
  attachmentPath,
415
529
  bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
@@ -428,6 +542,8 @@ export async function writeObsidianAttachment({
428
542
  content_type: contentType,
429
543
  byte_length: bytes.byteLength,
430
544
  sha256: createHash("sha256").update(bytes).digest("hex"),
545
+ request_id: requestId,
546
+ source_action: sourceAction,
431
547
  };
432
548
  }
433
549
 
@@ -442,12 +558,21 @@ export async function drainObsidianPluginRequestQueue({
442
558
  }
443
559
  const processed = [];
444
560
  const refused = [];
561
+ const refusedQueueItems = [];
445
562
 
446
563
  for (const request of queue) {
447
564
  try {
448
565
  if (!request || typeof request !== "object") {
449
566
  throw new Error("invalid_request");
450
567
  }
568
+ if (
569
+ ["capture_active_note", "create_note", "rename_note", "write_attachment"].includes(
570
+ request.action,
571
+ ) &&
572
+ !hasProtocolAuthority(request, settings)
573
+ ) {
574
+ throw new Error("obsidian_queue_authority_required");
575
+ }
451
576
  const target = request.note_path ?? request.path ?? "";
452
577
  if (target) assertSafeVaultPath(target);
453
578
  switch (request.action) {
@@ -496,6 +621,11 @@ export async function drainObsidianPluginRequestQueue({
496
621
  attachmentPath: target,
497
622
  data: request.data ?? "",
498
623
  contentType: request.content_type,
624
+ overwrite: request.overwrite ?? false,
625
+ maxBytes: defaultAttachmentMaxBytes,
626
+ allowedContentTypes: defaultAttachmentContentTypes,
627
+ requestId: request.id ?? null,
628
+ sourceAction: request.action,
499
629
  }),
500
630
  });
501
631
  break;
@@ -503,10 +633,19 @@ export async function drainObsidianPluginRequestQueue({
503
633
  throw new Error(`unsupported_action:${request.action}`);
504
634
  }
505
635
  } catch (error) {
636
+ const reason = error instanceof Error ? error.message : String(error);
637
+ const previousAttempts = Number.isInteger(request?.attempts) ? request.attempts : 0;
638
+ refusedQueueItems.push({
639
+ ...safeClone(request),
640
+ queue_status: "refused",
641
+ refusal_reason: reason,
642
+ attempts: previousAttempts + 1,
643
+ last_attempt_at: new Date().toISOString(),
644
+ });
506
645
  refused.push({
507
646
  request_id: request?.id ?? null,
508
647
  action: request?.action ?? null,
509
- reason: error instanceof Error ? error.message : String(error),
648
+ reason,
510
649
  records_as: "vault_context",
511
650
  build_evidence: false,
512
651
  });
@@ -520,6 +659,7 @@ export async function drainObsidianPluginRequestQueue({
520
659
  drained_at: new Date().toISOString(),
521
660
  processed,
522
661
  refused,
662
+ refusedQueueItems,
523
663
  };
524
664
  }
525
665
 
@@ -532,7 +672,7 @@ async function captureActiveNoteToVault({ app, Notice, MarkdownView, settings =
532
672
  file: activeFile,
533
673
  project: settings.projectName ?? "Meta-Architect",
534
674
  sourceWorkspace: settings.sourceWorkspace ?? null,
535
- }).catch(() => {});
675
+ });
536
676
  }
537
677
  const content = renderActiveContextNote(context);
538
678
  const result = await createVaultContextNote({
@@ -547,7 +687,22 @@ async function captureActiveNoteToVault({ app, Notice, MarkdownView, settings =
547
687
  return { ...result, active_context: context };
548
688
  }
549
689
 
550
- async function drainQueueFromVault({ app, MarkdownView, settings = {} }) {
690
+ let obsidianQueueBusy = Promise.resolve();
691
+
692
+ function withObsidianQueueLock(task) {
693
+ const previous = obsidianQueueBusy;
694
+ let release;
695
+ obsidianQueueBusy = new Promise((resolve) => {
696
+ release = resolve;
697
+ });
698
+ return previous.then(task).finally(release);
699
+ }
700
+
701
+ async function drainQueueFromVault(args) {
702
+ return withObsidianQueueLock(() => drainQueueFromVaultUnlocked(args));
703
+ }
704
+
705
+ async function drainQueueFromVaultUnlocked({ app, MarkdownView, settings = {} }) {
551
706
  const queuePath = settings.queuePath ?? obsidianPluginQueuePath;
552
707
  const file = app?.vault?.getAbstractFileByPath?.(queuePath);
553
708
  if (!file) {
@@ -556,28 +711,42 @@ async function drainQueueFromVault({ app, MarkdownView, settings = {} }) {
556
711
  const raw = await app.vault.read(file);
557
712
  const queue = JSON.parse(raw);
558
713
  const result = await drainObsidianPluginRequestQueue({ app, queue, MarkdownView, settings });
559
- await app.vault.modify(file, "[]\n");
714
+ const nextQueue = `${JSON.stringify(result.refusedQueueItems, null, 2)}\n`;
715
+ if (raw !== nextQueue) await app.vault.modify(file, nextQueue);
560
716
  return result;
561
717
  }
562
718
 
563
- async function writeQueueRequestToVault({ app, request }) {
719
+ async function writeQueueRequestToVault(args) {
720
+ return withObsidianQueueLock(() => writeQueueRequestToVaultUnlocked(args));
721
+ }
722
+
723
+ async function writeQueueRequestToVaultUnlocked({ app, request, settings = {} }) {
724
+ if (!hasProtocolAuthority(request, settings)) {
725
+ throw new Error("obsidian_protocol_queue_authority_required");
726
+ }
564
727
  const queuePath = obsidianPluginQueuePath;
565
728
  await ensureVaultFolder(app, path.dirname(queuePath));
566
729
  const file = app?.vault?.getAbstractFileByPath?.(queuePath);
567
730
  const existing = file ? JSON.parse(await app.vault.read(file)) : [];
731
+ const queuedRequest = Object.fromEntries(
732
+ Object.entries(request).filter(([key]) => key !== "protocol_token"),
733
+ );
568
734
  existing.push({
569
- ...request,
735
+ ...queuedRequest,
570
736
  id: request.id ?? `ma-${Date.now()}`,
571
737
  queued_at: new Date().toISOString(),
572
738
  records_as: "vault_context",
739
+ protocol_token_hash: request.protocol_token_hash ?? tokenHash(request.protocol_token),
573
740
  });
574
741
  const content = `${JSON.stringify(existing, null, 2)}\n`;
575
742
  if (file) await app.vault.modify(file, content);
576
743
  else await app.vault.create(queuePath, content);
744
+ return protocolReceipt("queue", request);
577
745
  }
578
746
 
579
747
  async function createVaultContextNote({ app, notePath, content, overwrite = true }) {
580
748
  assertSafeVaultPath(notePath);
749
+ assertMaOwnedPath(notePath, "Meta-Architect/");
581
750
  if (typeof content !== "string" || content.trim().length === 0) {
582
751
  throw new Error("vault_context note content required");
583
752
  }
@@ -657,6 +826,9 @@ function assertSafeVaultPath(targetPath) {
657
826
  if (normalized.includes("../") || normalized === ".." || normalized.startsWith("..")) {
658
827
  throw new Error("path_traversal_forbidden");
659
828
  }
829
+ if ([".obsidian/", ".trash/", ".git/"].some((prefix) => normalized.startsWith(prefix))) {
830
+ throw new Error("vault_control_path_forbidden");
831
+ }
660
832
  for (const forbidden of forbiddenMaTargets) {
661
833
  if (normalized === forbidden || normalized.startsWith(forbidden)) {
662
834
  throw new Error(`forbidden_authoritative_mutation:${forbidden}`);
@@ -664,6 +836,118 @@ function assertSafeVaultPath(targetPath) {
664
836
  }
665
837
  }
666
838
 
839
+ function assertMaOwnedPath(targetPath, prefix) {
840
+ const normalized = targetPath.replaceAll("\\", "/").replace(/^\/+/, "");
841
+ if (!normalized.startsWith(prefix)) throw new Error(`ma_owned_path_required:${prefix}`);
842
+ }
843
+
844
+ function hasVaultContextAuthority(params, settings) {
845
+ const requiredAuthority = settings.protocolAuthority ?? "vault_context";
846
+ return params.authority === requiredAuthority;
847
+ }
848
+
849
+ function hasProtocolAuthority(params, settings) {
850
+ return (
851
+ Boolean(settings.protocolToken) &&
852
+ (params?.protocol_token === settings.protocolToken ||
853
+ params?.protocol_token_hash === tokenHash(settings.protocolToken)) &&
854
+ hasVaultContextAuthority(params, settings)
855
+ );
856
+ }
857
+
858
+ function validateProtocolRequest(params) {
859
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
860
+ throw new Error("obsidian_protocol_request_invalid");
861
+ }
862
+ const serialized = JSON.stringify(params);
863
+ if (Buffer.byteLength(serialized, "utf8") > protocolMaxPayloadBytes) {
864
+ throw new Error("obsidian_protocol_payload_too_large");
865
+ }
866
+ for (const key of Object.keys(params)) {
867
+ if (!protocolFields.has(key)) throw new Error(`obsidian_protocol_field_forbidden:${key}`);
868
+ }
869
+ const action = params.action ?? (params.open === "active" ? "capture" : null);
870
+ if (!protocolActions.has(action)) throw new Error("obsidian_protocol_action_forbidden");
871
+ if (action === "open" && params.open === "active") {
872
+ throw new Error("obsidian_protocol_action_invalid");
873
+ }
874
+ const request = { ...params, action };
875
+ for (const target of [request.path, request.note_path, request.from, request.to]) {
876
+ if (target !== undefined) assertSafeVaultPath(target);
877
+ }
878
+ if (request.content !== undefined && typeof request.content !== "string") {
879
+ throw new Error("obsidian_protocol_content_invalid");
880
+ }
881
+ if (request.data !== undefined && typeof request.data !== "string") {
882
+ throw new Error("obsidian_protocol_data_invalid");
883
+ }
884
+ return request;
885
+ }
886
+
887
+ function protocolReceipt(action, request) {
888
+ return {
889
+ record_type: "obsidian_protocol_action",
890
+ records_as: "vault_context",
891
+ build_evidence: false,
892
+ action,
893
+ request_id: request.id ?? null,
894
+ status: "accepted",
895
+ authority: request.authority,
896
+ };
897
+ }
898
+
899
+ function tokenHash(token) {
900
+ return createHash("sha256")
901
+ .update(String(token ?? ""))
902
+ .digest("hex");
903
+ }
904
+
905
+ async function hashFile(filePath) {
906
+ return createHash("sha256")
907
+ .update(await fs.readFile(filePath))
908
+ .digest("hex");
909
+ }
910
+
911
+ async function assertNoSymlinkComponents(root, target) {
912
+ const rootPath = path.resolve(root);
913
+ const rootStat = await fs.lstat(rootPath);
914
+ if (rootStat.isSymbolicLink()) throw new Error("Obsidian vault root cannot be a symlink");
915
+ let current = rootPath;
916
+ for (const part of path
917
+ .relative(rootPath, path.resolve(target))
918
+ .split(path.sep)
919
+ .filter(Boolean)) {
920
+ current = path.join(current, part);
921
+ const stat = await fs.lstat(current).catch((error) => {
922
+ if (error?.code === "ENOENT") return null;
923
+ throw error;
924
+ });
925
+ if (stat?.isSymbolicLink())
926
+ throw new Error(`Obsidian plugin path cannot traverse a symlink: ${current}`);
927
+ }
928
+ }
929
+
930
+ function isAllowedAttachmentContentType(contentType, allowedContentTypes) {
931
+ if (!contentType || typeof contentType !== "string") {
932
+ return false;
933
+ }
934
+ if (allowedContentTypes instanceof Set) {
935
+ return allowedContentTypes.has(contentType);
936
+ }
937
+ if (Array.isArray(allowedContentTypes)) {
938
+ return allowedContentTypes.includes(contentType);
939
+ }
940
+ return defaultAttachmentContentTypes.has(contentType);
941
+ }
942
+
943
+ function safeClone(value) {
944
+ try {
945
+ return structuredClone(value);
946
+ } catch {
947
+ return JSON.parse(JSON.stringify(value ?? null));
948
+ }
949
+ }
950
+
667
951
  function toUint8Array(data) {
668
952
  if (data instanceof Uint8Array) return data;
669
953
  if (data instanceof ArrayBuffer) return new Uint8Array(data);
@@ -707,11 +991,8 @@ async function listPluginFiles(root) {
707
991
 
708
992
  async function enableObsidianCommunityPlugin({ vaultPath, pluginId }) {
709
993
  const configPath = path.join(vaultPath, ".obsidian", "community-plugins.json");
710
- let enabled = [];
711
- try {
712
- const parsed = JSON.parse(await fs.readFile(configPath, "utf8"));
713
- if (Array.isArray(parsed)) enabled = parsed.filter((entry) => typeof entry === "string");
714
- } catch {}
994
+ await assertNoSymlinkComponents(vaultPath, configPath);
995
+ const enabled = parseCommunityPlugins(await readOptionalFile(configPath));
715
996
  if (!enabled.includes(pluginId)) {
716
997
  enabled.push(pluginId);
717
998
  }
@@ -719,6 +1000,31 @@ async function enableObsidianCommunityPlugin({ vaultPath, pluginId }) {
719
1000
  return enabled;
720
1001
  }
721
1002
 
1003
+ async function readOptionalFile(filePath) {
1004
+ try {
1005
+ return await fs.readFile(filePath, "utf8");
1006
+ } catch (error) {
1007
+ if (error?.code === "ENOENT") return null;
1008
+ throw error;
1009
+ }
1010
+ }
1011
+
1012
+ async function pathExists(filePath) {
1013
+ try {
1014
+ await fs.lstat(filePath);
1015
+ return true;
1016
+ } catch (error) {
1017
+ if (error?.code === "ENOENT") return false;
1018
+ throw error;
1019
+ }
1020
+ }
1021
+
1022
+ function parseCommunityPlugins(content) {
1023
+ if (content === null) return [];
1024
+ const parsed = JSON.parse(content);
1025
+ return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
1026
+ }
1027
+
722
1028
  function createMetaArchitectSettingTab(app, plugin, PluginSettingTab, Setting) {
723
1029
  const instance = new PluginSettingTab(app, plugin);
724
1030
  instance.display = () => {
File without changes
@@ -0,0 +1,89 @@
1
+ export function isPiMaestroEnabled(env = process.env) {
2
+ return env.MA_MAESTRO_PI === "1";
3
+ }
4
+
5
+ export function assertPiMaestroToolAllowed(
6
+ name,
7
+ { controlPlaneReady = true, buildReadiness } = {},
8
+ ) {
9
+ if (!controlPlaneReady) throw new Error(`${name} blocked by control-plane readiness`);
10
+ if (name === "$build" && buildReadiness && buildReadiness.allowed !== true) {
11
+ throw new Error(
12
+ `$build blocked: ${(buildReadiness.blockers ?? []).join("; ") || "readiness failed"}`,
13
+ );
14
+ }
15
+ }
16
+
17
+ export function createPiMaestroToolSurface({
18
+ runners = {},
19
+ controlPlaneReady = true,
20
+ buildReadiness,
21
+ beforeToolCall,
22
+ afterToolCall,
23
+ isWaitingReview = () => false,
24
+ } = {}) {
25
+ const surface = {
26
+ tools: {},
27
+ async dispatch(name, input = {}) {
28
+ if (!runners[name]) throw new Error(`Unknown pi-maestro tool: ${name}`);
29
+ if (isWaitingReview()) return { terminate: true, status: "WAITING_REVIEW" };
30
+ assertPiMaestroToolAllowed(name, { controlPlaneReady, buildReadiness });
31
+ await beforeToolCall?.(name, input);
32
+ const result = await runners[name](input);
33
+ await afterToolCall?.(name, result);
34
+ return { terminate: false, result };
35
+ },
36
+ };
37
+ surface.tools = Object.fromEntries(
38
+ Object.keys(runners).map((name) => [
39
+ name,
40
+ {
41
+ name,
42
+ description: `Run the bounded ${name} Meta-Architect lane`,
43
+ execute: (input) => surface.dispatch(name, input),
44
+ },
45
+ ]),
46
+ );
47
+ return surface;
48
+ }
49
+
50
+ export const maestroGateLanes = ["$arch", "$sage", "$flow", "$vet", "$vibe", "$build"];
51
+
52
+ export function createPiMaestroCapabilityMapping({
53
+ runners = {},
54
+ enabled = isPiMaestroEnabled(),
55
+ ...options
56
+ } = {}) {
57
+ const laneRunners = Object.fromEntries(
58
+ maestroGateLanes
59
+ .filter((lane) => typeof runners[lane] === "function")
60
+ .map((lane) => [lane, runners[lane]]),
61
+ );
62
+ return {
63
+ enabled,
64
+ controlModel: "maestro-pi",
65
+ umbrella: "$maestro",
66
+ lanes: maestroGateLanes.map((id) => ({
67
+ id,
68
+ runner: laneRunners[id] ? "maestro" : "unavailable",
69
+ gated: id === "$build" || id === "$vet",
70
+ })),
71
+ surface: createPiMaestroToolSurface({ runners: laneRunners, ...options }),
72
+ };
73
+ }
74
+
75
+ export async function runPiMaestroExperimental({
76
+ agent,
77
+ tools,
78
+ beforeToolCall,
79
+ afterToolCall,
80
+ } = {}) {
81
+ if (!agent || typeof agent.run !== "function") {
82
+ return {
83
+ handled: false,
84
+ reason: "Maestro Pi control runtime is not installed or was not provided",
85
+ };
86
+ }
87
+ const result = await agent.run({ tools, beforeToolCall, afterToolCall });
88
+ return { handled: true, result };
89
+ }