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.
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/dist/agentic/compiler.d.ts +21 -0
- package/dist/agentic/compiler.d.ts.map +1 -0
- package/dist/agentic/compiler.js +267 -0
- package/dist/agentic/compiler.js.map +1 -0
- package/dist/agentic/golden.d.ts +25 -0
- package/dist/agentic/golden.d.ts.map +1 -0
- package/dist/agentic/golden.js +255 -0
- package/dist/agentic/golden.js.map +1 -0
- package/dist/agentic/index.d.ts +17 -0
- package/dist/agentic/index.d.ts.map +1 -0
- package/dist/agentic/index.js +153 -0
- package/dist/agentic/index.js.map +1 -0
- package/dist/agentic/linter.d.ts +20 -0
- package/dist/agentic/linter.d.ts.map +1 -0
- package/dist/agentic/linter.js +340 -0
- package/dist/agentic/linter.js.map +1 -0
- package/dist/agentic/renderer.d.ts +17 -0
- package/dist/agentic/renderer.d.ts.map +1 -0
- package/dist/agentic/renderer.js +277 -0
- package/dist/agentic/renderer.js.map +1 -0
- package/dist/agentic/types.d.ts +199 -0
- package/dist/agentic/types.d.ts.map +1 -0
- package/dist/agentic/types.js +8 -0
- package/dist/agentic/types.js.map +1 -0
- package/dist/architectAdrs.d.ts +19 -0
- package/dist/architectAdrs.d.ts.map +1 -0
- package/dist/architectAdrs.js +123 -0
- package/dist/architectAdrs.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/dataRoot.d.ts +45 -0
- package/dist/dataRoot.d.ts.map +1 -0
- package/dist/dataRoot.js +201 -0
- package/dist/dataRoot.js.map +1 -0
- package/dist/decibelPaths.d.ts +42 -0
- package/dist/decibelPaths.d.ts.map +1 -0
- package/dist/decibelPaths.js +150 -0
- package/dist/decibelPaths.js.map +1 -0
- package/dist/httpServer.d.ts +49 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +1472 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/lib/benchmark.d.ts +110 -0
- package/dist/lib/benchmark.d.ts.map +1 -0
- package/dist/lib/benchmark.js +338 -0
- package/dist/lib/benchmark.js.map +1 -0
- package/dist/lib/supabase.d.ts +123 -0
- package/dist/lib/supabase.d.ts.map +1 -0
- package/dist/lib/supabase.js +91 -0
- package/dist/lib/supabase.js.map +1 -0
- package/dist/projectPaths.d.ts +27 -0
- package/dist/projectPaths.d.ts.map +1 -0
- package/dist/projectPaths.js +86 -0
- package/dist/projectPaths.js.map +1 -0
- package/dist/projectRegistry.d.ts +97 -0
- package/dist/projectRegistry.d.ts.map +1 -0
- package/dist/projectRegistry.js +362 -0
- package/dist/projectRegistry.js.map +1 -0
- package/dist/sentinelIssues.d.ts +44 -0
- package/dist/sentinelIssues.d.ts.map +1 -0
- package/dist/sentinelIssues.js +213 -0
- package/dist/sentinelIssues.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +93 -0
- package/dist/server.js.map +1 -0
- package/dist/test.d.ts +7 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +77 -0
- package/dist/test.js.map +1 -0
- package/dist/tools/agentic/index.d.ts +7 -0
- package/dist/tools/agentic/index.d.ts.map +1 -0
- package/dist/tools/agentic/index.js +180 -0
- package/dist/tools/agentic/index.js.map +1 -0
- package/dist/tools/architect/index.d.ts +9 -0
- package/dist/tools/architect/index.d.ts.map +1 -0
- package/dist/tools/architect/index.js +383 -0
- package/dist/tools/architect/index.js.map +1 -0
- package/dist/tools/architect.d.ts +19 -0
- package/dist/tools/architect.d.ts.map +1 -0
- package/dist/tools/architect.js +88 -0
- package/dist/tools/architect.js.map +1 -0
- package/dist/tools/bench.d.ts +89 -0
- package/dist/tools/bench.d.ts.map +1 -0
- package/dist/tools/bench.js +826 -0
- package/dist/tools/bench.js.map +1 -0
- package/dist/tools/context/index.d.ts +11 -0
- package/dist/tools/context/index.d.ts.map +1 -0
- package/dist/tools/context/index.js +439 -0
- package/dist/tools/context/index.js.map +1 -0
- package/dist/tools/context.d.ts +146 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +481 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/crit.d.ts +63 -0
- package/dist/tools/crit.d.ts.map +1 -0
- package/dist/tools/crit.js +159 -0
- package/dist/tools/crit.js.map +1 -0
- package/dist/tools/data-inspector.d.ts +188 -0
- package/dist/tools/data-inspector.d.ts.map +1 -0
- package/dist/tools/data-inspector.js +650 -0
- package/dist/tools/data-inspector.js.map +1 -0
- package/dist/tools/deck.d.ts +11 -0
- package/dist/tools/deck.d.ts.map +1 -0
- package/dist/tools/deck.js +170 -0
- package/dist/tools/deck.js.map +1 -0
- package/dist/tools/designer/index.d.ts +6 -0
- package/dist/tools/designer/index.d.ts.map +1 -0
- package/dist/tools/designer/index.js +177 -0
- package/dist/tools/designer/index.js.map +1 -0
- package/dist/tools/designer.d.ts +20 -0
- package/dist/tools/designer.d.ts.map +1 -0
- package/dist/tools/designer.js +75 -0
- package/dist/tools/designer.js.map +1 -0
- package/dist/tools/dojo/index.d.ts +13 -0
- package/dist/tools/dojo/index.d.ts.map +1 -0
- package/dist/tools/dojo/index.js +547 -0
- package/dist/tools/dojo/index.js.map +1 -0
- package/dist/tools/dojo.d.ts +254 -0
- package/dist/tools/dojo.d.ts.map +1 -0
- package/dist/tools/dojo.js +933 -0
- package/dist/tools/dojo.js.map +1 -0
- package/dist/tools/dojoBench.d.ts +49 -0
- package/dist/tools/dojoBench.d.ts.map +1 -0
- package/dist/tools/dojoBench.js +205 -0
- package/dist/tools/dojoBench.js.map +1 -0
- package/dist/tools/dojoGraduated.d.ts +50 -0
- package/dist/tools/dojoGraduated.d.ts.map +1 -0
- package/dist/tools/dojoGraduated.js +174 -0
- package/dist/tools/dojoGraduated.js.map +1 -0
- package/dist/tools/dojoPolicy.d.ts +65 -0
- package/dist/tools/dojoPolicy.d.ts.map +1 -0
- package/dist/tools/dojoPolicy.js +263 -0
- package/dist/tools/dojoPolicy.js.map +1 -0
- package/dist/tools/friction/index.d.ts +7 -0
- package/dist/tools/friction/index.d.ts.map +1 -0
- package/dist/tools/friction/index.js +239 -0
- package/dist/tools/friction/index.js.map +1 -0
- package/dist/tools/friction.d.ts +82 -0
- package/dist/tools/friction.d.ts.map +1 -0
- package/dist/tools/friction.js +331 -0
- package/dist/tools/friction.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/learnings/index.d.ts +5 -0
- package/dist/tools/learnings/index.d.ts.map +1 -0
- package/dist/tools/learnings/index.js +123 -0
- package/dist/tools/learnings/index.js.map +1 -0
- package/dist/tools/learnings.d.ts +41 -0
- package/dist/tools/learnings.d.ts.map +1 -0
- package/dist/tools/learnings.js +149 -0
- package/dist/tools/learnings.js.map +1 -0
- package/dist/tools/oracle/index.d.ts +5 -0
- package/dist/tools/oracle/index.d.ts.map +1 -0
- package/dist/tools/oracle/index.js +97 -0
- package/dist/tools/oracle/index.js.map +1 -0
- package/dist/tools/oracle.d.ts +90 -0
- package/dist/tools/oracle.d.ts.map +1 -0
- package/dist/tools/oracle.js +529 -0
- package/dist/tools/oracle.js.map +1 -0
- package/dist/tools/policy.d.ts +119 -0
- package/dist/tools/policy.d.ts.map +1 -0
- package/dist/tools/policy.js +406 -0
- package/dist/tools/policy.js.map +1 -0
- package/dist/tools/provenance/index.d.ts +4 -0
- package/dist/tools/provenance/index.d.ts.map +1 -0
- package/dist/tools/provenance/index.js +58 -0
- package/dist/tools/provenance/index.js.map +1 -0
- package/dist/tools/provenance.d.ts +75 -0
- package/dist/tools/provenance.d.ts.map +1 -0
- package/dist/tools/provenance.js +197 -0
- package/dist/tools/provenance.js.map +1 -0
- package/dist/tools/rateLimiter.d.ts +45 -0
- package/dist/tools/rateLimiter.d.ts.map +1 -0
- package/dist/tools/rateLimiter.js +91 -0
- package/dist/tools/rateLimiter.js.map +1 -0
- package/dist/tools/registry/index.d.ts +10 -0
- package/dist/tools/registry/index.d.ts.map +1 -0
- package/dist/tools/registry/index.js +467 -0
- package/dist/tools/registry/index.js.map +1 -0
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +189 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/roadmap/index.d.ts +9 -0
- package/dist/tools/roadmap/index.d.ts.map +1 -0
- package/dist/tools/roadmap/index.js +250 -0
- package/dist/tools/roadmap/index.js.map +1 -0
- package/dist/tools/roadmap.d.ts +88 -0
- package/dist/tools/roadmap.d.ts.map +1 -0
- package/dist/tools/roadmap.js +365 -0
- package/dist/tools/roadmap.js.map +1 -0
- package/dist/tools/sentinel/index.d.ts +19 -0
- package/dist/tools/sentinel/index.d.ts.map +1 -0
- package/dist/tools/sentinel/index.js +832 -0
- package/dist/tools/sentinel/index.js.map +1 -0
- package/dist/tools/sentinel-scan-data.d.ts +90 -0
- package/dist/tools/sentinel-scan-data.d.ts.map +1 -0
- package/dist/tools/sentinel-scan-data.js +122 -0
- package/dist/tools/sentinel-scan-data.js.map +1 -0
- package/dist/tools/sentinel.d.ts +156 -0
- package/dist/tools/sentinel.d.ts.map +1 -0
- package/dist/tools/sentinel.js +603 -0
- package/dist/tools/sentinel.js.map +1 -0
- package/dist/tools/shared/index.d.ts +4 -0
- package/dist/tools/shared/index.d.ts.map +1 -0
- package/dist/tools/shared/index.js +7 -0
- package/dist/tools/shared/index.js.map +1 -0
- package/dist/tools/shared/project.d.ts +17 -0
- package/dist/tools/shared/project.d.ts.map +1 -0
- package/dist/tools/shared/project.js +36 -0
- package/dist/tools/shared/project.js.map +1 -0
- package/dist/tools/shared/response.d.ts +10 -0
- package/dist/tools/shared/response.d.ts.map +1 -0
- package/dist/tools/shared/response.js +36 -0
- package/dist/tools/shared/response.js.map +1 -0
- package/dist/tools/shared/validation.d.ts +10 -0
- package/dist/tools/shared/validation.d.ts.map +1 -0
- package/dist/tools/shared/validation.js +26 -0
- package/dist/tools/shared/validation.js.map +1 -0
- package/dist/tools/studio/cloud-spine.d.ts +27 -0
- package/dist/tools/studio/cloud-spine.d.ts.map +1 -0
- package/dist/tools/studio/cloud-spine.js +773 -0
- package/dist/tools/studio/cloud-spine.js.map +1 -0
- package/dist/tools/studio/index.d.ts +154 -0
- package/dist/tools/studio/index.d.ts.map +1 -0
- package/dist/tools/studio/index.js +525 -0
- package/dist/tools/studio/index.js.map +1 -0
- package/dist/tools/testSpec.d.ts +122 -0
- package/dist/tools/testSpec.d.ts.map +1 -0
- package/dist/tools/testSpec.js +525 -0
- package/dist/tools/testSpec.js.map +1 -0
- package/dist/tools/toolsIndex.d.ts +5 -0
- package/dist/tools/toolsIndex.d.ts.map +1 -0
- package/dist/tools/toolsIndex.js +37 -0
- package/dist/tools/toolsIndex.js.map +1 -0
- package/dist/tools/types.d.ts +30 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +7 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/voice/index.d.ts +8 -0
- package/dist/tools/voice/index.d.ts.map +1 -0
- package/dist/tools/voice/index.js +176 -0
- package/dist/tools/voice/index.js.map +1 -0
- package/dist/tools/voice.d.ts +291 -0
- package/dist/tools/voice.d.ts.map +1 -0
- package/dist/tools/voice.js +734 -0
- package/dist/tools/voice.js.map +1 -0
- 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
|