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.
Files changed (48) hide show
  1. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/PKG-INFO +1 -1
  2. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/__init__.py +1 -1
  3. dlab_cli-0.1.1/dlab/js/__init__.py +0 -0
  4. dlab_cli-0.1.1/dlab/js/parallel-agents.ts +417 -0
  5. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/PKG-INFO +1 -1
  6. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/SOURCES.txt +2 -0
  7. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/pyproject.toml +2 -1
  8. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/LICENSE +0 -0
  9. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/README.md +0 -0
  10. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/cli.py +0 -0
  11. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/config.py +0 -0
  12. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/create_dpack.py +0 -0
  13. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/create_dpack_wizard.py +0 -0
  14. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/create_parallel_agent_wizard.py +0 -0
  15. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/data/__init__.py +0 -0
  16. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/data/models.json +0 -0
  17. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/docker.py +0 -0
  18. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/local.py +0 -0
  19. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/model_fallback.py +0 -0
  20. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/parallel_tool.py +0 -0
  21. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/session.py +0 -0
  22. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/timeline.py +0 -0
  23. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/__init__.py +0 -0
  24. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/app.py +0 -0
  25. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/log_watcher.py +0 -0
  26. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/models.py +0 -0
  27. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/__init__.py +0 -0
  28. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/agent_list.py +0 -0
  29. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/artifacts_pane.py +0 -0
  30. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/log_view.py +0 -0
  31. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/search_popup.py +0 -0
  32. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab/tui/widgets/status_bar.py +0 -0
  33. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/dependency_links.txt +0 -0
  34. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/entry_points.txt +0 -0
  35. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/requires.txt +0 -0
  36. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/dlab_cli.egg-info/top_level.txt +0 -0
  37. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/setup.cfg +0 -0
  38. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_cli.py +0 -0
  39. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_config.py +0 -0
  40. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_create_dpack.py +0 -0
  41. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_create_dpack_wizard.py +0 -0
  42. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_create_parallel_agent_wizard.py +0 -0
  43. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_docker.py +0 -0
  44. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_generate_dpack_integration.py +0 -0
  45. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_integration.py +0 -0
  46. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_model_fallback.py +0 -0
  47. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_parallel_tool.py +0 -0
  48. {dlab_cli-0.1.0 → dlab_cli-0.1.1}/tests/test_session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dlab-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A harness for agentic data science — run coding agents with domain skills, parallel subagents, and frozen Docker environments
5
5
  Author: DecisionAI
6
6
  License: Apache-2.0
@@ -3,4 +3,4 @@ dlab: CLI wrapper that runs opencode in automated mode, sandboxed with Docker,
3
3
  and with parallel-agent capability.
4
4
  """
5
5
 
6
- __version__ = "0.1.0"
6
+ __version__ = "0.1.1"
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
+ })
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dlab-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A harness for agentic data science — run coding agents with domain skills, parallel subagents, and frozen Docker environments
5
5
  Author: DecisionAI
6
6
  License: Apache-2.0
@@ -15,6 +15,8 @@ dlab/session.py
15
15
  dlab/timeline.py
16
16
  dlab/data/__init__.py
17
17
  dlab/data/models.json
18
+ dlab/js/__init__.py
19
+ dlab/js/parallel-agents.ts
18
20
  dlab/tui/__init__.py
19
21
  dlab/tui/app.py
20
22
  dlab/tui/log_watcher.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dlab-cli"
7
- version = "0.1.0"
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