decibel-tools-mcp 1.0.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 (255) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +335 -0
  3. package/dist/agentic/compiler.d.ts +21 -0
  4. package/dist/agentic/compiler.d.ts.map +1 -0
  5. package/dist/agentic/compiler.js +267 -0
  6. package/dist/agentic/compiler.js.map +1 -0
  7. package/dist/agentic/golden.d.ts +25 -0
  8. package/dist/agentic/golden.d.ts.map +1 -0
  9. package/dist/agentic/golden.js +255 -0
  10. package/dist/agentic/golden.js.map +1 -0
  11. package/dist/agentic/index.d.ts +17 -0
  12. package/dist/agentic/index.d.ts.map +1 -0
  13. package/dist/agentic/index.js +153 -0
  14. package/dist/agentic/index.js.map +1 -0
  15. package/dist/agentic/linter.d.ts +20 -0
  16. package/dist/agentic/linter.d.ts.map +1 -0
  17. package/dist/agentic/linter.js +340 -0
  18. package/dist/agentic/linter.js.map +1 -0
  19. package/dist/agentic/renderer.d.ts +17 -0
  20. package/dist/agentic/renderer.d.ts.map +1 -0
  21. package/dist/agentic/renderer.js +277 -0
  22. package/dist/agentic/renderer.js.map +1 -0
  23. package/dist/agentic/types.d.ts +199 -0
  24. package/dist/agentic/types.d.ts.map +1 -0
  25. package/dist/agentic/types.js +8 -0
  26. package/dist/agentic/types.js.map +1 -0
  27. package/dist/architectAdrs.d.ts +19 -0
  28. package/dist/architectAdrs.d.ts.map +1 -0
  29. package/dist/architectAdrs.js +123 -0
  30. package/dist/architectAdrs.js.map +1 -0
  31. package/dist/config.d.ts +8 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +19 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/dataRoot.d.ts +45 -0
  36. package/dist/dataRoot.d.ts.map +1 -0
  37. package/dist/dataRoot.js +201 -0
  38. package/dist/dataRoot.js.map +1 -0
  39. package/dist/decibelPaths.d.ts +42 -0
  40. package/dist/decibelPaths.d.ts.map +1 -0
  41. package/dist/decibelPaths.js +150 -0
  42. package/dist/decibelPaths.js.map +1 -0
  43. package/dist/httpServer.d.ts +49 -0
  44. package/dist/httpServer.d.ts.map +1 -0
  45. package/dist/httpServer.js +1472 -0
  46. package/dist/httpServer.js.map +1 -0
  47. package/dist/lib/benchmark.d.ts +110 -0
  48. package/dist/lib/benchmark.d.ts.map +1 -0
  49. package/dist/lib/benchmark.js +338 -0
  50. package/dist/lib/benchmark.js.map +1 -0
  51. package/dist/lib/supabase.d.ts +123 -0
  52. package/dist/lib/supabase.d.ts.map +1 -0
  53. package/dist/lib/supabase.js +91 -0
  54. package/dist/lib/supabase.js.map +1 -0
  55. package/dist/projectPaths.d.ts +27 -0
  56. package/dist/projectPaths.d.ts.map +1 -0
  57. package/dist/projectPaths.js +86 -0
  58. package/dist/projectPaths.js.map +1 -0
  59. package/dist/projectRegistry.d.ts +97 -0
  60. package/dist/projectRegistry.d.ts.map +1 -0
  61. package/dist/projectRegistry.js +362 -0
  62. package/dist/projectRegistry.js.map +1 -0
  63. package/dist/sentinelIssues.d.ts +44 -0
  64. package/dist/sentinelIssues.d.ts.map +1 -0
  65. package/dist/sentinelIssues.js +213 -0
  66. package/dist/sentinelIssues.js.map +1 -0
  67. package/dist/server.d.ts +3 -0
  68. package/dist/server.d.ts.map +1 -0
  69. package/dist/server.js +93 -0
  70. package/dist/server.js.map +1 -0
  71. package/dist/test.d.ts +7 -0
  72. package/dist/test.d.ts.map +1 -0
  73. package/dist/test.js +77 -0
  74. package/dist/test.js.map +1 -0
  75. package/dist/tools/agentic/index.d.ts +7 -0
  76. package/dist/tools/agentic/index.d.ts.map +1 -0
  77. package/dist/tools/agentic/index.js +180 -0
  78. package/dist/tools/agentic/index.js.map +1 -0
  79. package/dist/tools/architect/index.d.ts +9 -0
  80. package/dist/tools/architect/index.d.ts.map +1 -0
  81. package/dist/tools/architect/index.js +383 -0
  82. package/dist/tools/architect/index.js.map +1 -0
  83. package/dist/tools/architect.d.ts +19 -0
  84. package/dist/tools/architect.d.ts.map +1 -0
  85. package/dist/tools/architect.js +88 -0
  86. package/dist/tools/architect.js.map +1 -0
  87. package/dist/tools/bench.d.ts +89 -0
  88. package/dist/tools/bench.d.ts.map +1 -0
  89. package/dist/tools/bench.js +826 -0
  90. package/dist/tools/bench.js.map +1 -0
  91. package/dist/tools/context/index.d.ts +11 -0
  92. package/dist/tools/context/index.d.ts.map +1 -0
  93. package/dist/tools/context/index.js +439 -0
  94. package/dist/tools/context/index.js.map +1 -0
  95. package/dist/tools/context.d.ts +146 -0
  96. package/dist/tools/context.d.ts.map +1 -0
  97. package/dist/tools/context.js +481 -0
  98. package/dist/tools/context.js.map +1 -0
  99. package/dist/tools/crit.d.ts +63 -0
  100. package/dist/tools/crit.d.ts.map +1 -0
  101. package/dist/tools/crit.js +159 -0
  102. package/dist/tools/crit.js.map +1 -0
  103. package/dist/tools/data-inspector.d.ts +188 -0
  104. package/dist/tools/data-inspector.d.ts.map +1 -0
  105. package/dist/tools/data-inspector.js +650 -0
  106. package/dist/tools/data-inspector.js.map +1 -0
  107. package/dist/tools/deck.d.ts +11 -0
  108. package/dist/tools/deck.d.ts.map +1 -0
  109. package/dist/tools/deck.js +170 -0
  110. package/dist/tools/deck.js.map +1 -0
  111. package/dist/tools/designer/index.d.ts +6 -0
  112. package/dist/tools/designer/index.d.ts.map +1 -0
  113. package/dist/tools/designer/index.js +177 -0
  114. package/dist/tools/designer/index.js.map +1 -0
  115. package/dist/tools/designer.d.ts +20 -0
  116. package/dist/tools/designer.d.ts.map +1 -0
  117. package/dist/tools/designer.js +75 -0
  118. package/dist/tools/designer.js.map +1 -0
  119. package/dist/tools/dojo/index.d.ts +13 -0
  120. package/dist/tools/dojo/index.d.ts.map +1 -0
  121. package/dist/tools/dojo/index.js +547 -0
  122. package/dist/tools/dojo/index.js.map +1 -0
  123. package/dist/tools/dojo.d.ts +254 -0
  124. package/dist/tools/dojo.d.ts.map +1 -0
  125. package/dist/tools/dojo.js +933 -0
  126. package/dist/tools/dojo.js.map +1 -0
  127. package/dist/tools/dojoBench.d.ts +49 -0
  128. package/dist/tools/dojoBench.d.ts.map +1 -0
  129. package/dist/tools/dojoBench.js +205 -0
  130. package/dist/tools/dojoBench.js.map +1 -0
  131. package/dist/tools/dojoGraduated.d.ts +50 -0
  132. package/dist/tools/dojoGraduated.d.ts.map +1 -0
  133. package/dist/tools/dojoGraduated.js +174 -0
  134. package/dist/tools/dojoGraduated.js.map +1 -0
  135. package/dist/tools/dojoPolicy.d.ts +65 -0
  136. package/dist/tools/dojoPolicy.d.ts.map +1 -0
  137. package/dist/tools/dojoPolicy.js +263 -0
  138. package/dist/tools/dojoPolicy.js.map +1 -0
  139. package/dist/tools/friction/index.d.ts +7 -0
  140. package/dist/tools/friction/index.d.ts.map +1 -0
  141. package/dist/tools/friction/index.js +239 -0
  142. package/dist/tools/friction/index.js.map +1 -0
  143. package/dist/tools/friction.d.ts +82 -0
  144. package/dist/tools/friction.d.ts.map +1 -0
  145. package/dist/tools/friction.js +331 -0
  146. package/dist/tools/friction.js.map +1 -0
  147. package/dist/tools/index.d.ts +5 -0
  148. package/dist/tools/index.d.ts.map +1 -0
  149. package/dist/tools/index.js +52 -0
  150. package/dist/tools/index.js.map +1 -0
  151. package/dist/tools/learnings/index.d.ts +5 -0
  152. package/dist/tools/learnings/index.d.ts.map +1 -0
  153. package/dist/tools/learnings/index.js +123 -0
  154. package/dist/tools/learnings/index.js.map +1 -0
  155. package/dist/tools/learnings.d.ts +41 -0
  156. package/dist/tools/learnings.d.ts.map +1 -0
  157. package/dist/tools/learnings.js +149 -0
  158. package/dist/tools/learnings.js.map +1 -0
  159. package/dist/tools/oracle/index.d.ts +5 -0
  160. package/dist/tools/oracle/index.d.ts.map +1 -0
  161. package/dist/tools/oracle/index.js +97 -0
  162. package/dist/tools/oracle/index.js.map +1 -0
  163. package/dist/tools/oracle.d.ts +90 -0
  164. package/dist/tools/oracle.d.ts.map +1 -0
  165. package/dist/tools/oracle.js +529 -0
  166. package/dist/tools/oracle.js.map +1 -0
  167. package/dist/tools/policy.d.ts +119 -0
  168. package/dist/tools/policy.d.ts.map +1 -0
  169. package/dist/tools/policy.js +406 -0
  170. package/dist/tools/policy.js.map +1 -0
  171. package/dist/tools/provenance/index.d.ts +4 -0
  172. package/dist/tools/provenance/index.d.ts.map +1 -0
  173. package/dist/tools/provenance/index.js +58 -0
  174. package/dist/tools/provenance/index.js.map +1 -0
  175. package/dist/tools/provenance.d.ts +75 -0
  176. package/dist/tools/provenance.d.ts.map +1 -0
  177. package/dist/tools/provenance.js +197 -0
  178. package/dist/tools/provenance.js.map +1 -0
  179. package/dist/tools/rateLimiter.d.ts +45 -0
  180. package/dist/tools/rateLimiter.d.ts.map +1 -0
  181. package/dist/tools/rateLimiter.js +91 -0
  182. package/dist/tools/rateLimiter.js.map +1 -0
  183. package/dist/tools/registry/index.d.ts +10 -0
  184. package/dist/tools/registry/index.d.ts.map +1 -0
  185. package/dist/tools/registry/index.js +467 -0
  186. package/dist/tools/registry/index.js.map +1 -0
  187. package/dist/tools/registry.d.ts +3 -0
  188. package/dist/tools/registry.d.ts.map +1 -0
  189. package/dist/tools/registry.js +189 -0
  190. package/dist/tools/registry.js.map +1 -0
  191. package/dist/tools/roadmap/index.d.ts +9 -0
  192. package/dist/tools/roadmap/index.d.ts.map +1 -0
  193. package/dist/tools/roadmap/index.js +250 -0
  194. package/dist/tools/roadmap/index.js.map +1 -0
  195. package/dist/tools/roadmap.d.ts +88 -0
  196. package/dist/tools/roadmap.d.ts.map +1 -0
  197. package/dist/tools/roadmap.js +365 -0
  198. package/dist/tools/roadmap.js.map +1 -0
  199. package/dist/tools/sentinel/index.d.ts +19 -0
  200. package/dist/tools/sentinel/index.d.ts.map +1 -0
  201. package/dist/tools/sentinel/index.js +832 -0
  202. package/dist/tools/sentinel/index.js.map +1 -0
  203. package/dist/tools/sentinel-scan-data.d.ts +90 -0
  204. package/dist/tools/sentinel-scan-data.d.ts.map +1 -0
  205. package/dist/tools/sentinel-scan-data.js +122 -0
  206. package/dist/tools/sentinel-scan-data.js.map +1 -0
  207. package/dist/tools/sentinel.d.ts +156 -0
  208. package/dist/tools/sentinel.d.ts.map +1 -0
  209. package/dist/tools/sentinel.js +603 -0
  210. package/dist/tools/sentinel.js.map +1 -0
  211. package/dist/tools/shared/index.d.ts +4 -0
  212. package/dist/tools/shared/index.d.ts.map +1 -0
  213. package/dist/tools/shared/index.js +7 -0
  214. package/dist/tools/shared/index.js.map +1 -0
  215. package/dist/tools/shared/project.d.ts +17 -0
  216. package/dist/tools/shared/project.d.ts.map +1 -0
  217. package/dist/tools/shared/project.js +36 -0
  218. package/dist/tools/shared/project.js.map +1 -0
  219. package/dist/tools/shared/response.d.ts +10 -0
  220. package/dist/tools/shared/response.d.ts.map +1 -0
  221. package/dist/tools/shared/response.js +36 -0
  222. package/dist/tools/shared/response.js.map +1 -0
  223. package/dist/tools/shared/validation.d.ts +10 -0
  224. package/dist/tools/shared/validation.d.ts.map +1 -0
  225. package/dist/tools/shared/validation.js +26 -0
  226. package/dist/tools/shared/validation.js.map +1 -0
  227. package/dist/tools/studio/cloud-spine.d.ts +27 -0
  228. package/dist/tools/studio/cloud-spine.d.ts.map +1 -0
  229. package/dist/tools/studio/cloud-spine.js +773 -0
  230. package/dist/tools/studio/cloud-spine.js.map +1 -0
  231. package/dist/tools/studio/index.d.ts +154 -0
  232. package/dist/tools/studio/index.d.ts.map +1 -0
  233. package/dist/tools/studio/index.js +525 -0
  234. package/dist/tools/studio/index.js.map +1 -0
  235. package/dist/tools/testSpec.d.ts +122 -0
  236. package/dist/tools/testSpec.d.ts.map +1 -0
  237. package/dist/tools/testSpec.js +525 -0
  238. package/dist/tools/testSpec.js.map +1 -0
  239. package/dist/tools/toolsIndex.d.ts +5 -0
  240. package/dist/tools/toolsIndex.d.ts.map +1 -0
  241. package/dist/tools/toolsIndex.js +37 -0
  242. package/dist/tools/toolsIndex.js.map +1 -0
  243. package/dist/tools/types.d.ts +30 -0
  244. package/dist/tools/types.d.ts.map +1 -0
  245. package/dist/tools/types.js +7 -0
  246. package/dist/tools/types.js.map +1 -0
  247. package/dist/tools/voice/index.d.ts +8 -0
  248. package/dist/tools/voice/index.d.ts.map +1 -0
  249. package/dist/tools/voice/index.js +176 -0
  250. package/dist/tools/voice/index.js.map +1 -0
  251. package/dist/tools/voice.d.ts +291 -0
  252. package/dist/tools/voice.d.ts.map +1 -0
  253. package/dist/tools/voice.js +734 -0
  254. package/dist/tools/voice.js.map +1 -0
  255. package/package.json +54 -0
@@ -0,0 +1,933 @@
1
+ /**
2
+ * Dojo MCP Tools - AI Feature Incubator
3
+ *
4
+ * Provides MCP tools for AI to:
5
+ * - Create proposals for new features
6
+ * - Scaffold experiments from proposals
7
+ * - Run experiments in sandbox mode
8
+ * - Track wishes (capabilities AI wants)
9
+ * - View experiment results
10
+ *
11
+ * Safety: AI cannot enable/disable/graduate experiments.
12
+ * Those actions are human-only via CLI.
13
+ *
14
+ * Project-scoped: All operations require project_id and work within {project_root}/dojo
15
+ * Role-based: caller_role determines what operations are allowed
16
+ */
17
+ import { spawn } from 'child_process';
18
+ import fs from 'fs/promises';
19
+ import path from 'path';
20
+ import YAML from 'yaml';
21
+ import { log } from '../config.js';
22
+ import { ensureDir } from '../dataRoot.js';
23
+ import { resolveProjectRoot } from '../projectPaths.js';
24
+ import { getDefaultProject, listProjects } from '../projectRegistry.js';
25
+ import { enforceToolAccess, getSandboxPolicy, expandSandboxPaths } from './dojoPolicy.js';
26
+ import { emitCreateProvenance } from './provenance.js';
27
+ import { checkRateLimit, recordRequestStart, recordRequestEnd } from './rateLimiter.js';
28
+ // ============================================================================
29
+ // ID Generation Helpers (for native file operations)
30
+ // ============================================================================
31
+ /**
32
+ * Get the next sequential ID for a given prefix by scanning directory
33
+ * @param dir Directory to scan
34
+ * @param prefix ID prefix (e.g., 'WISH', 'DOJO-PROP', 'DOJO-EXP')
35
+ * @param extension File extension (default: '.yaml')
36
+ */
37
+ async function getNextSequentialId(dir, prefix, extension = '.yaml') {
38
+ try {
39
+ const files = await fs.readdir(dir);
40
+ const pattern = new RegExp(`^${prefix}-(\\d+)${extension.replace('.', '\\.')}$`);
41
+ const ids = files
42
+ .map(f => {
43
+ const match = f.match(pattern);
44
+ return match ? parseInt(match[1], 10) : NaN;
45
+ })
46
+ .filter(n => !isNaN(n));
47
+ const maxId = ids.length > 0 ? Math.max(...ids) : 0;
48
+ return `${prefix}-${String(maxId + 1).padStart(4, '0')}`;
49
+ }
50
+ catch {
51
+ return `${prefix}-0001`;
52
+ }
53
+ }
54
+ /**
55
+ * Resolve the Dojo root for a project.
56
+ * Returns the path to {project_root}/.decibel/dojo
57
+ * If projectId is not provided, attempts to use the default project.
58
+ */
59
+ export async function resolveDojoRoot(projectId) {
60
+ // If project_id provided, use it directly
61
+ if (projectId) {
62
+ const project = await resolveProjectRoot(projectId);
63
+ const dojoRoot = path.join(project.root, '.decibel', 'dojo');
64
+ log(`dojo: Resolved project "${projectId}" to Dojo root: ${dojoRoot}`);
65
+ return { projectId, projectRoot: project.root, dojoRoot };
66
+ }
67
+ // Try to get default project
68
+ const defaultProject = getDefaultProject();
69
+ if (defaultProject) {
70
+ const dojoRoot = path.join(defaultProject.path, '.decibel', 'dojo');
71
+ log(`dojo: Using default project "${defaultProject.id}" -> Dojo root: ${dojoRoot}`);
72
+ return { projectId: defaultProject.id, projectRoot: defaultProject.path, dojoRoot };
73
+ }
74
+ // No project could be resolved - provide helpful error
75
+ const registered = listProjects();
76
+ let errorMsg = 'No project specified and no default project could be determined.';
77
+ if (registered.length > 0) {
78
+ errorMsg += ` Available projects: ${registered.map(p => p.id).join(', ')}.`;
79
+ errorMsg += ' Specify project_id or set one as default with DECIBEL_DEFAULT_PROJECT env var.';
80
+ }
81
+ else {
82
+ errorMsg += ' No projects registered. Register a project with "decibel registry add".';
83
+ }
84
+ throw new Error(errorMsg);
85
+ }
86
+ /**
87
+ * Build a full Dojo context with policy and rate limit enforcement
88
+ */
89
+ export async function buildDojoContext(projectId, callerRole = 'human', toolName, agentId) {
90
+ // Check rate limits first (for AI callers)
91
+ const rateLimitResult = checkRateLimit(callerRole);
92
+ if (!rateLimitResult.allowed) {
93
+ throw new Error(`Rate limit: ${rateLimitResult.reason}`);
94
+ }
95
+ // Enforce policy
96
+ enforceToolAccess(toolName, callerRole);
97
+ // Record request start (for concurrent tracking)
98
+ recordRequestStart(callerRole);
99
+ // Resolve paths (handles default project fallback)
100
+ const resolved = await resolveDojoRoot(projectId);
101
+ // Audit log for AI callers
102
+ if (callerRole !== 'human') {
103
+ log(`dojo-audit: [${new Date().toISOString()}] agent=${agentId || 'unknown'} role=${callerRole} tool=${toolName} project=${resolved.projectId}`);
104
+ }
105
+ return {
106
+ projectId: resolved.projectId,
107
+ projectRoot: resolved.projectRoot,
108
+ dojoRoot: resolved.dojoRoot,
109
+ callerRole,
110
+ agentId,
111
+ };
112
+ }
113
+ /**
114
+ * Mark a Dojo request as complete (call in finally block)
115
+ */
116
+ export function finishDojoRequest(callerRole = 'human') {
117
+ recordRequestEnd(callerRole);
118
+ }
119
+ // ============================================================================
120
+ // Tool Implementations
121
+ // ============================================================================
122
+ /**
123
+ * Create a new Dojo proposal (native file operations)
124
+ */
125
+ export async function createProposal(input) {
126
+ const callerRole = input.caller_role || 'human';
127
+ // Build context with policy enforcement
128
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_create_proposal', input.agent_id);
129
+ try {
130
+ const timestamp = new Date().toISOString();
131
+ const proposalDir = path.join(ctx.dojoRoot, 'proposals');
132
+ ensureDir(proposalDir);
133
+ // Generate next sequential proposal ID
134
+ const proposalId = await getNextSequentialId(proposalDir, 'DOJO-PROP');
135
+ const proposalPath = path.join(proposalDir, `${proposalId}.yaml`);
136
+ // Build proposal data structure
137
+ const proposalData = {
138
+ id: proposalId,
139
+ title: input.title,
140
+ problem: input.problem,
141
+ hypothesis: input.hypothesis,
142
+ owner: input.owner || 'ai',
143
+ state: 'draft',
144
+ created_at: timestamp,
145
+ project_id: ctx.projectId,
146
+ };
147
+ // Optional fields
148
+ if (input.target_module) {
149
+ proposalData.target_module = input.target_module;
150
+ }
151
+ if (input.scope_in && input.scope_in.length > 0) {
152
+ proposalData.scope_in = input.scope_in;
153
+ }
154
+ if (input.scope_out && input.scope_out.length > 0) {
155
+ proposalData.scope_out = input.scope_out;
156
+ }
157
+ if (input.acceptance && input.acceptance.length > 0) {
158
+ proposalData.acceptance = input.acceptance;
159
+ }
160
+ if (input.risks && input.risks.length > 0) {
161
+ proposalData.risks = input.risks;
162
+ }
163
+ if (input.follows) {
164
+ proposalData.follows = input.follows;
165
+ }
166
+ if (input.insight) {
167
+ proposalData.insight = input.insight;
168
+ }
169
+ if (input.wish_id) {
170
+ proposalData.wish_id = input.wish_id;
171
+ // Mark wish as resolved if it exists
172
+ const wishPath = path.join(ctx.dojoRoot, 'wishes', `${input.wish_id}.yaml`);
173
+ try {
174
+ const wishContent = await fs.readFile(wishPath, 'utf-8');
175
+ const wishData = YAML.parse(wishContent);
176
+ wishData.status = 'resolved';
177
+ wishData.resolved_by = proposalId;
178
+ wishData.resolved_at = timestamp;
179
+ await fs.writeFile(wishPath, YAML.stringify(wishData), 'utf-8');
180
+ log(`dojo: Marked wish ${input.wish_id} as resolved by ${proposalId}`);
181
+ }
182
+ catch {
183
+ // Wish doesn't exist or can't be updated - continue anyway
184
+ log(`dojo: Could not update wish ${input.wish_id}`);
185
+ }
186
+ }
187
+ // Write YAML file
188
+ await fs.writeFile(proposalPath, YAML.stringify(proposalData), 'utf-8');
189
+ log(`dojo: Created proposal ${proposalId} at ${proposalPath}`);
190
+ // Emit provenance
191
+ await emitCreateProvenance(`dojo:proposal:${proposalId}`, YAML.stringify(proposalData), `Created proposal: ${input.title}`, ctx.projectId);
192
+ return {
193
+ proposal_id: proposalId,
194
+ filepath: proposalPath,
195
+ title: input.title,
196
+ };
197
+ }
198
+ finally {
199
+ finishDojoRequest(callerRole);
200
+ }
201
+ }
202
+ /**
203
+ * Scaffold an experiment from a proposal (native file operations)
204
+ */
205
+ export async function scaffoldExperiment(input) {
206
+ const callerRole = input.caller_role || 'human';
207
+ // Build context with policy enforcement
208
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_scaffold_experiment', input.agent_id);
209
+ try {
210
+ const timestamp = new Date().toISOString();
211
+ const scriptType = input.script_type || 'py';
212
+ const experimentType = input.experiment_type || 'script';
213
+ // Read proposal to get info
214
+ const proposalPath = path.join(ctx.dojoRoot, 'proposals', `${input.proposal_id}.yaml`);
215
+ let proposalData;
216
+ try {
217
+ const proposalContent = await fs.readFile(proposalPath, 'utf-8');
218
+ proposalData = YAML.parse(proposalContent);
219
+ }
220
+ catch {
221
+ return {
222
+ error: `Proposal not found: ${input.proposal_id}`,
223
+ exitCode: 1,
224
+ stderr: `Could not read proposal file: ${proposalPath}`,
225
+ };
226
+ }
227
+ // Generate experiment directory
228
+ const experimentsDir = path.join(ctx.dojoRoot, 'experiments');
229
+ const experimentId = await getNextSequentialId(experimentsDir, 'DOJO-EXP', '');
230
+ const experimentDir = path.join(experimentsDir, experimentId);
231
+ ensureDir(experimentDir);
232
+ // Create manifest.yaml
233
+ const manifestData = {
234
+ id: experimentId,
235
+ proposal_id: input.proposal_id,
236
+ title: proposalData.title || 'Untitled Experiment',
237
+ type: experimentType,
238
+ script_type: scriptType,
239
+ enabled: false,
240
+ created_at: timestamp,
241
+ project_id: ctx.projectId,
242
+ };
243
+ const manifestPath = path.join(experimentDir, 'manifest.yaml');
244
+ await fs.writeFile(manifestPath, YAML.stringify(manifestData), 'utf-8');
245
+ // Create run script
246
+ const entrypoint = scriptType === 'ts' ? 'run.ts' : 'run.py';
247
+ const scriptPath = path.join(experimentDir, entrypoint);
248
+ const scriptContent = scriptType === 'ts'
249
+ ? `#!/usr/bin/env npx ts-node
250
+ /**
251
+ * Experiment: ${experimentId}
252
+ * Proposal: ${input.proposal_id}
253
+ * Title: ${proposalData.title || 'Untitled'}
254
+ */
255
+
256
+ async function main() {
257
+ console.log('Running experiment ${experimentId}...');
258
+
259
+ // TODO: Implement experiment logic
260
+
261
+ console.log('Experiment complete.');
262
+ }
263
+
264
+ main().catch(console.error);
265
+ `
266
+ : `#!/usr/bin/env python3
267
+ """
268
+ Experiment: ${experimentId}
269
+ Proposal: ${input.proposal_id}
270
+ Title: ${proposalData.title || 'Untitled'}
271
+ """
272
+
273
+ def main():
274
+ print(f"Running experiment ${experimentId}...")
275
+
276
+ # TODO: Implement experiment logic
277
+
278
+ print("Experiment complete.")
279
+
280
+ if __name__ == "__main__":
281
+ main()
282
+ `;
283
+ await fs.writeFile(scriptPath, scriptContent, 'utf-8');
284
+ // Create README.md
285
+ const readmePath = path.join(experimentDir, 'README.md');
286
+ const readmeContent = `# ${experimentId}: ${proposalData.title || 'Untitled'}
287
+
288
+ ## Proposal
289
+ ${input.proposal_id}
290
+
291
+ ## Problem
292
+ ${proposalData.problem || 'N/A'}
293
+
294
+ ## Hypothesis
295
+ ${proposalData.hypothesis || 'N/A'}
296
+
297
+ ## Running
298
+ \`\`\`bash
299
+ ${scriptType === 'ts' ? 'npx ts-node run.ts' : 'python3 run.py'}
300
+ \`\`\`
301
+
302
+ ## Results
303
+ Results will be written to \`.decibel/dojo/results/${experimentId}/\`
304
+ `;
305
+ await fs.writeFile(readmePath, readmeContent, 'utf-8');
306
+ // Update proposal state
307
+ proposalData.state = 'has_experiment';
308
+ proposalData.experiment_id = experimentId;
309
+ await fs.writeFile(proposalPath, YAML.stringify(proposalData), 'utf-8');
310
+ const filesCreated = ['manifest.yaml', entrypoint, 'README.md'];
311
+ log(`dojo: Scaffolded experiment ${experimentId} at ${experimentDir}`);
312
+ // Emit provenance
313
+ await emitCreateProvenance(`dojo:experiment:${experimentId}`, YAML.stringify(manifestData), `Scaffolded experiment from ${input.proposal_id}`, ctx.projectId);
314
+ return {
315
+ experiment_id: experimentId,
316
+ proposal_id: input.proposal_id,
317
+ directory: experimentDir,
318
+ entrypoint,
319
+ files_created: filesCreated,
320
+ };
321
+ }
322
+ finally {
323
+ finishDojoRequest(callerRole);
324
+ }
325
+ }
326
+ /**
327
+ * List proposals, experiments, and wishes (native file operations)
328
+ */
329
+ export async function listDojo(input) {
330
+ const callerRole = input.caller_role || 'human';
331
+ // Build context with policy enforcement
332
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_list', input.agent_id);
333
+ try {
334
+ const filter = input.filter || 'all';
335
+ const proposals = [];
336
+ const experiments = [];
337
+ const wishes = [];
338
+ let enabledCount = 0;
339
+ // Read proposals
340
+ if (filter === 'all' || filter === 'proposals') {
341
+ const proposalDir = path.join(ctx.dojoRoot, 'proposals');
342
+ try {
343
+ const files = await fs.readdir(proposalDir);
344
+ for (const file of files) {
345
+ if (!file.endsWith('.yaml'))
346
+ continue;
347
+ try {
348
+ const content = await fs.readFile(path.join(proposalDir, file), 'utf-8');
349
+ const data = YAML.parse(content);
350
+ proposals.push({
351
+ id: data.id || file.replace('.yaml', ''),
352
+ title: data.title || 'Untitled',
353
+ owner: data.owner || 'ai',
354
+ state: data.state || 'draft',
355
+ });
356
+ }
357
+ catch {
358
+ // Skip malformed files
359
+ }
360
+ }
361
+ }
362
+ catch {
363
+ // Directory doesn't exist
364
+ }
365
+ }
366
+ // Read experiments
367
+ if (filter === 'all' || filter === 'experiments') {
368
+ const experimentsDir = path.join(ctx.dojoRoot, 'experiments');
369
+ try {
370
+ const dirs = await fs.readdir(experimentsDir);
371
+ for (const dir of dirs) {
372
+ const manifestPath = path.join(experimentsDir, dir, 'manifest.yaml');
373
+ try {
374
+ const content = await fs.readFile(manifestPath, 'utf-8');
375
+ const data = YAML.parse(content);
376
+ const isEnabled = data.enabled === true;
377
+ if (isEnabled)
378
+ enabledCount++;
379
+ experiments.push({
380
+ id: data.id || dir,
381
+ proposal_id: data.proposal_id || 'unknown',
382
+ title: data.title || 'Untitled',
383
+ type: data.type || 'script',
384
+ enabled: isEnabled,
385
+ });
386
+ }
387
+ catch {
388
+ // Skip directories without manifest
389
+ }
390
+ }
391
+ }
392
+ catch {
393
+ // Directory doesn't exist
394
+ }
395
+ }
396
+ // Read wishes
397
+ if (filter === 'all' || filter === 'wishes') {
398
+ const wishDir = path.join(ctx.dojoRoot, 'wishes');
399
+ try {
400
+ const files = await fs.readdir(wishDir);
401
+ for (const file of files) {
402
+ if (!file.endsWith('.yaml'))
403
+ continue;
404
+ try {
405
+ const content = await fs.readFile(path.join(wishDir, file), 'utf-8');
406
+ const data = YAML.parse(content);
407
+ wishes.push({
408
+ id: data.id || file.replace('.yaml', ''),
409
+ capability: data.capability || 'Unknown',
410
+ reason: data.reason || 'Unknown',
411
+ resolved_by: data.resolved_by,
412
+ });
413
+ }
414
+ catch {
415
+ // Skip malformed files
416
+ }
417
+ }
418
+ }
419
+ catch {
420
+ // Directory doesn't exist
421
+ }
422
+ }
423
+ return {
424
+ proposals,
425
+ experiments,
426
+ wishes,
427
+ summary: {
428
+ total_proposals: proposals.length,
429
+ total_experiments: experiments.length,
430
+ total_wishes: wishes.length,
431
+ enabled_count: enabledCount,
432
+ },
433
+ };
434
+ }
435
+ finally {
436
+ finishDojoRequest(callerRole);
437
+ }
438
+ }
439
+ /**
440
+ * Run an experiment in sandbox mode (native file operations)
441
+ * Note: This still uses spawn() to run the experiment script, but not the CLI
442
+ */
443
+ export async function runExperiment(input) {
444
+ const callerRole = input.caller_role || 'human';
445
+ // Build context with policy enforcement
446
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_run_experiment', input.agent_id);
447
+ try {
448
+ // Get sandbox policy for the caller
449
+ const sandbox = getSandboxPolicy(ctx.callerRole);
450
+ expandSandboxPaths(sandbox, ctx.dojoRoot);
451
+ // Read experiment manifest
452
+ const experimentDir = path.join(ctx.dojoRoot, 'experiments', input.experiment_id);
453
+ const manifestPath = path.join(experimentDir, 'manifest.yaml');
454
+ let manifestData;
455
+ try {
456
+ const content = await fs.readFile(manifestPath, 'utf-8');
457
+ manifestData = YAML.parse(content);
458
+ }
459
+ catch {
460
+ return {
461
+ error: `Experiment not found: ${input.experiment_id}`,
462
+ exitCode: 1,
463
+ stderr: `Could not read manifest: ${manifestPath}`,
464
+ };
465
+ }
466
+ // Determine entrypoint and runner
467
+ const scriptType = manifestData.script_type || 'py';
468
+ const entrypoint = scriptType === 'ts' ? 'run.ts' : 'run.py';
469
+ const scriptPath = path.join(experimentDir, entrypoint);
470
+ // Check script exists
471
+ try {
472
+ await fs.access(scriptPath);
473
+ }
474
+ catch {
475
+ return {
476
+ error: `Experiment script not found: ${entrypoint}`,
477
+ exitCode: 1,
478
+ stderr: `Script does not exist: ${scriptPath}`,
479
+ };
480
+ }
481
+ // Generate run ID
482
+ const now = new Date();
483
+ const runId = now.toISOString().replace(/[-:]/g, '').slice(0, 15).replace('T', '-');
484
+ // Create results directory
485
+ const resultsDir = path.join(ctx.dojoRoot, 'results', input.experiment_id, runId);
486
+ ensureDir(resultsDir);
487
+ const startTime = Date.now();
488
+ // Run the experiment script
489
+ const runner = scriptType === 'ts' ? 'npx' : 'python3';
490
+ const runnerArgs = scriptType === 'ts' ? ['ts-node', entrypoint] : [entrypoint];
491
+ const { stdout, exitCode } = await new Promise((resolve) => {
492
+ const proc = spawn(runner, runnerArgs, {
493
+ cwd: experimentDir,
494
+ stdio: ['ignore', 'pipe', 'pipe'],
495
+ env: {
496
+ ...process.env,
497
+ DOJO_RESULTS_DIR: resultsDir,
498
+ DOJO_EXPERIMENT_ID: input.experiment_id,
499
+ DOJO_RUN_ID: runId,
500
+ },
501
+ });
502
+ let stdout = '';
503
+ let stderr = '';
504
+ proc.stdout.on('data', (data) => {
505
+ stdout += data.toString();
506
+ });
507
+ proc.stderr.on('data', (data) => {
508
+ stderr += data.toString();
509
+ });
510
+ proc.on('error', (err) => {
511
+ log(`dojo: Experiment run error: ${err.message}`);
512
+ resolve({ stdout: stderr || err.message, exitCode: -1 });
513
+ });
514
+ proc.on('close', (code) => {
515
+ resolve({ stdout: stdout + stderr, exitCode: code ?? -1 });
516
+ });
517
+ });
518
+ const endTime = Date.now();
519
+ const durationSeconds = (endTime - startTime) / 1000;
520
+ // Write result metadata
521
+ const resultMeta = {
522
+ experiment_id: input.experiment_id,
523
+ run_id: runId,
524
+ exit_code: exitCode,
525
+ duration_seconds: durationSeconds,
526
+ timestamp: now.toISOString(),
527
+ status: exitCode === 0 ? 'success' : 'failure',
528
+ };
529
+ await fs.writeFile(path.join(resultsDir, 'result.yaml'), YAML.stringify(resultMeta), 'utf-8');
530
+ // Save stdout
531
+ if (stdout) {
532
+ await fs.writeFile(path.join(resultsDir, 'stdout.log'), stdout, 'utf-8');
533
+ }
534
+ // Scan results directory for artifacts
535
+ let artifacts = [];
536
+ try {
537
+ const files = await fs.readdir(resultsDir);
538
+ artifacts = files.filter(f => !f.startsWith('.'));
539
+ }
540
+ catch {
541
+ log(`dojo: Could not read results dir: ${resultsDir}`);
542
+ }
543
+ log(`dojo: Experiment ${input.experiment_id} run ${runId} completed with exit code ${exitCode}`);
544
+ return {
545
+ experiment_id: input.experiment_id,
546
+ run_id: runId,
547
+ status: exitCode === 0 ? 'success' : 'failure',
548
+ exit_code: exitCode,
549
+ duration_seconds: durationSeconds,
550
+ artifacts,
551
+ stdout,
552
+ };
553
+ }
554
+ finally {
555
+ finishDojoRequest(callerRole);
556
+ }
557
+ }
558
+ /**
559
+ * Get results from a previous experiment run (native file operations)
560
+ */
561
+ export async function getExperimentResults(input) {
562
+ const callerRole = input.caller_role || 'human';
563
+ // Build context with policy enforcement
564
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_get_results', input.agent_id);
565
+ try {
566
+ const experimentResultsDir = path.join(ctx.dojoRoot, 'results', input.experiment_id);
567
+ // Get run ID - either specified, or find latest
568
+ let runId = input.run_id;
569
+ if (!runId) {
570
+ // Find the latest run by listing directories and sorting
571
+ try {
572
+ const runs = await fs.readdir(experimentResultsDir);
573
+ const sortedRuns = runs.filter(r => !r.startsWith('.')).sort().reverse();
574
+ if (sortedRuns.length === 0) {
575
+ return {
576
+ error: `No runs found for experiment: ${input.experiment_id}`,
577
+ exitCode: 1,
578
+ stderr: 'No result directories found',
579
+ };
580
+ }
581
+ runId = sortedRuns[0];
582
+ }
583
+ catch {
584
+ return {
585
+ error: `No results found for experiment: ${input.experiment_id}`,
586
+ exitCode: 1,
587
+ stderr: `Results directory does not exist: ${experimentResultsDir}`,
588
+ };
589
+ }
590
+ }
591
+ const resultsDir = path.join(experimentResultsDir, runId);
592
+ // Read result metadata
593
+ let resultMeta = {};
594
+ let stdout = '';
595
+ let stderr = '';
596
+ try {
597
+ const resultPath = path.join(resultsDir, 'result.yaml');
598
+ const content = await fs.readFile(resultPath, 'utf-8');
599
+ resultMeta = YAML.parse(content);
600
+ }
601
+ catch {
602
+ // No result.yaml, try to infer from files
603
+ }
604
+ // Try to read stdout log
605
+ try {
606
+ stdout = await fs.readFile(path.join(resultsDir, 'stdout.log'), 'utf-8');
607
+ }
608
+ catch {
609
+ // No stdout log
610
+ }
611
+ // Try to read stderr log
612
+ try {
613
+ stderr = await fs.readFile(path.join(resultsDir, 'stderr.log'), 'utf-8');
614
+ }
615
+ catch {
616
+ // No stderr log
617
+ }
618
+ // Scan results directory for artifacts
619
+ let artifacts = [];
620
+ try {
621
+ const files = await fs.readdir(resultsDir);
622
+ artifacts = files.filter(f => !f.startsWith('.'));
623
+ }
624
+ catch {
625
+ log(`dojo: Could not read results dir: ${resultsDir}`);
626
+ }
627
+ return {
628
+ experiment_id: input.experiment_id,
629
+ run_id: runId,
630
+ timestamp: resultMeta.timestamp || new Date().toISOString(),
631
+ exit_code: resultMeta.exit_code ?? 0,
632
+ artifacts,
633
+ stdout: stdout || undefined,
634
+ stderr: stderr || undefined,
635
+ };
636
+ }
637
+ finally {
638
+ finishDojoRequest(callerRole);
639
+ }
640
+ }
641
+ /**
642
+ * Add a wish to the wishlist (native file operations)
643
+ */
644
+ export async function addWish(input) {
645
+ const callerRole = input.caller_role || 'human';
646
+ // Build context with policy enforcement
647
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_add_wish', input.agent_id);
648
+ try {
649
+ const timestamp = new Date().toISOString();
650
+ const wishDir = path.join(ctx.dojoRoot, 'wishes');
651
+ ensureDir(wishDir);
652
+ // Generate next sequential wish ID
653
+ const wishId = await getNextSequentialId(wishDir, 'WISH');
654
+ const wishPath = path.join(wishDir, `${wishId}.yaml`);
655
+ // Build wish data structure
656
+ const wishData = {
657
+ id: wishId,
658
+ capability: input.capability,
659
+ reason: input.reason,
660
+ inputs: input.inputs || [],
661
+ outputs: input.outputs || {},
662
+ created_at: timestamp,
663
+ status: 'open',
664
+ project_id: ctx.projectId,
665
+ };
666
+ // Optional fields
667
+ if (input.integration_point) {
668
+ wishData.integration_point = input.integration_point;
669
+ }
670
+ if (input.success_metric) {
671
+ wishData.success_metric = input.success_metric;
672
+ }
673
+ if (input.risks && input.risks.length > 0) {
674
+ wishData.risks = input.risks;
675
+ }
676
+ if (input.mvp) {
677
+ wishData.mvp = input.mvp;
678
+ }
679
+ if (input.algorithm_outline) {
680
+ wishData.algorithm_outline = input.algorithm_outline;
681
+ }
682
+ if (input.context) {
683
+ wishData.context = input.context;
684
+ }
685
+ // Write YAML file
686
+ await fs.writeFile(wishPath, YAML.stringify(wishData), 'utf-8');
687
+ log(`dojo: Created wish ${wishId} at ${wishPath}`);
688
+ // Emit provenance
689
+ await emitCreateProvenance(`dojo:wish:${wishId}`, YAML.stringify(wishData), `Created wish: ${input.capability}`, ctx.projectId);
690
+ return {
691
+ wish_id: wishId,
692
+ capability: input.capability,
693
+ timestamp,
694
+ };
695
+ }
696
+ finally {
697
+ finishDojoRequest(callerRole);
698
+ }
699
+ }
700
+ /**
701
+ * List wishes (native file operations)
702
+ */
703
+ export async function listWishes(input) {
704
+ const callerRole = input.caller_role || 'human';
705
+ // Build context with policy enforcement
706
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_list_wishes', input.agent_id);
707
+ try {
708
+ const wishDir = path.join(ctx.dojoRoot, 'wishes');
709
+ const wishes = [];
710
+ let unresolvedCount = 0;
711
+ try {
712
+ const files = await fs.readdir(wishDir);
713
+ for (const file of files) {
714
+ if (!file.endsWith('.yaml'))
715
+ continue;
716
+ const wishPath = path.join(wishDir, file);
717
+ try {
718
+ const content = await fs.readFile(wishPath, 'utf-8');
719
+ const data = YAML.parse(content);
720
+ const isResolved = data.status === 'resolved' || !!data.resolved_by;
721
+ // Apply filter
722
+ if (input.unresolved_only && isResolved)
723
+ continue;
724
+ if (!isResolved)
725
+ unresolvedCount++;
726
+ wishes.push({
727
+ id: data.id || file.replace('.yaml', ''),
728
+ capability: data.capability || 'Unknown',
729
+ reason: data.reason || 'Unknown',
730
+ resolved_by: data.resolved_by,
731
+ });
732
+ }
733
+ catch {
734
+ // Skip malformed files
735
+ log(`dojo: Could not parse wish file: ${wishPath}`);
736
+ }
737
+ }
738
+ }
739
+ catch {
740
+ // Directory doesn't exist yet - return empty
741
+ }
742
+ return {
743
+ wishes,
744
+ total: wishes.length,
745
+ unresolved: unresolvedCount,
746
+ };
747
+ }
748
+ finally {
749
+ finishDojoRequest(callerRole);
750
+ }
751
+ }
752
+ /**
753
+ * Check if an experiment can be graduated (read-only)
754
+ */
755
+ export async function canGraduate(input) {
756
+ const callerRole = input.caller_role || 'human';
757
+ // Build context with policy enforcement
758
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_can_graduate', input.agent_id);
759
+ try {
760
+ // Read experiment manifest to check graduation eligibility
761
+ const manifestPath = path.join(ctx.dojoRoot, 'experiments', input.experiment_id, 'manifest.yaml');
762
+ let manifestData;
763
+ try {
764
+ const content = await fs.readFile(manifestPath, 'utf-8');
765
+ manifestData = YAML.parse(content);
766
+ }
767
+ catch {
768
+ return {
769
+ error: `Experiment not found: ${input.experiment_id}`,
770
+ exitCode: 1,
771
+ stderr: `Could not read manifest: ${manifestPath}`,
772
+ };
773
+ }
774
+ // Check graduation criteria
775
+ const isEnabled = manifestData.enabled === true;
776
+ const hasToolDef = manifestData.type === 'tool';
777
+ // Check if there are successful runs
778
+ let hasSuccessfulRun = false;
779
+ const resultsDir = path.join(ctx.dojoRoot, 'results', input.experiment_id);
780
+ try {
781
+ const runs = await fs.readdir(resultsDir);
782
+ for (const run of runs) {
783
+ const resultPath = path.join(resultsDir, run, 'result.yaml');
784
+ try {
785
+ const resultContent = await fs.readFile(resultPath, 'utf-8');
786
+ const resultData = YAML.parse(resultContent);
787
+ if (resultData.exit_code === 0 || resultData.status === 'success') {
788
+ hasSuccessfulRun = true;
789
+ break;
790
+ }
791
+ }
792
+ catch {
793
+ // Skip runs without result files
794
+ }
795
+ }
796
+ }
797
+ catch {
798
+ // No results directory
799
+ }
800
+ const reasons = [];
801
+ reasons.push(isEnabled ? 'Experiment is enabled' : 'Experiment not enabled yet');
802
+ reasons.push(hasToolDef ? 'Has tool definition' : 'No tool definition (type != tool)');
803
+ reasons.push(hasSuccessfulRun ? 'Has at least one successful run' : 'No successful runs yet');
804
+ return {
805
+ experiment_id: input.experiment_id,
806
+ can_graduate: isEnabled && hasToolDef && hasSuccessfulRun,
807
+ has_tool_definition: hasToolDef,
808
+ is_enabled: isEnabled,
809
+ reasons,
810
+ };
811
+ }
812
+ finally {
813
+ finishDojoRequest(callerRole);
814
+ }
815
+ }
816
+ /**
817
+ * Read an artifact file from experiment results
818
+ * Returns parsed content for yaml/json, raw text for others
819
+ */
820
+ export async function readArtifact(input) {
821
+ const callerRole = input.caller_role || 'human';
822
+ // Build context with policy enforcement
823
+ const ctx = await buildDojoContext(input.project_id, callerRole, 'dojo_read_artifact', input.agent_id);
824
+ try {
825
+ // Validate filename (no path traversal)
826
+ if (input.filename.includes('/') || input.filename.includes('..')) {
827
+ return {
828
+ error: 'Invalid filename: path traversal not allowed',
829
+ exitCode: 1,
830
+ stderr: 'Filename must be a simple filename without path separators',
831
+ };
832
+ }
833
+ // Build path to artifact
834
+ const artifactPath = path.join(ctx.dojoRoot, 'results', input.experiment_id, input.run_id, input.filename);
835
+ // Check file exists
836
+ try {
837
+ await fs.access(artifactPath);
838
+ }
839
+ catch {
840
+ return {
841
+ error: `Artifact not found: ${input.filename}`,
842
+ exitCode: 1,
843
+ stderr: `File does not exist: ${artifactPath}`,
844
+ };
845
+ }
846
+ // Determine content type from extension
847
+ const ext = path.extname(input.filename).toLowerCase();
848
+ let contentType;
849
+ let content;
850
+ if (ext === '.yaml' || ext === '.yml') {
851
+ contentType = 'yaml';
852
+ const raw = await fs.readFile(artifactPath, 'utf-8');
853
+ try {
854
+ content = YAML.parse(raw);
855
+ }
856
+ catch {
857
+ content = raw; // Return raw if parsing fails
858
+ }
859
+ }
860
+ else if (ext === '.json') {
861
+ contentType = 'json';
862
+ const raw = await fs.readFile(artifactPath, 'utf-8');
863
+ try {
864
+ content = JSON.parse(raw);
865
+ }
866
+ catch {
867
+ content = raw;
868
+ }
869
+ }
870
+ else if (['.txt', '.log', '.md', '.py', '.ts', '.js'].includes(ext)) {
871
+ contentType = 'text';
872
+ content = await fs.readFile(artifactPath, 'utf-8');
873
+ }
874
+ else {
875
+ // Binary files (images, etc.) - return base64
876
+ contentType = 'binary';
877
+ const buffer = await fs.readFile(artifactPath);
878
+ content = buffer.toString('base64');
879
+ }
880
+ return {
881
+ experiment_id: input.experiment_id,
882
+ run_id: input.run_id,
883
+ filename: input.filename,
884
+ content_type: contentType,
885
+ content,
886
+ };
887
+ }
888
+ finally {
889
+ finishDojoRequest(callerRole);
890
+ }
891
+ }
892
+ /**
893
+ * Type guard for DojoError
894
+ */
895
+ export function isDojoError(result) {
896
+ return (typeof result === 'object' &&
897
+ result !== null &&
898
+ 'error' in result &&
899
+ 'exitCode' in result);
900
+ }
901
+ /**
902
+ * List all available projects for Dojo operations.
903
+ * Helps AI callers discover what projects they can use.
904
+ */
905
+ export function dojoListProjects() {
906
+ const registered = listProjects();
907
+ const defaultProject = getDefaultProject();
908
+ const projects = registered.map((p) => ({
909
+ id: p.id,
910
+ name: p.name,
911
+ path: p.path,
912
+ aliases: p.aliases,
913
+ isDefault: p.default === true || p.id === defaultProject?.id,
914
+ }));
915
+ // If there's a discovered project not in registry, add it
916
+ if (defaultProject && !registered.find((p) => p.id === defaultProject.id)) {
917
+ projects.push({
918
+ id: defaultProject.id,
919
+ name: undefined,
920
+ path: defaultProject.path,
921
+ aliases: undefined,
922
+ isDefault: true,
923
+ });
924
+ }
925
+ return {
926
+ projects,
927
+ defaultProject: defaultProject?.id,
928
+ hint: defaultProject
929
+ ? `Default project: "${defaultProject.id}". You can omit project_id to use it.`
930
+ : 'No default project. Specify project_id in your requests.',
931
+ };
932
+ }
933
+ //# sourceMappingURL=dojo.js.map