dlab-cli 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/PKG-INFO +1 -1
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/__init__.py +1 -1
- dlab_cli-0.1.1/dlab/js/__init__.py +0 -0
- dlab_cli-0.1.1/dlab/js/parallel-agents.ts +417 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/PKG-INFO +1 -1
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/SOURCES.txt +2 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/pyproject.toml +2 -1
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/LICENSE +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/README.md +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/cli.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/config.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/create_dpack.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/create_dpack_wizard.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/create_parallel_agent_wizard.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/data/__init__.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/data/models.json +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/docker.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/local.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/model_fallback.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/parallel_tool.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/session.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/timeline.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/__init__.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/app.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/log_watcher.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/models.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/__init__.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/agent_list.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/artifacts_pane.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/log_view.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/search_popup.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/status_bar.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/dependency_links.txt +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/entry_points.txt +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/requires.txt +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/top_level.txt +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/setup.cfg +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_cli.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_config.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_create_dpack.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_create_dpack_wizard.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_create_parallel_agent_wizard.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_docker.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_generate_dpack_integration.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_integration.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_model_fallback.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_parallel_tool.py +0 -0
- {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_session.py +0 -0
|
File without changes
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync, appendFileSync, readdirSync, copyFileSync, cpSync, statSync } from "fs"
|
|
3
|
+
import { parse as parseYaml } from "yaml"
|
|
4
|
+
import { join, basename } from "path"
|
|
5
|
+
|
|
6
|
+
// Helper: Copy directory contents excluding certain paths
|
|
7
|
+
function copyWorkDir(src: string, dest: string, exclude: string[]) {
|
|
8
|
+
mkdirSync(dest, { recursive: true })
|
|
9
|
+
for (const item of readdirSync(src)) {
|
|
10
|
+
if (exclude.includes(item)) continue
|
|
11
|
+
const srcPath = join(src, item)
|
|
12
|
+
const destPath = join(dest, item)
|
|
13
|
+
const stat = statSync(srcPath)
|
|
14
|
+
if (stat.isDirectory()) {
|
|
15
|
+
cpSync(srcPath, destPath, { recursive: true })
|
|
16
|
+
} else {
|
|
17
|
+
copyFileSync(srcPath, destPath)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Helper: Parse YAML frontmatter from agent markdown
|
|
23
|
+
function parseAgentFrontmatter(agentPath: string): Record<string, any> {
|
|
24
|
+
const content = readFileSync(agentPath, "utf-8")
|
|
25
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/)
|
|
26
|
+
if (!match) return {}
|
|
27
|
+
return parseYaml(match[1])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Helper: Build permission config from agent frontmatter tools
|
|
31
|
+
// Some permissions support nested objects (read, edit, glob, grep, list, bash, task, external_directory, lsp)
|
|
32
|
+
// Others only accept simple strings (todoread, todowrite, question, webfetch, websearch, codesearch, doom_loop)
|
|
33
|
+
function buildPermissionsFromFrontmatter(agentPath: string): Record<string, any> {
|
|
34
|
+
const frontmatter = parseAgentFrontmatter(agentPath)
|
|
35
|
+
const tools = frontmatter.tools || {}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
// Permissions that support nested objects - use { "*": "action" } format
|
|
39
|
+
"read": { "*": "allow" },
|
|
40
|
+
"glob": { "*": "allow" },
|
|
41
|
+
"grep": { "*": "allow" },
|
|
42
|
+
"list": { "*": "allow" },
|
|
43
|
+
"lsp": { "*": "allow" },
|
|
44
|
+
"external_directory": { "*": "allow" },
|
|
45
|
+
|
|
46
|
+
// Write/execute tools - based on frontmatter
|
|
47
|
+
"edit": { "*": tools["edit"] === true ? "allow" : "deny" },
|
|
48
|
+
"bash": { "*": tools["bash"] === true ? "allow" : "deny" },
|
|
49
|
+
|
|
50
|
+
// Subagent spawning - deny
|
|
51
|
+
"task": { "*": "deny" },
|
|
52
|
+
|
|
53
|
+
// Simple string permissions
|
|
54
|
+
"todoread": "allow",
|
|
55
|
+
"todowrite": tools["edit"] === true ? "allow" : "deny",
|
|
56
|
+
"question": "deny",
|
|
57
|
+
"webfetch": "allow",
|
|
58
|
+
"websearch": "allow",
|
|
59
|
+
"codesearch": "allow",
|
|
60
|
+
"doom_loop": "allow",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Helper: Setup minimal consolidator environment (read-only, no custom tools)
|
|
65
|
+
function setupConsolidator(runDir: string, srcOpencode: string, summarizerPrompt: string) {
|
|
66
|
+
const destOpencode = join(runDir, ".opencode")
|
|
67
|
+
|
|
68
|
+
// Create directories
|
|
69
|
+
mkdirSync(join(destOpencode, "agents"), { recursive: true })
|
|
70
|
+
|
|
71
|
+
// Create minimal consolidator agent from summarizer_prompt
|
|
72
|
+
const consolidatorAgent = `---
|
|
73
|
+
description: Consolidator agent for comparing parallel agent results
|
|
74
|
+
mode: primary
|
|
75
|
+
tools:
|
|
76
|
+
read: true
|
|
77
|
+
edit: false
|
|
78
|
+
bash: false
|
|
79
|
+
parallel-agents: false
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
${summarizerPrompt}
|
|
83
|
+
`
|
|
84
|
+
writeFileSync(join(destOpencode, "agents", "consolidator.md"), consolidatorAgent)
|
|
85
|
+
|
|
86
|
+
// Copy and modify opencode.json
|
|
87
|
+
const opconfig = JSON.parse(readFileSync(join(srcOpencode, "opencode.json"), "utf-8"))
|
|
88
|
+
opconfig.default_agent = "consolidator"
|
|
89
|
+
|
|
90
|
+
// Hardcoded read-only permissions for consolidator
|
|
91
|
+
opconfig.permission = {
|
|
92
|
+
// Read/discovery tools - always allowed
|
|
93
|
+
"read": { "*": "allow" },
|
|
94
|
+
"glob": { "*": "allow" },
|
|
95
|
+
"grep": { "*": "allow" },
|
|
96
|
+
"list": { "*": "allow" },
|
|
97
|
+
"lsp": { "*": "allow" },
|
|
98
|
+
"external_directory": { "*": "allow" },
|
|
99
|
+
|
|
100
|
+
// Write/execute tools - always denied
|
|
101
|
+
"edit": { "*": "deny" },
|
|
102
|
+
"bash": { "*": "deny" },
|
|
103
|
+
"task": { "*": "deny" },
|
|
104
|
+
|
|
105
|
+
// Simple permissions
|
|
106
|
+
"todoread": "allow",
|
|
107
|
+
"todowrite": "deny",
|
|
108
|
+
"question": "deny",
|
|
109
|
+
"webfetch": "allow",
|
|
110
|
+
"websearch": "allow",
|
|
111
|
+
"codesearch": "allow",
|
|
112
|
+
"doom_loop": "allow",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
writeFileSync(join(destOpencode, "opencode.json"), JSON.stringify(opconfig, null, 2))
|
|
116
|
+
|
|
117
|
+
// NOTE: Do NOT copy tools/ directory - consolidator has no custom tools
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Helper: Copy only allowed tools based on agent frontmatter
|
|
121
|
+
function copyAllowedTools(srcOpencode: string, destOpencode: string, agentName: string) {
|
|
122
|
+
const agentPath = join(srcOpencode, "agents", `${agentName}.md`)
|
|
123
|
+
const frontmatter = parseAgentFrontmatter(agentPath)
|
|
124
|
+
const tools = frontmatter.tools || {}
|
|
125
|
+
|
|
126
|
+
// Create dest directories
|
|
127
|
+
mkdirSync(join(destOpencode, "agents"), { recursive: true })
|
|
128
|
+
mkdirSync(join(destOpencode, "tools"), { recursive: true })
|
|
129
|
+
|
|
130
|
+
// Copy opencode.json and package.json
|
|
131
|
+
copyFileSync(join(srcOpencode, "opencode.json"), join(destOpencode, "opencode.json"))
|
|
132
|
+
if (existsSync(join(srcOpencode, "package.json"))) {
|
|
133
|
+
copyFileSync(join(srcOpencode, "package.json"), join(destOpencode, "package.json"))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// NOTE: Do NOT copy parallel_agents/ to subagents - only orchestrator needs it
|
|
137
|
+
|
|
138
|
+
// Copy the target agent definition, converting subagent to primary mode
|
|
139
|
+
// (OpenCode doesn't allow subagents to be the default_agent)
|
|
140
|
+
let agentContent = readFileSync(agentPath, "utf-8")
|
|
141
|
+
agentContent = agentContent.replace(/^(---.*)mode:\s*subagent(.*---)$/ms, "$1mode: primary$2")
|
|
142
|
+
writeFileSync(join(destOpencode, "agents", `${agentName}.md`), agentContent)
|
|
143
|
+
|
|
144
|
+
// Copy only tools that are explicitly allowed (true in frontmatter)
|
|
145
|
+
const srcTools = join(srcOpencode, "tools")
|
|
146
|
+
if (existsSync(srcTools)) {
|
|
147
|
+
for (const file of readdirSync(srcTools)) {
|
|
148
|
+
const toolName = file.replace(/\.(ts|js)$/, "")
|
|
149
|
+
// NEVER copy parallel-agents to subagents
|
|
150
|
+
if (toolName === "parallel-agents") continue
|
|
151
|
+
// Only copy if explicitly allowed
|
|
152
|
+
if (tools[toolName] === true) {
|
|
153
|
+
copyFileSync(join(srcTools, file), join(destOpencode, "tools", file))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Copy skills declared in frontmatter
|
|
159
|
+
const skills = frontmatter.skills || []
|
|
160
|
+
if (skills.length > 0) {
|
|
161
|
+
const srcSkills = join(srcOpencode, "skills")
|
|
162
|
+
if (existsSync(srcSkills)) {
|
|
163
|
+
for (const skillName of skills) {
|
|
164
|
+
const srcSkill = join(srcSkills, skillName)
|
|
165
|
+
if (existsSync(srcSkill)) {
|
|
166
|
+
const destSkill = join(destOpencode, "skills", skillName)
|
|
167
|
+
cpSync(srcSkill, destSkill, { recursive: true })
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default tool({
|
|
175
|
+
description: "Spawn parallel subagents for a configured agent type",
|
|
176
|
+
|
|
177
|
+
args: {
|
|
178
|
+
agent: tool.schema.string().describe("Agent name (must have config in parallel_agents/)"),
|
|
179
|
+
prompts: tool.schema.array(tool.schema.string()).describe("Array of prompts, one per instance"),
|
|
180
|
+
models: tool.schema.array(tool.schema.string()).optional()
|
|
181
|
+
.describe("Optional: model per instance (defaults to config default_model)"),
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async execute(args, ctx) {
|
|
185
|
+
const cwd = process.cwd()
|
|
186
|
+
const timestamp = Date.now()
|
|
187
|
+
const runDir = join(cwd, "parallel", `run-${timestamp}`)
|
|
188
|
+
const logsDir = join(cwd, "_opencode_logs", `${args.agent}-parallel-run-${timestamp}`)
|
|
189
|
+
|
|
190
|
+
// Load parallel agent config
|
|
191
|
+
const configPath = join(cwd, ".opencode", "parallel_agents", `${args.agent}.yaml`)
|
|
192
|
+
if (!existsSync(configPath)) {
|
|
193
|
+
throw new Error(`No parallel config found: ${configPath}`)
|
|
194
|
+
}
|
|
195
|
+
const config = parseYaml(readFileSync(configPath, "utf-8"))
|
|
196
|
+
|
|
197
|
+
// Validate instance count
|
|
198
|
+
const numInstances = args.prompts.length
|
|
199
|
+
if (numInstances < 1) {
|
|
200
|
+
throw new Error("At least 1 prompt required")
|
|
201
|
+
}
|
|
202
|
+
if (config.max_instances && numInstances > config.max_instances) {
|
|
203
|
+
throw new Error(`Max ${config.max_instances} instances allowed, got ${numInstances}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
mkdirSync(runDir, { recursive: true })
|
|
207
|
+
mkdirSync(logsDir, { recursive: true })
|
|
208
|
+
|
|
209
|
+
// Spawn instances
|
|
210
|
+
const processes: Array<{
|
|
211
|
+
proc: ReturnType<typeof Bun.spawn>
|
|
212
|
+
dir: string
|
|
213
|
+
logFile: string
|
|
214
|
+
model: string
|
|
215
|
+
prompt: string
|
|
216
|
+
}> = []
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < numInstances; i++) {
|
|
219
|
+
const instanceDir = join(runDir, `instance-${i + 1}`)
|
|
220
|
+
mkdirSync(instanceDir, { recursive: true })
|
|
221
|
+
|
|
222
|
+
// HACK: Initialize git repo to stop OpenCode config traversal to parent directories.
|
|
223
|
+
// This is an EVIL HACK. OpenCode traverses up looking for .opencode/ configs and merges
|
|
224
|
+
// them, causing subagents to see parent agents and get confused about their role.
|
|
225
|
+
// Creating a .git dir makes OpenCode think this is a project root, stopping traversal.
|
|
226
|
+
// TODO: Replace with proper solution when OpenCode supports disabling parent traversal.
|
|
227
|
+
Bun.spawnSync(["git", "init"], { cwd: instanceDir, stdout: "ignore", stderr: "ignore" })
|
|
228
|
+
|
|
229
|
+
// Copy entire work directory to instance (excluding special dirs)
|
|
230
|
+
// This gives subagents access to all work done by the orchestrator so far
|
|
231
|
+
copyWorkDir(cwd, instanceDir, [".opencode", "_opencode_logs", "parallel", ".state.json", ".git"])
|
|
232
|
+
|
|
233
|
+
// Copy .opencode with ONLY allowed tools
|
|
234
|
+
copyAllowedTools(join(cwd, ".opencode"), join(instanceDir, ".opencode"), args.agent)
|
|
235
|
+
|
|
236
|
+
// Set default agent AND build granular permissions from frontmatter
|
|
237
|
+
const opconfigPath = join(instanceDir, ".opencode", "opencode.json")
|
|
238
|
+
const opconfig = JSON.parse(readFileSync(opconfigPath, "utf-8"))
|
|
239
|
+
opconfig.default_agent = args.agent
|
|
240
|
+
|
|
241
|
+
// Build permissions based on agent's tools frontmatter
|
|
242
|
+
const agentPath = join(cwd, ".opencode", "agents", `${args.agent}.md`)
|
|
243
|
+
opconfig.permission = buildPermissionsFromFrontmatter(agentPath)
|
|
244
|
+
|
|
245
|
+
writeFileSync(opconfigPath, JSON.stringify(opconfig, null, 2))
|
|
246
|
+
|
|
247
|
+
// Build prompt with subagent context and suffix
|
|
248
|
+
// Model priority: 1) explicit models array, 2) instance_models from config, 3) default_model
|
|
249
|
+
const instanceModels = config.instance_models || []
|
|
250
|
+
const model = args.models?.[i] || instanceModels[i] || config.default_model
|
|
251
|
+
|
|
252
|
+
// Automatic subagent context (always added to prevent hangs and enforce workspace boundaries)
|
|
253
|
+
const subagentContext = `IMPORTANT CONTEXT: You are running as a parallel subagent in non-interactive mode.
|
|
254
|
+
|
|
255
|
+
YOUR WORKING DIRECTORY: ${instanceDir}
|
|
256
|
+
|
|
257
|
+
CRITICAL OUTPUT RULES:
|
|
258
|
+
- Write ALL output files to the ROOT of your working directory
|
|
259
|
+
- Do NOT create subdirectories - write directly to: ${instanceDir}/
|
|
260
|
+
- Required outputs:
|
|
261
|
+
- summary.md -> ${instanceDir}/summary.md
|
|
262
|
+
- cleaned_data.parquet -> ${instanceDir}/cleaned_data.parquet (if applicable)
|
|
263
|
+
- You may READ files from absolute paths if mentioned in your prompt
|
|
264
|
+
- You must NEVER WRITE or EDIT files outside your working directory
|
|
265
|
+
- NEVER ask for permission or confirmation - all operations are pre-approved or pre-denied
|
|
266
|
+
- If an operation fails, report the error and continue with alternatives
|
|
267
|
+
|
|
268
|
+
`
|
|
269
|
+
const fullPrompt = subagentContext + args.prompts[i] + "\n\n" + (config.subagent_suffix_prompt || "")
|
|
270
|
+
|
|
271
|
+
// Spawn - log file starts with JSON directly (no header)
|
|
272
|
+
const logFile = join(logsDir, `instance-${i + 1}.log`)
|
|
273
|
+
|
|
274
|
+
const proc = Bun.spawn(["opencode", "run", "--format", "json", "--log-level", "DEBUG", "--model", model, fullPrompt], {
|
|
275
|
+
cwd: instanceDir,
|
|
276
|
+
stdout: "pipe",
|
|
277
|
+
stderr: "pipe",
|
|
278
|
+
env: process.env, // Inherit PYTHONPATH and other env vars
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Stream to logs (async)
|
|
282
|
+
;(async () => {
|
|
283
|
+
const reader = proc.stdout.getReader()
|
|
284
|
+
while (true) {
|
|
285
|
+
const { done, value } = await reader.read()
|
|
286
|
+
if (done) break
|
|
287
|
+
appendFileSync(logFile, new TextDecoder().decode(value))
|
|
288
|
+
}
|
|
289
|
+
})()
|
|
290
|
+
;(async () => {
|
|
291
|
+
const reader = proc.stderr.getReader()
|
|
292
|
+
while (true) {
|
|
293
|
+
const { done, value } = await reader.read()
|
|
294
|
+
if (done) break
|
|
295
|
+
appendFileSync(logFile, "[STDERR] " + new TextDecoder().decode(value))
|
|
296
|
+
}
|
|
297
|
+
})()
|
|
298
|
+
|
|
299
|
+
processes.push({ proc, dir: instanceDir, logFile, model, prompt: args.prompts[i] })
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Wait for all
|
|
303
|
+
const results: Array<{
|
|
304
|
+
dir: string
|
|
305
|
+
summaryPath: string
|
|
306
|
+
exitCode: number | null
|
|
307
|
+
}> = []
|
|
308
|
+
|
|
309
|
+
for (const p of processes) {
|
|
310
|
+
await p.proc.exited
|
|
311
|
+
results.push({
|
|
312
|
+
dir: p.dir,
|
|
313
|
+
summaryPath: join(p.dir, "summary.md"),
|
|
314
|
+
exitCode: p.proc.exitCode,
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Run consolidator if >= 3 instances completed
|
|
319
|
+
// For n=2, the orchestrator can read both summaries directly
|
|
320
|
+
let consolidatedSummary = ""
|
|
321
|
+
if (numInstances >= 3 && config.summarizer_prompt) {
|
|
322
|
+
// Use ABSOLUTE paths so consolidator doesn't need to search
|
|
323
|
+
const summaryPaths = results
|
|
324
|
+
.filter(r => existsSync(r.summaryPath))
|
|
325
|
+
.map(r => r.summaryPath) // Already absolute paths
|
|
326
|
+
|
|
327
|
+
const consolidatedSummaryPath = join(runDir, "consolidated_summary.md")
|
|
328
|
+
|
|
329
|
+
// Build consolidator prompt with absolute paths and explicit output location
|
|
330
|
+
const consolidatorContext = `IMPORTANT CONTEXT: You are running as a consolidator subagent in non-interactive mode.
|
|
331
|
+
|
|
332
|
+
READ ONLY THESE EXACT FILES (do NOT search or glob):
|
|
333
|
+
${summaryPaths.map((p: string) => `- ${p}`).join("\n")}
|
|
334
|
+
|
|
335
|
+
WRITE YOUR OUTPUT TO: ${consolidatedSummaryPath}
|
|
336
|
+
|
|
337
|
+
RULES:
|
|
338
|
+
- NEVER ask for permission or confirmation - all operations are pre-approved or pre-denied
|
|
339
|
+
- NEVER use glob or search for files - read the exact paths listed above
|
|
340
|
+
- If an operation fails, report the error and continue with alternatives
|
|
341
|
+
|
|
342
|
+
`
|
|
343
|
+
const consolidatorPrompt = consolidatorContext + config.summarizer_prompt
|
|
344
|
+
.replace("{summary_paths}", summaryPaths.map((p: string) => `- ${p}`).join("\n"))
|
|
345
|
+
|
|
346
|
+
const consolidatorModel = config.summarizer_model || config.default_model
|
|
347
|
+
|
|
348
|
+
// HACK: Initialize git repo to stop OpenCode config traversal (same as instances)
|
|
349
|
+
// Without this, OpenCode traverses up and merges parent's .opencode/, running as orchestrator
|
|
350
|
+
Bun.spawnSync(["git", "init"], { cwd: runDir, stdout: "ignore", stderr: "ignore" })
|
|
351
|
+
|
|
352
|
+
// Setup minimal consolidator environment (read-only, no custom tools)
|
|
353
|
+
setupConsolidator(runDir, join(cwd, ".opencode"), config.summarizer_prompt)
|
|
354
|
+
|
|
355
|
+
const consLogFile = join(logsDir, "consolidator.log")
|
|
356
|
+
|
|
357
|
+
const consProc = Bun.spawn(["opencode", "run", "--format", "json", "--log-level", "DEBUG", "--model", consolidatorModel, consolidatorPrompt], {
|
|
358
|
+
cwd: runDir,
|
|
359
|
+
stdout: "pipe",
|
|
360
|
+
stderr: "pipe",
|
|
361
|
+
env: process.env, // Inherit PYTHONPATH and other env vars
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// Stream stdout to log file (don't use as summary - it's JSON logs)
|
|
365
|
+
const reader = consProc.stdout.getReader()
|
|
366
|
+
while (true) {
|
|
367
|
+
const { done, value } = await reader.read()
|
|
368
|
+
if (done) break
|
|
369
|
+
appendFileSync(consLogFile, new TextDecoder().decode(value))
|
|
370
|
+
}
|
|
371
|
+
await consProc.exited
|
|
372
|
+
|
|
373
|
+
// Read the consolidated summary from file (not stdout)
|
|
374
|
+
if (existsSync(consolidatedSummaryPath)) {
|
|
375
|
+
consolidatedSummary = readFileSync(consolidatedSummaryPath, "utf-8")
|
|
376
|
+
} else {
|
|
377
|
+
consolidatedSummary = "[Consolidator did not produce a summary file]"
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Build final output
|
|
382
|
+
let finalOutput = "## Parallel Agent Results\n\n"
|
|
383
|
+
finalOutput += `Ran ${numInstances} instances of agent: ${args.agent}\n\n`
|
|
384
|
+
|
|
385
|
+
for (const r of results) {
|
|
386
|
+
finalOutput += `### ${basename(r.dir)}\n`
|
|
387
|
+
finalOutput += `- Exit code: ${r.exitCode}\n`
|
|
388
|
+
if (existsSync(r.summaryPath)) {
|
|
389
|
+
finalOutput += `- Summary: ${r.summaryPath}\n`
|
|
390
|
+
}
|
|
391
|
+
finalOutput += "\n"
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (consolidatedSummary) {
|
|
395
|
+
finalOutput += "## Consolidated Summary\n\n"
|
|
396
|
+
finalOutput += consolidatedSummary + "\n\n"
|
|
397
|
+
finalOutput += "---\n\n"
|
|
398
|
+
finalOutput += "If you need more details, individual summaries are at:\n"
|
|
399
|
+
for (const r of results) {
|
|
400
|
+
if (existsSync(r.summaryPath)) {
|
|
401
|
+
finalOutput += `- ${r.summaryPath}\n`
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
// No consolidator (n <= 2) - tell orchestrator where to find summaries
|
|
406
|
+
finalOutput += "## Summary Locations\n\n"
|
|
407
|
+
finalOutput += "Read these summary files to compare results:\n"
|
|
408
|
+
for (const r of results) {
|
|
409
|
+
if (existsSync(r.summaryPath)) {
|
|
410
|
+
finalOutput += `- ${r.summaryPath}\n`
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return finalOutput
|
|
416
|
+
},
|
|
417
|
+
})
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dlab-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "A harness for agentic data science — run coding agents with domain skills, parallel subagents, and frozen Docker environments"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -43,3 +43,4 @@ include = ["dlab*"]
|
|
|
43
43
|
|
|
44
44
|
[tool.setuptools.package-data]
|
|
45
45
|
"dlab.data" = ["*.json"]
|
|
46
|
+
"dlab.js" = ["*.ts"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|