@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
File without changes
File without changes
@@ -0,0 +1,58 @@
1
+ export function createMaestroView(maestroState) {
2
+ const tracks = Object.entries(maestroState?.runtime_tracks ?? {}).map(([id, track]) => ({
3
+ id,
4
+ gate: track.active_gate ?? "none",
5
+ status: track.status ?? "unknown",
6
+ blockers: Array.isArray(track.blockers) ? track.blockers : [],
7
+ }));
8
+ const locks = Object.entries(maestroState?.downstream_lock_table ?? {}).map(([gate, lock]) => ({
9
+ gate,
10
+ status: lock.is_locked ? "locked" : "unlocked",
11
+ lockedBy: Array.isArray(lock.locked_by) ? lock.locked_by : [],
12
+ unlockCriteria: lock.unlock_criteria ?? null,
13
+ }));
14
+ const blockers = tracks.flatMap((track) =>
15
+ track.blockers.map((message) => `${track.id}: ${message}`),
16
+ );
17
+ return {
18
+ schemaVersion: "0.1.0",
19
+ scope: "maestro",
20
+ globalStatus: maestroState?.global_status ?? "unknown",
21
+ orchestrationId: maestroState?.orchestration_id ?? null,
22
+ tracks,
23
+ locks,
24
+ blockers,
25
+ nextAction:
26
+ blockers[0] ??
27
+ (locks.some((lock) => lock.status === "locked")
28
+ ? "Resolve downstream locks"
29
+ : "Continue active lane"),
30
+ };
31
+ }
32
+
33
+ export function formatMaestroView(view) {
34
+ const lines = [
35
+ "Maestro View",
36
+ "Meta-Architect / Maestro",
37
+ "========================",
38
+ `Global status: ${view.globalStatus}`,
39
+ `Orchestration id: ${view.orchestrationId ?? "not started"}`,
40
+ `Status: ${view.globalStatus}`,
41
+ `Run: ${view.orchestrationId ?? "not started"}`,
42
+ "",
43
+ "Lane Gate Status",
44
+ "---------- -------------------- ----------",
45
+ ];
46
+ if (view.tracks.length === 0) lines.push("none");
47
+ for (const track of view.tracks)
48
+ lines.push(`${track.id.padEnd(10)} ${track.gate.padEnd(20)} ${track.status}`);
49
+ lines.push("", "Blockers");
50
+ lines.push(...(view.blockers.length ? view.blockers.map((item) => `- ${item}`) : ["- none"]));
51
+ lines.push("", `Next: ${view.nextAction}`, "", "Downstream locks");
52
+ lines.push(
53
+ ...(view.locks.length
54
+ ? view.locks.map((lock) => `- ${lock.gate}: ${lock.status}`)
55
+ : ["- none"]),
56
+ );
57
+ return `${lines.join("\n")}\n`;
58
+ }
File without changes
@@ -0,0 +1,29 @@
1
+ const START = "<!-- MA:MANAGED:START";
2
+ const END = "<!-- MA:MANAGED:END -->";
3
+
4
+ export function createManagedMarkdownBlock({ id, source, body }) {
5
+ return `${START} id=${id} source=${source} -->\n${body.trim()}\n${END}\n`;
6
+ }
7
+
8
+ export function replaceManagedMarkdownBlock(existing, generated, id) {
9
+ const marker = new RegExp(
10
+ `<!-- MA:MANAGED:START id=${escapeRegExp(id)}[^>]*-->[\\s\\S]*?${END}`,
11
+ "g",
12
+ );
13
+ const starts = existing.match(/<!-- MA:MANAGED:START\b/g)?.length ?? 0;
14
+ const ends = existing.match(new RegExp(escapeRegExp(END), "g"))?.length ?? 0;
15
+ if (starts === 1 && ends === 1 && marker.test(existing))
16
+ return existing.replace(marker, generated.trim());
17
+ return `${existing.trimEnd()}\n\n${generated}`;
18
+ }
19
+
20
+ export function removeManagedMarkdownBlock(existing, id) {
21
+ const marker = new RegExp(
22
+ `\\n?<!-- MA:MANAGED:START id=${escapeRegExp(id)}[^>]*-->[\\s\\S]*?${END}\\n?`,
23
+ );
24
+ return existing.replace(marker, "");
25
+ }
26
+
27
+ function escapeRegExp(value) {
28
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29
+ }
@@ -0,0 +1,147 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { writeJson } from "../fs-utils.js";
5
+ import { getRuntimeWritePath } from "../paths.js";
6
+ import { resolveHookProfile } from "./hook-profiles.js";
7
+ import { maskSensitiveText } from "./redaction-gateway.js";
8
+ import { queueMailboxProposal } from "./runtime-state.js";
9
+
10
+ const writePolicies = {
11
+ "state.sync_release_status": "state-write",
12
+ "state.append_decision": "state-write",
13
+ "memory.store_note": "memory-write",
14
+ "team_run.submit_task": "task-control",
15
+ "team_run.control_task": "task-control",
16
+ };
17
+
18
+ function getField(options, camel, snake) {
19
+ return options?.[camel] ?? options?.[snake];
20
+ }
21
+
22
+ function getProfile(options) {
23
+ const requested = getField(options, "profile", "profile") ?? process.env.MA_MCP_PROFILE;
24
+ return requested ? resolveHookProfile(requested) : resolveHookProfile();
25
+ }
26
+
27
+ function validateMetadata(options) {
28
+ const metadata = {
29
+ actor: getField(options, "actor", "actor"),
30
+ authority: options?.authority ?? getField(options, "authorityBoundary", "authority_boundary"),
31
+ reason: getField(options, "reason", "reason"),
32
+ evidence: getField(options, "evidence", "evidence"),
33
+ };
34
+ const missing = Object.entries(metadata)
35
+ .filter(([, value]) => value == null || (typeof value === "string" && value.trim() === ""))
36
+ .map(([key]) => key);
37
+ return { metadata, missing };
38
+ }
39
+
40
+ function receiptPath() {
41
+ return path.join(getRuntimeWritePath("mcp", "receipts"), `${Date.now()}-${randomUUID()}.json`);
42
+ }
43
+
44
+ async function writeAuditReceipt({ tool, classification, profile, metadata, outcome, reason }) {
45
+ const safe = maskSensitiveText(
46
+ JSON.stringify({
47
+ actor: metadata.actor,
48
+ authority: metadata.authority,
49
+ reason: metadata.reason,
50
+ evidence: metadata.evidence,
51
+ }),
52
+ ).sanitizedText;
53
+ const receipt = {
54
+ schemaVersion: "0.1.0",
55
+ record_type: "mcp_write_audit_receipt",
56
+ tool,
57
+ classification,
58
+ profile: profile.id,
59
+ outcome,
60
+ denial_reason: reason ?? null,
61
+ metadata: JSON.parse(safe),
62
+ recordedAt: new Date().toISOString(),
63
+ };
64
+ const target = receiptPath();
65
+ await writeJson(target, receipt);
66
+ await fs.chmod(target, 0o600).catch(() => {});
67
+ return target;
68
+ }
69
+
70
+ export function listMcpWritePolicies() {
71
+ return Object.entries(writePolicies).map(([tool, classification]) => ({ tool, classification }));
72
+ }
73
+
74
+ export function mcpWriteCapabilityStatus() {
75
+ const profile = resolveHookProfile(
76
+ process.env.MA_MCP_PROFILE ?? process.env.MA_PROFILE ?? "local",
77
+ );
78
+ return {
79
+ profile: profile.id,
80
+ readOnly: profile.readOnly === true,
81
+ tools: listMcpWritePolicies(),
82
+ detail: profile.readOnly
83
+ ? `MCP writes disabled by ${profile.id} profile`
84
+ : `MCP writes require actor, authority, reason, and evidence in ${profile.id} profile`,
85
+ };
86
+ }
87
+
88
+ export async function loadMcpAuditReceipts() {
89
+ const root = getRuntimeWritePath("mcp", "receipts");
90
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
91
+ const receipts = [];
92
+ for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(".json"))) {
93
+ try {
94
+ receipts.push(JSON.parse(await fs.readFile(path.join(root, entry.name), "utf8")));
95
+ } catch {
96
+ // Ignore incomplete receipts; doctor can still report the directory.
97
+ }
98
+ }
99
+ return receipts.sort((a, b) => `${a.recordedAt}`.localeCompare(`${b.recordedAt}`));
100
+ }
101
+
102
+ export async function withMcpWriteGate({ tool, options = {}, payload = {} }, operation) {
103
+ const classification = writePolicies[tool];
104
+ if (!classification) throw new Error(`Unknown MCP write tool: ${tool}`);
105
+ const profile = getProfile(options);
106
+ const { metadata, missing } = validateMetadata(options);
107
+
108
+ if (profile.readOnly === true) {
109
+ const auditReceipt = await writeAuditReceipt({
110
+ tool,
111
+ classification,
112
+ profile,
113
+ metadata,
114
+ outcome: "denied",
115
+ reason: "profile_read_only",
116
+ });
117
+ return { proposed: false, denied: true, reason: "profile_read_only", auditReceipt };
118
+ }
119
+
120
+ if (missing.length > 0) {
121
+ const proposalPath = await queueMailboxProposal({
122
+ actor: metadata.actor ?? "mcp-unknown",
123
+ kind: `mcp-${classification}`,
124
+ payload: { tool, payload, missingAuthority: missing },
125
+ });
126
+ const auditReceipt = await writeAuditReceipt({
127
+ tool,
128
+ classification,
129
+ profile,
130
+ metadata,
131
+ outcome: "proposed",
132
+ reason: `missing_authority:${missing.join(",")}`,
133
+ });
134
+ return { proposed: true, proposalPath, auditReceipt, reason: "missing_authority" };
135
+ }
136
+
137
+ const result = await operation(metadata);
138
+ const auditReceipt = await writeAuditReceipt({
139
+ tool,
140
+ classification,
141
+ profile,
142
+ metadata,
143
+ outcome: result?.proposed ? "proposed" : "allowed",
144
+ reason: result?.proposed ? "leader_authority_required" : null,
145
+ });
146
+ return { ...result, auditReceipt };
147
+ }
File without changes
@@ -3,12 +3,50 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { ensureDir, readJson, writeFileIfMissing, writeJson } from "../fs-utils.js";
5
5
  import { getRuntimeSubsystemPath } from "../paths.js";
6
+ import { createManagedMarkdownBlock } from "./managed-markdown.js";
6
7
  import { createDefaultObsidianPluginBridgeManifest } from "./obsidian-plugin-bridge.js";
7
8
 
8
9
  export const obsidianIntegrationSchemaVersion = "0.1.0";
9
10
  export const obsidianGraphMapNotePath = "Meta-Architect/Map of Content.md";
11
+ const obsidianConfigPath = () => getRuntimeSubsystemPath("obsidian", "config.json");
10
12
  const obsidianGraphSectionHeading = "## Obsidian Graph Links";
11
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
+
12
50
  export function getObsidianBridgePath() {
13
51
  return getRuntimeSubsystemPath("context", "obsidian-bridge.json");
14
52
  }
@@ -104,6 +142,9 @@ export function createObsidianPluginRequest({
104
142
  if (!action || typeof action !== "string") {
105
143
  throw new Error("Obsidian plugin request requires an action");
106
144
  }
145
+ if (!allowedObsidianPluginActions.has(action)) {
146
+ throw new Error(`Obsidian plugin request action is not allowed: ${action}`);
147
+ }
107
148
 
108
149
  return {
109
150
  record_type: "obsidian_plugin_request",
@@ -111,8 +152,8 @@ export function createObsidianPluginRequest({
111
152
  reason,
112
153
  records_as: "vault_context",
113
154
  authoritative_change: false,
114
- note_paths: notePaths.filter((entry) => typeof entry === "string" && entry.trim()),
115
- tags: tags.filter((entry) => typeof entry === "string" && entry.trim()),
155
+ note_paths: notePaths.map(assertSafeQueuedPath),
156
+ tags: tags.map(assertSafeQueuedTag),
116
157
  queued_at: new Date().toISOString(),
117
158
  };
118
159
  }
@@ -284,6 +325,7 @@ export async function indexObsidianVault({ vaultPath, maxExcerptChars = 360 } =
284
325
 
285
326
  export async function listObsidianNotes({ vaultPath } = {}) {
286
327
  const resolvedVaultPath = await resolveVaultRoot(vaultPath);
328
+ await configureObsidianVault(resolvedVaultPath);
287
329
  const notePaths = await listMarkdownNotes(resolvedVaultPath);
288
330
  return {
289
331
  record_type: "obsidian_note_list",
@@ -324,7 +366,7 @@ export async function createObsidianNote({ vaultPath, notePath, content, overwri
324
366
  throw new Error(`Obsidian note already exists: ${target.relativePath}`);
325
367
  }
326
368
  await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
327
- await fs.writeFile(target.absolutePath, content, "utf8");
369
+ await writeVaultNote(target.absolutePath, content, overwrite);
328
370
  return createObsidianOperationReceipt({
329
371
  operation: overwrite ? "update_note" : "create_note",
330
372
  target,
@@ -337,10 +379,24 @@ export async function updateObsidianNote({ vaultPath, notePath, content } = {})
337
379
  throw new Error("Obsidian update requires non-empty note content");
338
380
  }
339
381
  const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
340
- await fs.writeFile(target.absolutePath, content, "utf8");
382
+ await writeVaultNote(target.absolutePath, content, true);
341
383
  return createObsidianOperationReceipt({ operation: "update_note", target, content });
342
384
  }
343
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
+
344
400
  export async function deleteObsidianNote({ vaultPath, notePath } = {}) {
345
401
  const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
346
402
  const content = await fs.readFile(target.absolutePath, "utf8");
@@ -390,7 +446,7 @@ export async function ensureObsidianGraphLinks({ vaultPath } = {}) {
390
446
  hasCoreNote,
391
447
  });
392
448
  if (linked === current) continue;
393
- await fs.writeFile(target.absolutePath, linked, "utf8");
449
+ await writeVaultNote(target.absolutePath, linked, true);
394
450
  receipts.push(
395
451
  createObsidianOperationReceipt({
396
452
  operation: "update_note",
@@ -416,7 +472,9 @@ export async function appendObsidianOperationReceipt(receipt) {
416
472
  let log = createDefaultObsidianVaultOperations();
417
473
  try {
418
474
  log = validateObsidianVaultOperations(await readJson(getObsidianVaultOperationsPath()));
419
- } catch {}
475
+ } catch (error) {
476
+ if (error?.code !== "ENOENT") throw error;
477
+ }
420
478
  log.operations.push(receipt);
421
479
  await writeJson(getObsidianVaultOperationsPath(), log);
422
480
  return log;
@@ -610,6 +668,69 @@ export async function writeObsidianVaultIndex({ vaultPath, ensureGraphLinks = tr
610
668
  return index;
611
669
  }
612
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
+
613
734
  async function upsertObsidianNoteContent({ vaultPath, notePath, content }) {
614
735
  const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: false });
615
736
  let current = null;
@@ -619,7 +740,7 @@ async function upsertObsidianNoteContent({ vaultPath, notePath, content }) {
619
740
  if (current === content) return null;
620
741
 
621
742
  await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
622
- await fs.writeFile(target.absolutePath, content, "utf8");
743
+ await writeVaultNote(target.absolutePath, content, Boolean(current));
623
744
  return createObsidianOperationReceipt({
624
745
  operation: current === null ? "create_note" : "update_note",
625
746
  target,
@@ -663,6 +784,53 @@ These linked notes are vault_context. They are not build_evidence. Build evidenc
663
784
  `;
664
785
  }
665
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
+
666
834
  function withObsidianGraphSection(content, { currentPath, hasCoreNote }) {
667
835
  const base = content
668
836
  .replace(new RegExp(`\\n${escapeRegExp(obsidianGraphSectionHeading)}[\\s\\S]*$`, "m"), "")
@@ -705,15 +873,7 @@ function createObsidianOperationReceipt({ operation, target, content }) {
705
873
  }
706
874
 
707
875
  async function resolveVaultRoot(vaultPath) {
708
- if (!vaultPath || typeof vaultPath !== "string") {
709
- throw new Error("Obsidian vault operation requires vaultPath");
710
- }
711
- const resolvedVaultPath = path.resolve(vaultPath);
712
- const stat = await fs.stat(resolvedVaultPath);
713
- if (!stat.isDirectory()) {
714
- throw new Error(`Obsidian vault path is not a directory: ${resolvedVaultPath}`);
715
- }
716
- return resolvedVaultPath;
876
+ return resolveObsidianVault(vaultPath);
717
877
  }
718
878
 
719
879
  async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
@@ -721,7 +881,10 @@ async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
721
881
  if (!notePath || typeof notePath !== "string") {
722
882
  throw new Error("Obsidian note operation requires notePath");
723
883
  }
724
- const normalizedInput = normalizeVaultPath(notePath.trim()).replace(/^\/+/, "");
884
+ const normalizedInput = path.posix
885
+ .normalize(normalizeVaultPath(notePath.trim()))
886
+ .replace(/^(\.\/)+/, "")
887
+ .replace(/^\/+/, "");
725
888
  if (
726
889
  !normalizedInput ||
727
890
  normalizedInput.startsWith("../") ||
@@ -740,6 +903,7 @@ async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
740
903
  const relativePath = normalizedInput.toLowerCase().endsWith(".md")
741
904
  ? normalizedInput
742
905
  : `${normalizedInput}.md`;
906
+ await rejectSymlinkComponents(resolvedVaultPath, relativePath);
743
907
  const absolutePath = path.resolve(resolvedVaultPath, relativePath);
744
908
  if (!absolutePath.startsWith(`${resolvedVaultPath}${path.sep}`)) {
745
909
  throw new Error("Obsidian note path resolved outside the vault");
@@ -747,9 +911,53 @@ async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
747
911
  if (mustExist && !(await pathExists(absolutePath))) {
748
912
  throw new Error(`Obsidian note does not exist: ${relativePath}`);
749
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
+ }
750
928
  return { vaultPath: resolvedVaultPath, relativePath, absolutePath };
751
929
  }
752
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
+
753
961
  async function pathExists(filePath) {
754
962
  try {
755
963
  await fs.access(filePath);
@@ -834,6 +1042,36 @@ function normalizeLinkKey(value) {
834
1042
  .toLowerCase();
835
1043
  }
836
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
+
837
1075
  function escapeRegExp(value) {
838
1076
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
839
1077
  }