@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,1089 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { ensureDir, readJson, writeFileIfMissing, writeJson } from "../fs-utils.js";
5
+ import { getRuntimeSubsystemPath } from "../paths.js";
6
+ import { createManagedMarkdownBlock } from "./managed-markdown.js";
7
+ import { createDefaultObsidianPluginBridgeManifest } from "./obsidian-plugin-bridge.js";
8
+
9
+ export const obsidianIntegrationSchemaVersion = "0.1.0";
10
+ export const obsidianGraphMapNotePath = "Meta-Architect/Map of Content.md";
11
+ const obsidianConfigPath = () => getRuntimeSubsystemPath("obsidian", "config.json");
12
+ const obsidianGraphSectionHeading = "## Obsidian Graph Links";
13
+
14
+ export async function configureObsidianVault(vaultPath) {
15
+ const resolved = path.resolve(vaultPath ?? "");
16
+ const stat = await fs.stat(resolved).catch(() => null);
17
+ if (!stat?.isDirectory()) throw new Error(`Invalid Obsidian vault: ${resolved}`);
18
+ const config = {
19
+ schemaVersion: obsidianIntegrationSchemaVersion,
20
+ vaultPath: resolved,
21
+ updatedAt: new Date().toISOString(),
22
+ };
23
+ await writeJson(obsidianConfigPath(), config);
24
+ return config;
25
+ }
26
+
27
+ export async function loadObsidianVaultConfig() {
28
+ return readJson(obsidianConfigPath()).catch(() => null);
29
+ }
30
+
31
+ export async function resolveObsidianVault(vaultPath) {
32
+ const configured =
33
+ vaultPath || process.env.MA_OBSIDIAN_VAULT || (await loadObsidianVaultConfig())?.vaultPath;
34
+ if (!configured)
35
+ throw new Error("Obsidian vault path required; run `ma obsidian configure <vault-path>`");
36
+ const resolved = path.resolve(configured);
37
+ const stat = await fs.stat(resolved).catch(() => null);
38
+ if (!stat?.isDirectory()) throw new Error(`Invalid Obsidian vault: ${resolved}`);
39
+ return fs.realpath(resolved);
40
+ }
41
+ const allowedObsidianPluginActions = new Set([
42
+ "capture_active_note",
43
+ "create_note",
44
+ "index_vault",
45
+ "rename_note",
46
+ "select-notes",
47
+ "write_attachment",
48
+ ]);
49
+
50
+ export function getObsidianBridgePath() {
51
+ return getRuntimeSubsystemPath("context", "obsidian-bridge.json");
52
+ }
53
+
54
+ export function getObsidianVaultIndexPath() {
55
+ return getRuntimeSubsystemPath("context", "obsidian-vault-index.json");
56
+ }
57
+
58
+ export function getObsidianVaultOperationsPath() {
59
+ return getRuntimeSubsystemPath("context", "obsidian-vault-operations.json");
60
+ }
61
+
62
+ export function createDefaultObsidianBridge() {
63
+ return {
64
+ schemaVersion: obsidianIntegrationSchemaVersion,
65
+ product: "Meta-Architect",
66
+ purpose:
67
+ "Defines the default MA-owned Obsidian bridge for vault context, plugin requests, and note-selection metadata.",
68
+ compatibility_references: {
69
+ api_docs: "https://gitmcp.io/obsidianmd/obsidian-api",
70
+ sample_plugin_structure: "https://github.com/obsidianmd/obsidian-sample-plugin",
71
+ },
72
+ semantic_boundary: {
73
+ semantic_role: "brain_context",
74
+ records_as: "vault_context",
75
+ never_records_as: "build_evidence",
76
+ },
77
+ graph_link_policy: {
78
+ enabled_by_default: true,
79
+ semantic_symlink_type: "obsidian_wikilink",
80
+ map_note_path: obsidianGraphMapNotePath,
81
+ every_ma_note_links_to_map: true,
82
+ map_links_all_ma_notes: true,
83
+ records_as: "vault_context",
84
+ build_evidence: false,
85
+ },
86
+ plugin_contract: {
87
+ may_queue_requests: true,
88
+ may_expose_note_selection_metadata: true,
89
+ allowed_vault_operations: [
90
+ "create_note",
91
+ "read_note",
92
+ "update_note",
93
+ "delete_note",
94
+ "list_notes",
95
+ ],
96
+ must_not_mutate: [".ma/release.json", ".ma/decisions.json", ".ma/plans/", ".ma/specs/"],
97
+ authoritative_changes_return_through: "$maestro_or_owning_lane",
98
+ },
99
+ plugin_runtime: createDefaultObsidianPluginBridgeManifest(),
100
+ default_request_queue: [],
101
+ note_selection_metadata: [],
102
+ tag_graph_claims: [],
103
+ };
104
+ }
105
+
106
+ export function createDefaultObsidianVaultIndex() {
107
+ return {
108
+ schemaVersion: obsidianIntegrationSchemaVersion,
109
+ record_type: "obsidian_vault_index",
110
+ records_as: "vault_context",
111
+ build_evidence: false,
112
+ read_only_snapshot: true,
113
+ vault_path: null,
114
+ indexed_at: null,
115
+ note_count: 0,
116
+ total_words: 0,
117
+ total_chars: 0,
118
+ tags: [],
119
+ unresolved_links: [],
120
+ notes: [],
121
+ tag_graph_claims: [],
122
+ };
123
+ }
124
+
125
+ export function createDefaultObsidianVaultOperations() {
126
+ return {
127
+ schemaVersion: obsidianIntegrationSchemaVersion,
128
+ record_type: "obsidian_vault_operations",
129
+ records_as: "vault_context",
130
+ build_evidence: false,
131
+ authority_boundary: "$maestro_or_owning_lane",
132
+ operations: [],
133
+ };
134
+ }
135
+
136
+ export function createObsidianPluginRequest({
137
+ action,
138
+ notePaths = [],
139
+ tags = [],
140
+ reason = "operator_selected_context",
141
+ }) {
142
+ if (!action || typeof action !== "string") {
143
+ throw new Error("Obsidian plugin request requires an action");
144
+ }
145
+ if (!allowedObsidianPluginActions.has(action)) {
146
+ throw new Error(`Obsidian plugin request action is not allowed: ${action}`);
147
+ }
148
+
149
+ return {
150
+ record_type: "obsidian_plugin_request",
151
+ action,
152
+ reason,
153
+ records_as: "vault_context",
154
+ authoritative_change: false,
155
+ note_paths: notePaths.map(assertSafeQueuedPath),
156
+ tags: tags.map(assertSafeQueuedTag),
157
+ queued_at: new Date().toISOString(),
158
+ };
159
+ }
160
+
161
+ export function createObsidianVaultContext({ notePath, title, tags = [], claims = [] }) {
162
+ if (!notePath || typeof notePath !== "string") {
163
+ throw new Error("Obsidian vault context requires notePath");
164
+ }
165
+
166
+ return {
167
+ record_type: "vault_context",
168
+ note_path: notePath,
169
+ title: title || notePath,
170
+ tags: tags.filter((entry) => typeof entry === "string" && entry.trim()),
171
+ claims: claims.map((claim) => ({
172
+ ...claim,
173
+ records_as: "vault_context",
174
+ build_evidence: false,
175
+ })),
176
+ };
177
+ }
178
+
179
+ export function createObsidianVaultSnapshotExport({
180
+ artifactPath,
181
+ vaultPath,
182
+ notePath,
183
+ title,
184
+ tags = [],
185
+ provenance = [],
186
+ }) {
187
+ if (!artifactPath || !notePath) {
188
+ throw new Error("Obsidian vault snapshot export requires artifactPath and notePath");
189
+ }
190
+
191
+ return {
192
+ record_type: "obsidian_vault_snapshot_export",
193
+ records_as: "vault_context",
194
+ build_evidence: false,
195
+ read_only_snapshot: true,
196
+ artifact_path: artifactPath,
197
+ vault_path: vaultPath ?? null,
198
+ note_path: notePath,
199
+ title: title || notePath,
200
+ tags: tags.filter((entry) => typeof entry === "string" && entry.trim()),
201
+ provenance: provenance.filter((entry) => typeof entry === "string" && entry.trim()),
202
+ authoritative_change: false,
203
+ authority_boundary: "$maestro_or_owning_lane",
204
+ };
205
+ }
206
+
207
+ export function createObsidianIntakeContext({ allowedNotes = [], tagGraph = [] }) {
208
+ const notes = allowedNotes.map((note) =>
209
+ createObsidianVaultContext({
210
+ notePath: note.notePath,
211
+ title: note.title,
212
+ tags: note.tags,
213
+ claims: note.claims,
214
+ }),
215
+ );
216
+ return {
217
+ record_type: "obsidian_intake_context",
218
+ records_as: "vault_context",
219
+ build_evidence: false,
220
+ allowed_notes: notes,
221
+ tag_graph_claims: tagGraph.map((claim) => ({
222
+ ...claim,
223
+ records_as: "vault_context",
224
+ build_evidence: false,
225
+ })),
226
+ requires_operator_allowlist: true,
227
+ authoritative_change: false,
228
+ };
229
+ }
230
+
231
+ export async function indexObsidianVault({ vaultPath, maxExcerptChars = 360 } = {}) {
232
+ if (!vaultPath || typeof vaultPath !== "string") {
233
+ throw new Error("Obsidian vault indexing requires vaultPath");
234
+ }
235
+
236
+ const resolvedVaultPath = path.resolve(vaultPath);
237
+ const stat = await fs.stat(resolvedVaultPath);
238
+ if (!stat.isDirectory()) {
239
+ throw new Error(`Obsidian vault path is not a directory: ${resolvedVaultPath}`);
240
+ }
241
+
242
+ const notePaths = await listMarkdownNotes(resolvedVaultPath);
243
+ const notes = [];
244
+ const tagCounts = new Map();
245
+ const linkTargets = new Map();
246
+ let totalWords = 0;
247
+ let totalChars = 0;
248
+
249
+ for (const absolutePath of notePaths) {
250
+ const content = await fs.readFile(absolutePath, "utf8");
251
+ const relativePath = normalizeVaultPath(path.relative(resolvedVaultPath, absolutePath));
252
+ const tags = extractObsidianTags(content);
253
+ const links = extractObsidianLinks(content);
254
+ const title = extractTitle(content) ?? path.basename(relativePath, ".md");
255
+ const words = countWords(content);
256
+ totalWords += words;
257
+ totalChars += content.length;
258
+
259
+ for (const tag of tags) {
260
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
261
+ }
262
+ for (const link of links) {
263
+ const targetKey = normalizeLinkKey(link.target);
264
+ const refs = linkTargets.get(targetKey) ?? [];
265
+ refs.push(relativePath);
266
+ linkTargets.set(targetKey, refs);
267
+ }
268
+
269
+ notes.push({
270
+ record_type: "vault_context_note",
271
+ records_as: "vault_context",
272
+ build_evidence: false,
273
+ relative_path: relativePath,
274
+ title,
275
+ tags,
276
+ links,
277
+ word_count: words,
278
+ char_count: content.length,
279
+ content_sha256: createHash("sha256").update(content).digest("hex"),
280
+ excerpt: createExcerpt(content, maxExcerptChars),
281
+ });
282
+ }
283
+
284
+ const existingNoteKeys = new Set([
285
+ ...notes.map((note) => normalizeLinkKey(note.title)),
286
+ ...notes.map((note) => normalizeLinkKey(note.relative_path)),
287
+ ...notes.map((note) => normalizeLinkKey(note.relative_path.replace(/\.md$/i, ""))),
288
+ ]);
289
+ const unresolvedLinks = [...linkTargets.entries()]
290
+ .filter(([target]) => !existingNoteKeys.has(target))
291
+ .map(([target, sources]) => ({
292
+ target,
293
+ sources: [...new Set(sources)].sort(),
294
+ records_as: "vault_context",
295
+ build_evidence: false,
296
+ }))
297
+ .sort((a, b) => a.target.localeCompare(b.target));
298
+ const tags = [...tagCounts.entries()]
299
+ .map(([tag, count]) => ({ tag, count }))
300
+ .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
301
+ const tagGraphClaims = tags.map(({ tag, count }) => ({
302
+ tag,
303
+ claim: `Tag ${tag} appears in ${count} Obsidian note${count === 1 ? "" : "s"}.`,
304
+ records_as: "vault_context",
305
+ build_evidence: false,
306
+ }));
307
+
308
+ return validateObsidianVaultIndex({
309
+ schemaVersion: obsidianIntegrationSchemaVersion,
310
+ record_type: "obsidian_vault_index",
311
+ records_as: "vault_context",
312
+ build_evidence: false,
313
+ read_only_snapshot: true,
314
+ vault_path: resolvedVaultPath,
315
+ indexed_at: new Date().toISOString(),
316
+ note_count: notes.length,
317
+ total_words: totalWords,
318
+ total_chars: totalChars,
319
+ tags,
320
+ unresolved_links: unresolvedLinks,
321
+ notes,
322
+ tag_graph_claims: tagGraphClaims,
323
+ });
324
+ }
325
+
326
+ export async function listObsidianNotes({ vaultPath } = {}) {
327
+ const resolvedVaultPath = await resolveVaultRoot(vaultPath);
328
+ await configureObsidianVault(resolvedVaultPath);
329
+ const notePaths = await listMarkdownNotes(resolvedVaultPath);
330
+ return {
331
+ record_type: "obsidian_note_list",
332
+ records_as: "vault_context",
333
+ build_evidence: false,
334
+ vault_path: resolvedVaultPath,
335
+ notes: notePaths.map((absolutePath) =>
336
+ normalizeVaultPath(path.relative(resolvedVaultPath, absolutePath)),
337
+ ),
338
+ };
339
+ }
340
+
341
+ export async function readObsidianNote({ vaultPath, notePath } = {}) {
342
+ const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
343
+ const content = await fs.readFile(target.absolutePath, "utf8");
344
+ const stat = await fs.stat(target.absolutePath);
345
+ return {
346
+ record_type: "obsidian_note_read",
347
+ records_as: "vault_context",
348
+ build_evidence: false,
349
+ vault_path: target.vaultPath,
350
+ relative_path: target.relativePath,
351
+ title: extractTitle(content) ?? path.basename(target.relativePath, ".md"),
352
+ content,
353
+ content_sha256: createHash("sha256").update(content).digest("hex"),
354
+ char_count: content.length,
355
+ word_count: countWords(content),
356
+ updated_at: stat.mtime.toISOString(),
357
+ };
358
+ }
359
+
360
+ export async function createObsidianNote({ vaultPath, notePath, content, overwrite = false } = {}) {
361
+ if (typeof content !== "string" || content.trim().length === 0) {
362
+ throw new Error("Obsidian create requires non-empty note content");
363
+ }
364
+ const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: false });
365
+ if (!overwrite && (await pathExists(target.absolutePath))) {
366
+ throw new Error(`Obsidian note already exists: ${target.relativePath}`);
367
+ }
368
+ await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
369
+ await writeVaultNote(target.absolutePath, content, overwrite);
370
+ return createObsidianOperationReceipt({
371
+ operation: overwrite ? "update_note" : "create_note",
372
+ target,
373
+ content,
374
+ });
375
+ }
376
+
377
+ export async function updateObsidianNote({ vaultPath, notePath, content } = {}) {
378
+ if (typeof content !== "string" || content.trim().length === 0) {
379
+ throw new Error("Obsidian update requires non-empty note content");
380
+ }
381
+ const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
382
+ await writeVaultNote(target.absolutePath, content, true);
383
+ return createObsidianOperationReceipt({ operation: "update_note", target, content });
384
+ }
385
+
386
+ async function writeVaultNote(filePath, content, overwrite) {
387
+ const flags =
388
+ fs.constants.O_WRONLY |
389
+ fs.constants.O_CREAT |
390
+ (overwrite ? fs.constants.O_TRUNC : fs.constants.O_EXCL) |
391
+ (fs.constants.O_NOFOLLOW ?? 0);
392
+ const handle = await fs.open(filePath, flags, 0o600);
393
+ try {
394
+ await handle.writeFile(content, "utf8");
395
+ } finally {
396
+ await handle.close();
397
+ }
398
+ }
399
+
400
+ export async function deleteObsidianNote({ vaultPath, notePath } = {}) {
401
+ const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
402
+ const content = await fs.readFile(target.absolutePath, "utf8");
403
+ await fs.unlink(target.absolutePath);
404
+ return createObsidianOperationReceipt({
405
+ operation: "delete_note",
406
+ target,
407
+ content,
408
+ });
409
+ }
410
+
411
+ export async function ensureObsidianGraphLinks({ vaultPath } = {}) {
412
+ const resolvedVaultPath = await resolveVaultRoot(vaultPath);
413
+ const notePaths = await listMarkdownNotes(resolvedVaultPath);
414
+ const maNotePaths = notePaths
415
+ .map((absolutePath) => normalizeVaultPath(path.relative(resolvedVaultPath, absolutePath)))
416
+ .filter((relativePath) => relativePath.startsWith("Meta-Architect/"));
417
+ const mapPath = obsidianGraphMapNotePath;
418
+ const nonMapMaNotes = maNotePaths.filter((relativePath) => relativePath !== mapPath).sort();
419
+ const receipts = [];
420
+
421
+ const mapContent = createObsidianGraphMapContent(nonMapMaNotes);
422
+ const mapReceipt = await upsertObsidianNoteContent({
423
+ vaultPath: resolvedVaultPath,
424
+ notePath: mapPath,
425
+ content: mapContent,
426
+ });
427
+ if (mapReceipt) receipts.push(mapReceipt);
428
+
429
+ const refreshedNotePaths = await listMarkdownNotes(resolvedVaultPath);
430
+ const refreshedMaNotes = refreshedNotePaths
431
+ .map((absolutePath) => normalizeVaultPath(path.relative(resolvedVaultPath, absolutePath)))
432
+ .filter((relativePath) => relativePath.startsWith("Meta-Architect/"))
433
+ .filter((relativePath) => relativePath !== mapPath)
434
+ .sort();
435
+ const hasCoreNote = refreshedMaNotes.includes("Meta-Architect/Core Brain Context.md");
436
+
437
+ for (const notePath of refreshedMaNotes) {
438
+ const target = await resolveVaultNotePath({
439
+ vaultPath: resolvedVaultPath,
440
+ notePath,
441
+ mustExist: true,
442
+ });
443
+ const current = await fs.readFile(target.absolutePath, "utf8");
444
+ const linked = withObsidianGraphSection(current, {
445
+ currentPath: notePath,
446
+ hasCoreNote,
447
+ });
448
+ if (linked === current) continue;
449
+ await writeVaultNote(target.absolutePath, linked, true);
450
+ receipts.push(
451
+ createObsidianOperationReceipt({
452
+ operation: "update_note",
453
+ target,
454
+ content: linked,
455
+ }),
456
+ );
457
+ }
458
+
459
+ return {
460
+ record_type: "obsidian_graph_link_result",
461
+ records_as: "vault_context",
462
+ build_evidence: false,
463
+ semantic_symlink_type: "obsidian_wikilink",
464
+ map_note_path: mapPath,
465
+ linked_note_count: refreshedMaNotes.length + 1,
466
+ operations: receipts,
467
+ };
468
+ }
469
+
470
+ export async function appendObsidianOperationReceipt(receipt) {
471
+ validateObsidianOperationReceipt(receipt);
472
+ let log = createDefaultObsidianVaultOperations();
473
+ try {
474
+ log = validateObsidianVaultOperations(await readJson(getObsidianVaultOperationsPath()));
475
+ } catch (error) {
476
+ if (error?.code !== "ENOENT") throw error;
477
+ }
478
+ log.operations.push(receipt);
479
+ await writeJson(getObsidianVaultOperationsPath(), log);
480
+ return log;
481
+ }
482
+
483
+ export function validateObsidianBridge(value) {
484
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
485
+ throw new Error("Obsidian bridge must be an object");
486
+ }
487
+ if (value.schemaVersion !== obsidianIntegrationSchemaVersion) {
488
+ throw new Error(`Unsupported Obsidian bridge schemaVersion: ${value.schemaVersion}`);
489
+ }
490
+ if (
491
+ value.semantic_boundary?.records_as !== "vault_context" ||
492
+ value.semantic_boundary?.never_records_as !== "build_evidence"
493
+ ) {
494
+ throw new Error("Obsidian bridge must record as vault_context, not build_evidence");
495
+ }
496
+ if (value.plugin_contract?.authoritative_changes_return_through !== "$maestro_or_owning_lane") {
497
+ throw new Error("Obsidian bridge authoritative changes must return through MA lanes");
498
+ }
499
+ if (
500
+ value.graph_link_policy?.enabled_by_default !== true ||
501
+ value.graph_link_policy?.semantic_symlink_type !== "obsidian_wikilink" ||
502
+ value.graph_link_policy?.map_note_path !== obsidianGraphMapNotePath ||
503
+ value.graph_link_policy?.records_as !== "vault_context" ||
504
+ value.graph_link_policy?.build_evidence !== false
505
+ ) {
506
+ throw new Error("Obsidian bridge must enable default wikilink graph policy");
507
+ }
508
+ const forbidden = new Set(value.plugin_contract?.must_not_mutate ?? []);
509
+ for (const required of [".ma/release.json", ".ma/decisions.json", ".ma/plans/", ".ma/specs/"]) {
510
+ if (!forbidden.has(required)) {
511
+ throw new Error(`Obsidian bridge must forbid direct mutation of ${required}`);
512
+ }
513
+ }
514
+ if (!Array.isArray(value.default_request_queue)) {
515
+ throw new Error("Obsidian bridge requires a request queue");
516
+ }
517
+ if (
518
+ value.plugin_runtime?.plugin_id !== "meta-architect" ||
519
+ value.plugin_runtime?.records_as !== "vault_context" ||
520
+ value.plugin_runtime?.build_evidence !== false ||
521
+ !Array.isArray(value.plugin_runtime?.capabilities)
522
+ ) {
523
+ throw new Error("Obsidian bridge requires the default MA in-app plugin runtime");
524
+ }
525
+ for (const capability of [
526
+ "in_app_plugin_surface",
527
+ "active_note_selection_context",
528
+ "metadata_cache_deep_graph",
529
+ "frontmatter_authority_layer",
530
+ "link_safe_rename_move",
531
+ "protocol_handler",
532
+ "event_watchers",
533
+ "canvas_visual_workspace_context",
534
+ "attachments_binary_resources",
535
+ "plugin_request_queue",
536
+ ]) {
537
+ if (!value.plugin_runtime.capabilities.some((entry) => entry.capability === capability)) {
538
+ throw new Error(`Obsidian plugin runtime missing capability: ${capability}`);
539
+ }
540
+ }
541
+ if (!Array.isArray(value.note_selection_metadata)) {
542
+ throw new Error("Obsidian bridge requires note selection metadata");
543
+ }
544
+ if (!Array.isArray(value.tag_graph_claims)) {
545
+ throw new Error("Obsidian bridge requires tag graph claims");
546
+ }
547
+ return value;
548
+ }
549
+
550
+ export function validateObsidianVaultOperations(value) {
551
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
552
+ throw new Error("Obsidian vault operations must be an object");
553
+ }
554
+ if (value.schemaVersion !== obsidianIntegrationSchemaVersion) {
555
+ throw new Error(`Unsupported Obsidian operations schemaVersion: ${value.schemaVersion}`);
556
+ }
557
+ if (
558
+ value.record_type !== "obsidian_vault_operations" ||
559
+ value.records_as !== "vault_context" ||
560
+ value.build_evidence !== false ||
561
+ value.authority_boundary !== "$maestro_or_owning_lane" ||
562
+ !Array.isArray(value.operations)
563
+ ) {
564
+ throw new Error("Obsidian vault operations must remain lane-owned vault_context");
565
+ }
566
+ for (const operation of value.operations) {
567
+ validateObsidianOperationReceipt(operation);
568
+ }
569
+ return value;
570
+ }
571
+
572
+ export function validateObsidianOperationReceipt(value) {
573
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
574
+ throw new Error("Obsidian operation receipt must be an object");
575
+ }
576
+ if (
577
+ !["create_note", "read_note", "update_note", "delete_note", "plugin_install"].includes(
578
+ value.operation,
579
+ ) ||
580
+ value.records_as !== "vault_context" ||
581
+ value.build_evidence !== false ||
582
+ value.authority_boundary !== "$maestro_or_owning_lane" ||
583
+ typeof value.relative_path !== "string" ||
584
+ typeof value.content_sha256 !== "string"
585
+ ) {
586
+ throw new Error("Obsidian operation receipt must be lane-owned vault_context");
587
+ }
588
+ return value;
589
+ }
590
+
591
+ export function validateObsidianVaultIndex(value) {
592
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
593
+ throw new Error("Obsidian vault index must be an object");
594
+ }
595
+ if (value.schemaVersion !== obsidianIntegrationSchemaVersion) {
596
+ throw new Error(`Unsupported Obsidian vault index schemaVersion: ${value.schemaVersion}`);
597
+ }
598
+ if (
599
+ value.record_type !== "obsidian_vault_index" ||
600
+ value.records_as !== "vault_context" ||
601
+ value.build_evidence !== false ||
602
+ value.read_only_snapshot !== true
603
+ ) {
604
+ throw new Error("Obsidian vault index must be read-only vault_context");
605
+ }
606
+ if (!Number.isInteger(value.note_count) || value.note_count < 0) {
607
+ throw new Error("Obsidian vault index requires note_count");
608
+ }
609
+ if (!Array.isArray(value.notes) || value.notes.length !== value.note_count) {
610
+ throw new Error("Obsidian vault index notes must match note_count");
611
+ }
612
+ for (const note of value.notes) {
613
+ if (
614
+ note.record_type !== "vault_context_note" ||
615
+ note.records_as !== "vault_context" ||
616
+ note.build_evidence !== false ||
617
+ typeof note.relative_path !== "string" ||
618
+ typeof note.content_sha256 !== "string"
619
+ ) {
620
+ throw new Error("Obsidian vault index note entries must be vault_context notes");
621
+ }
622
+ }
623
+ for (const claim of value.tag_graph_claims ?? []) {
624
+ if (claim.records_as !== "vault_context" || claim.build_evidence !== false) {
625
+ throw new Error("Obsidian tag graph claims must remain vault_context");
626
+ }
627
+ }
628
+ return value;
629
+ }
630
+
631
+ export async function seedObsidianBridgeArtifacts() {
632
+ await ensureDir(getRuntimeSubsystemPath("context"));
633
+ await writeFileIfMissing(
634
+ getObsidianBridgePath(),
635
+ `${JSON.stringify(createDefaultObsidianBridge(), null, 2)}\n`,
636
+ );
637
+ await writeFileIfMissing(
638
+ getObsidianVaultIndexPath(),
639
+ `${JSON.stringify(createDefaultObsidianVaultIndex(), null, 2)}\n`,
640
+ );
641
+ await writeFileIfMissing(
642
+ getObsidianVaultOperationsPath(),
643
+ `${JSON.stringify(createDefaultObsidianVaultOperations(), null, 2)}\n`,
644
+ );
645
+ }
646
+
647
+ export async function loadObsidianBridge() {
648
+ return validateObsidianBridge(await readJson(getObsidianBridgePath()));
649
+ }
650
+
651
+ export async function loadObsidianVaultIndex() {
652
+ return validateObsidianVaultIndex(await readJson(getObsidianVaultIndexPath()));
653
+ }
654
+
655
+ export async function loadObsidianVaultOperations() {
656
+ return validateObsidianVaultOperations(await readJson(getObsidianVaultOperationsPath()));
657
+ }
658
+
659
+ export async function writeObsidianVaultIndex({ vaultPath, ensureGraphLinks = true } = {}) {
660
+ if (ensureGraphLinks) {
661
+ const graph = await ensureObsidianGraphLinks({ vaultPath });
662
+ for (const receipt of graph.operations) {
663
+ await appendObsidianOperationReceipt(receipt);
664
+ }
665
+ }
666
+ const index = await indexObsidianVault({ vaultPath });
667
+ await writeJson(getObsidianVaultIndexPath(), index);
668
+ return index;
669
+ }
670
+
671
+ export async function embedProjectContext({ vaultPath, projectIndex } = {}) {
672
+ if (!projectIndex || typeof projectIndex !== "object") {
673
+ throw new Error("Obsidian project context requires a project index");
674
+ }
675
+
676
+ const resolvedVaultPath = await resolveVaultRoot(vaultPath);
677
+ await configureObsidianVault(resolvedVaultPath);
678
+ const projectName =
679
+ String(projectIndex.project?.name ?? "project")
680
+ .replace(/[^a-zA-Z0-9._ -]/g, "-")
681
+ .trim()
682
+ .replace(/[ .]+$/g, "") || "project";
683
+ const notePath = `Meta-Architect/Projects/${projectName}/Project Context.md`;
684
+ const content = createProjectContextNote(projectIndex);
685
+ const operation = await upsertObsidianNoteContent({
686
+ vaultPath: resolvedVaultPath,
687
+ notePath,
688
+ content,
689
+ });
690
+ if (operation) await appendObsidianOperationReceipt(operation);
691
+
692
+ const index = await writeObsidianVaultIndex({ vaultPath: resolvedVaultPath });
693
+ return {
694
+ vaultPath: resolvedVaultPath,
695
+ notePath,
696
+ noteStatus: operation ? operation.operation : "unchanged",
697
+ index,
698
+ };
699
+ }
700
+
701
+ export async function appendObsidianLearningRecord({ vaultPath, record } = {}) {
702
+ if (!record || typeof record !== "object") throw new Error("Learning record is required");
703
+ const notePath = "Meta-Architect/Learning Memory.md";
704
+ const existing = await readObsidianNote({ vaultPath, notePath }).catch(
705
+ () => "# Learning Memory\n",
706
+ );
707
+ const lines = [
708
+ `## ${record.created_at ?? new Date().toISOString()}`,
709
+ "",
710
+ `- Claim: ${record.claim}`,
711
+ `- Source: ${record.source}`,
712
+ `- Status: ${record.status}`,
713
+ ...(record.failure_state
714
+ ? [
715
+ `- Failure state: ${record.failure_state}`,
716
+ `- Resolution status: ${record.resolution_status ?? record.failure_state}`,
717
+ `- Attempted fixes: ${(record.attempted_fixes ?? []).join(", ") || "none"}`,
718
+ ]
719
+ : []),
720
+ `- Evidence: ${(record.evidence ?? []).join(", ") || "none"}`,
721
+ `- Files: ${(record.files_involved ?? []).join(", ") || "none"}`,
722
+ `- Next verification: ${record.next_verification}`,
723
+ "",
724
+ ];
725
+ await upsertObsidianNoteContent({
726
+ vaultPath,
727
+ notePath,
728
+ content: `${existing.trimEnd()}\n\n${lines.join("\n")}`,
729
+ });
730
+ await ensureObsidianGraphLinks({ vaultPath });
731
+ return notePath;
732
+ }
733
+
734
+ async function upsertObsidianNoteContent({ vaultPath, notePath, content }) {
735
+ const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: false });
736
+ let current = null;
737
+ if (await pathExists(target.absolutePath)) {
738
+ current = await fs.readFile(target.absolutePath, "utf8");
739
+ }
740
+ if (current === content) return null;
741
+
742
+ await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
743
+ await writeVaultNote(target.absolutePath, content, Boolean(current));
744
+ return createObsidianOperationReceipt({
745
+ operation: current === null ? "create_note" : "update_note",
746
+ target,
747
+ content,
748
+ });
749
+ }
750
+
751
+ function createObsidianGraphMapContent(notePaths) {
752
+ const noteLinks = [...new Set(notePaths)]
753
+ .filter((notePath) => notePath !== obsidianGraphMapNotePath)
754
+ .sort()
755
+ .map((notePath) => `- ${toObsidianWikiLink(notePath)} — MA vault context note.`);
756
+ const noteList =
757
+ noteLinks.length > 0 ? noteLinks.join("\n") : "- No MA context notes have been recorded yet.";
758
+
759
+ return `# Meta-Architect Map of Content
760
+
761
+ This is the canonical Obsidian graph hub for Meta-Architect vault context. MA creates and maintains this note automatically when a vault is indexed or when MA performs vault CRUD.
762
+
763
+ ## Canonical MA Notes
764
+
765
+ ${noteList}
766
+
767
+ ## Core Capability Links
768
+
769
+ - Obsidian Integration Core uses wikilinks as semantic symlinks between MA vault notes.
770
+ - Context Economy Core preserves technical meaning while keeping note text compact when applicable.
771
+ - Ralph Execution Core and Active Autonomy Core use these notes as vault_context only.
772
+
773
+ ## Evidence Boundary
774
+
775
+ These linked notes are vault_context. They are not build_evidence. Build evidence stays in MA lane artifacts and command outputs.
776
+
777
+ ## Graph Rules
778
+
779
+ - Every MA-generated Obsidian note links back to ${toObsidianWikiLink(obsidianGraphMapNotePath, "MA Map of Content")}.
780
+ - The map links every note under Meta-Architect/.
781
+ - Obsidian links are semantic graph links, not release authority. They must not mutate .ma/release.json, .ma/decisions.json, or lane artifacts directly.
782
+
783
+ #ma/core #ma/map #ma/obsidian #ma/vault-context
784
+ `;
785
+ }
786
+
787
+ function createProjectContextNote(projectIndex) {
788
+ const project = projectIndex.project ?? {};
789
+ const lines = [
790
+ `# ${project.name ?? "Project"}`,
791
+ "",
792
+ "This note is Meta-Architect vault context, not build evidence.",
793
+ "",
794
+ `- Graph hub: ${toObsidianWikiLink(obsidianGraphMapNotePath, "MA Map of Content")}`,
795
+ `- Source authority: ${projectIndex.authority ?? "source_truth"}`,
796
+ `- Freshness: ${projectIndex.freshness?.status ?? "unknown"}`,
797
+ `- Source files scanned: ${projectIndex.quality?.coverage?.sourceFiles ?? 0}`,
798
+ `- Context quality: ${projectIndex.quality?.completeness ?? "unknown"} / ${projectIndex.quality?.confidence ?? "unknown"}`,
799
+ `- Canonical facts: ${(projectIndex.facts ?? []).map((fact) => fact.id).join(", ") || "none"}`,
800
+ "",
801
+ "## Stack",
802
+ "",
803
+ `- Languages: ${(projectIndex.languages ?? []).join(", ") || "unknown"}`,
804
+ `- Frameworks: ${(projectIndex.frameworks ?? []).join(", ") || "unknown"}`,
805
+ `- Package manager: ${projectIndex.packageManager ?? "unknown"}`,
806
+ "",
807
+ "## Commands",
808
+ "",
809
+ ...Object.entries(projectIndex.commands ?? {}).map(
810
+ ([name, command]) => `- ${name}: \`${command}\``,
811
+ ),
812
+ "",
813
+ "## Entry points and documentation",
814
+ "",
815
+ ...((projectIndex.entrypoints ?? []).length
816
+ ? projectIndex.entrypoints.map((entry) => `- ${entry}`)
817
+ : ["- No entry points detected."]),
818
+ ...((projectIndex.importantDocs ?? []).length
819
+ ? projectIndex.importantDocs.map((entry) => `- ${entry}`)
820
+ : ["- No important documentation detected."]),
821
+ "",
822
+ "## Agent integrations",
823
+ "",
824
+ `- ${(projectIndex.vendorIntegrations ?? []).join(", ") || "None detected"}`,
825
+ "",
826
+ ];
827
+ return createManagedMarkdownBlock({
828
+ id: "obsidian-project-context",
829
+ source: ".ma/context/project-index.json",
830
+ body: lines.join("\n"),
831
+ });
832
+ }
833
+
834
+ function withObsidianGraphSection(content, { currentPath, hasCoreNote }) {
835
+ const base = content
836
+ .replace(new RegExp(`\\n${escapeRegExp(obsidianGraphSectionHeading)}[\\s\\S]*$`, "m"), "")
837
+ .trimEnd();
838
+ const links = [`- Hub: ${toObsidianWikiLink(obsidianGraphMapNotePath, "MA Map of Content")}`];
839
+ if (hasCoreNote && currentPath !== "Meta-Architect/Core Brain Context.md") {
840
+ links.push(
841
+ `- Core context: ${toObsidianWikiLink("Meta-Architect/Core Brain Context.md", "Core Brain Context")}`,
842
+ );
843
+ }
844
+ links.push("- This note records as vault_context, not build_evidence.");
845
+
846
+ return `${base}
847
+
848
+ ${obsidianGraphSectionHeading}
849
+
850
+ ${links.join("\n")}
851
+ `;
852
+ }
853
+
854
+ function toObsidianWikiLink(notePath, alias = null) {
855
+ const target = normalizeVaultPath(notePath).replace(/\.md$/i, "");
856
+ return alias ? `[[${target}|${alias}]]` : `[[${target}]]`;
857
+ }
858
+
859
+ function createObsidianOperationReceipt({ operation, target, content }) {
860
+ return validateObsidianOperationReceipt({
861
+ record_type: "obsidian_vault_operation",
862
+ operation,
863
+ records_as: "vault_context",
864
+ build_evidence: false,
865
+ authority_boundary: "$maestro_or_owning_lane",
866
+ vault_path: target.vaultPath,
867
+ relative_path: target.relativePath,
868
+ content_sha256: createHash("sha256").update(content).digest("hex"),
869
+ char_count: content.length,
870
+ word_count: countWords(content),
871
+ operated_at: new Date().toISOString(),
872
+ });
873
+ }
874
+
875
+ async function resolveVaultRoot(vaultPath) {
876
+ return resolveObsidianVault(vaultPath);
877
+ }
878
+
879
+ async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
880
+ const resolvedVaultPath = await resolveVaultRoot(vaultPath);
881
+ if (!notePath || typeof notePath !== "string") {
882
+ throw new Error("Obsidian note operation requires notePath");
883
+ }
884
+ const normalizedInput = path.posix
885
+ .normalize(normalizeVaultPath(notePath.trim()))
886
+ .replace(/^(\.\/)+/, "")
887
+ .replace(/^\/+/, "");
888
+ if (
889
+ !normalizedInput ||
890
+ normalizedInput.startsWith("../") ||
891
+ normalizedInput.includes("/../") ||
892
+ normalizedInput === ".."
893
+ ) {
894
+ throw new Error("Obsidian note path must stay inside the vault");
895
+ }
896
+ if (
897
+ normalizedInput.startsWith(".obsidian/") ||
898
+ normalizedInput.startsWith(".trash/") ||
899
+ normalizedInput.startsWith(".git/")
900
+ ) {
901
+ throw new Error("Obsidian note operation cannot target vault control directories");
902
+ }
903
+ const relativePath = normalizedInput.toLowerCase().endsWith(".md")
904
+ ? normalizedInput
905
+ : `${normalizedInput}.md`;
906
+ await rejectSymlinkComponents(resolvedVaultPath, relativePath);
907
+ const absolutePath = path.resolve(resolvedVaultPath, relativePath);
908
+ if (!absolutePath.startsWith(`${resolvedVaultPath}${path.sep}`)) {
909
+ throw new Error("Obsidian note path resolved outside the vault");
910
+ }
911
+ if (mustExist && !(await pathExists(absolutePath))) {
912
+ throw new Error(`Obsidian note does not exist: ${relativePath}`);
913
+ }
914
+ try {
915
+ if ((await fs.lstat(absolutePath)).isSymbolicLink()) {
916
+ throw new Error("Obsidian note path cannot be a symlink");
917
+ }
918
+ } catch (error) {
919
+ if (error?.code !== "ENOENT") throw error;
920
+ }
921
+ const resolvedTarget = await resolveExistingPathOrParent(absolutePath);
922
+ if (
923
+ resolvedTarget !== resolvedVaultPath &&
924
+ !resolvedTarget.startsWith(`${resolvedVaultPath}${path.sep}`)
925
+ ) {
926
+ throw new Error("Obsidian note path resolves through a symlink outside the vault");
927
+ }
928
+ return { vaultPath: resolvedVaultPath, relativePath, absolutePath };
929
+ }
930
+
931
+ async function resolveExistingPathOrParent(filePath) {
932
+ let candidate = filePath;
933
+ while (candidate !== path.dirname(candidate)) {
934
+ try {
935
+ return await fs.realpath(candidate);
936
+ } catch (error) {
937
+ if (error?.code !== "ENOENT") {
938
+ throw error;
939
+ }
940
+ candidate = path.dirname(candidate);
941
+ }
942
+ }
943
+ return fs.realpath(candidate);
944
+ }
945
+
946
+ async function rejectSymlinkComponents(rootPath, relativePath) {
947
+ let current = rootPath;
948
+ for (const component of relativePath.split("/")) {
949
+ current = path.join(current, component);
950
+ try {
951
+ if ((await fs.lstat(current)).isSymbolicLink()) {
952
+ throw new Error("Obsidian note path cannot traverse a symlink");
953
+ }
954
+ } catch (error) {
955
+ if (error?.code !== "ENOENT") throw error;
956
+ break;
957
+ }
958
+ }
959
+ }
960
+
961
+ async function pathExists(filePath) {
962
+ try {
963
+ await fs.access(filePath);
964
+ return true;
965
+ } catch {
966
+ return false;
967
+ }
968
+ }
969
+
970
+ async function listMarkdownNotes(root) {
971
+ const result = [];
972
+ async function walk(directory) {
973
+ const entries = await fs.readdir(directory, { withFileTypes: true });
974
+ for (const entry of entries) {
975
+ if (entry.name === ".obsidian" || entry.name === ".trash" || entry.name === ".git") {
976
+ continue;
977
+ }
978
+ const absolutePath = path.join(directory, entry.name);
979
+ if (entry.isDirectory()) {
980
+ await walk(absolutePath);
981
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
982
+ result.push(absolutePath);
983
+ }
984
+ }
985
+ }
986
+ await walk(root);
987
+ return result.sort();
988
+ }
989
+
990
+ function extractTitle(content) {
991
+ const heading = content.match(/^#\s+(.+)$/m);
992
+ return heading?.[1]?.trim() || null;
993
+ }
994
+
995
+ function extractObsidianTags(content) {
996
+ const tags = new Set();
997
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
998
+ if (frontmatter) {
999
+ const tagBlock = frontmatter[1].match(/^tags:\s*([\s\S]*?)(?:\n\S|\n?$)/m);
1000
+ if (tagBlock) {
1001
+ for (const tag of tagBlock[1].split(/[\s,[\]]+/)) {
1002
+ addTag(tags, tag);
1003
+ }
1004
+ }
1005
+ }
1006
+ for (const match of content.matchAll(/(^|[\s([{])#([A-Za-z0-9_/-]+)/g)) {
1007
+ addTag(tags, match[2]);
1008
+ }
1009
+ return [...tags].sort();
1010
+ }
1011
+
1012
+ function extractObsidianLinks(content) {
1013
+ const links = [];
1014
+ for (const match of content.matchAll(/\[\[([^\]\n]+)\]\]/g)) {
1015
+ const [target, alias] = match[1].split("|").map((part) => part.trim());
1016
+ links.push({ type: "wikilink", target, alias: alias || null });
1017
+ }
1018
+ for (const match of content.matchAll(/\[[^\]\n]+\]\(([^)\n]+)\)/g)) {
1019
+ const target = match[1].trim();
1020
+ if (!target.startsWith("http://") && !target.startsWith("https://")) {
1021
+ links.push({ type: "markdown", target, alias: null });
1022
+ }
1023
+ }
1024
+ return links.sort((a, b) => a.target.localeCompare(b.target));
1025
+ }
1026
+
1027
+ function addTag(tags, value) {
1028
+ const cleaned = value?.trim().replace(/^#/, "").replace(/,$/, "");
1029
+ if (cleaned && /^[A-Za-z0-9_/-]+$/.test(cleaned)) {
1030
+ tags.add(cleaned);
1031
+ }
1032
+ }
1033
+
1034
+ function normalizeVaultPath(value) {
1035
+ return value.split(path.sep).join("/");
1036
+ }
1037
+
1038
+ function normalizeLinkKey(value) {
1039
+ return normalizeVaultPath(String(value).trim())
1040
+ .replace(/\.md$/i, "")
1041
+ .replace(/^\/+/, "")
1042
+ .toLowerCase();
1043
+ }
1044
+
1045
+ function assertSafeQueuedPath(value) {
1046
+ if (typeof value !== "string" || !value.trim()) {
1047
+ throw new Error("Obsidian plugin request path must be a non-empty string");
1048
+ }
1049
+ const normalized = normalizeVaultPath(value.trim()).replace(/^\/+/, "");
1050
+ if (
1051
+ normalized.startsWith("../") ||
1052
+ normalized.includes("/../") ||
1053
+ normalized === ".." ||
1054
+ normalized.startsWith(".obsidian/") ||
1055
+ normalized.startsWith(".trash/") ||
1056
+ normalized.startsWith(".git/") ||
1057
+ normalized.startsWith(".ma/")
1058
+ ) {
1059
+ throw new Error(`Obsidian plugin request path is not allowed: ${normalized}`);
1060
+ }
1061
+ return normalized;
1062
+ }
1063
+
1064
+ function assertSafeQueuedTag(value) {
1065
+ if (typeof value !== "string" || !value.trim()) {
1066
+ throw new Error("Obsidian plugin request tag must be a non-empty string");
1067
+ }
1068
+ const normalized = value.trim().replace(/^#/, "");
1069
+ if (!/^[A-Za-z0-9_/-]+$/.test(normalized)) {
1070
+ throw new Error(`Obsidian plugin request tag is not allowed: ${normalized}`);
1071
+ }
1072
+ return normalized;
1073
+ }
1074
+
1075
+ function escapeRegExp(value) {
1076
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1077
+ }
1078
+
1079
+ function countWords(content) {
1080
+ return content.split(/\s+/).filter(Boolean).length;
1081
+ }
1082
+
1083
+ function createExcerpt(content, maxChars) {
1084
+ return content
1085
+ .replace(/^---\n[\s\S]*?\n---/, "")
1086
+ .replace(/\s+/g, " ")
1087
+ .trim()
1088
+ .slice(0, maxChars);
1089
+ }