pi-crew 0.1.20 → 0.1.22
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/docs/source-runtime-refactor-map.md +83 -0
- package/package.json +1 -1
- package/src/extension/register.ts +44 -10
- package/src/extension/team-tool.ts +8 -46
- package/src/runtime/async-runner.ts +12 -7
- package/src/runtime/background-runner.ts +9 -5
- package/src/runtime/child-pi.ts +12 -7
- package/src/runtime/concurrency.ts +1 -1
- package/src/runtime/direct-run.ts +35 -0
- package/src/runtime/parallel-research.ts +44 -0
- package/src/runtime/subagent-manager.ts +34 -0
- package/src/runtime/team-runner.ts +16 -6
- package/src/state/state-store.ts +1 -0
- package/src/state/types.ts +1 -0
- package/teams/parallel-research.team.md +1 -1
- package/workflows/implementation.workflow.md +1 -1
- package/workflows/parallel-research.workflow.md +1 -5
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# pi-crew runtime refactor source map
|
|
2
|
+
|
|
3
|
+
This document records the source projects used as the baseline for the pi-crew subagent/runtime refactor. The goal is to avoid ad-hoc fixes in critical process orchestration paths and instead align pi-crew with proven Pi extension patterns.
|
|
4
|
+
|
|
5
|
+
## Source/pi-subagents
|
|
6
|
+
|
|
7
|
+
Primary source for child-process worker execution.
|
|
8
|
+
|
|
9
|
+
- `pi-spawn.ts`: robust Pi CLI resolution on Windows and package installs.
|
|
10
|
+
- `async-execution.ts`: detached async runner with `windowsHide: true` to avoid blank console windows.
|
|
11
|
+
- `subagent-runner.ts`: streaming child Pi process runner, output capture, result extraction.
|
|
12
|
+
- `post-exit-stdio-guard.ts`: guards for child processes that exit before stdio fully closes.
|
|
13
|
+
- `result-watcher.ts` and `async-job-tracker.ts`: durable async job/result observation patterns.
|
|
14
|
+
- `model-fallback.ts`: model fallback policy independent of hardcoded provider assumptions.
|
|
15
|
+
- `subagent-control.ts`, `run-status.ts`: status and control semantics.
|
|
16
|
+
|
|
17
|
+
pi-crew alignment:
|
|
18
|
+
|
|
19
|
+
- Background runner and child worker spawn options now explicitly set `windowsHide: true`.
|
|
20
|
+
- Parallel research no longer gates all shard workers behind a single discover worker.
|
|
21
|
+
- Further work should consolidate `child-pi.ts`, `async-runner.ts`, and `subagent-manager.ts` into a durable-first subagent runtime module.
|
|
22
|
+
|
|
23
|
+
## Source/pi-subagents2
|
|
24
|
+
|
|
25
|
+
Primary source for higher-level agent management and UI patterns.
|
|
26
|
+
|
|
27
|
+
- `src/agent-manager.ts`: agent lifecycle registry boundaries.
|
|
28
|
+
- `src/agent-runner.ts`: invocation/run abstraction separate from UI registration.
|
|
29
|
+
- `src/model-resolver.ts`: cleaner model resolution responsibility.
|
|
30
|
+
- `src/output-file.ts`: output file abstraction.
|
|
31
|
+
- `src/ui/agent-widget.ts`, `src/ui/conversation-viewer.ts`: compact live status and transcript viewing.
|
|
32
|
+
|
|
33
|
+
pi-crew alignment:
|
|
34
|
+
|
|
35
|
+
- Keep `Agent`/`crew_agent` tools as thin adapters over a durable manager.
|
|
36
|
+
- Avoid storing essential run mapping in memory only.
|
|
37
|
+
- Keep UI active-only and file-backed.
|
|
38
|
+
|
|
39
|
+
## Source/pi-mono
|
|
40
|
+
|
|
41
|
+
Primary source for Pi extension API/lifecycle constraints.
|
|
42
|
+
|
|
43
|
+
- `packages/coding-agent/src/core/extensions/types.ts`: extension context/tool contracts.
|
|
44
|
+
- `packages/coding-agent/src/core/extensions/runner.ts`: extension execution boundaries.
|
|
45
|
+
- `packages/coding-agent/src/core/model-registry.ts`: available model discovery.
|
|
46
|
+
- `packages/coding-agent/src/modes/interactive/interactive-mode.ts`: session lifecycle/UI behavior.
|
|
47
|
+
|
|
48
|
+
pi-crew alignment:
|
|
49
|
+
|
|
50
|
+
- Treat session-bound foreground workers differently from explicit async background workers.
|
|
51
|
+
- Do not assume hardcoded providers/models.
|
|
52
|
+
- Use Pi-native UI calls without modal auto-open by default.
|
|
53
|
+
|
|
54
|
+
## Source/pi-powerbar, pi-plan, pi-diff-review, pi-extensions*
|
|
55
|
+
|
|
56
|
+
Sources for UI and small-extension patterns.
|
|
57
|
+
|
|
58
|
+
- `pi-powerbar/src/powerbar/*`: low-noise status segment publishing.
|
|
59
|
+
- `pi-plan/src/plan-action-ui.ts`: action-oriented UI without persistent heavy overlays.
|
|
60
|
+
- `pi-diff-review/src/*`: command/tool registration and review UX patterns.
|
|
61
|
+
- `pi-extensions2/files-widget/*`: file-backed UI composition and navigation.
|
|
62
|
+
|
|
63
|
+
pi-crew alignment:
|
|
64
|
+
|
|
65
|
+
- Keep persistent widget active-only.
|
|
66
|
+
- Prefer manual dashboard/transcript commands for history.
|
|
67
|
+
- Avoid expensive render scans and auto-opening focus-capturing overlays.
|
|
68
|
+
|
|
69
|
+
## Current refactor checkpoints
|
|
70
|
+
|
|
71
|
+
- [x] Hide Windows console windows for background runner and child Pi workers.
|
|
72
|
+
- [x] Make parallel research shard workers start in parallel instead of depending on a single discover worker.
|
|
73
|
+
- [x] Keep direct-agent reconstruction gated by `workflow === "direct-agent"` only.
|
|
74
|
+
- [x] Persist subagent records and recover terminal results after restart.
|
|
75
|
+
- [x] Fail fast for unrecoverable persisted records without `runId` instead of hanging.
|
|
76
|
+
- [x] Persist direct-agent model override into task state for background/resume reconstruction.
|
|
77
|
+
|
|
78
|
+
## Remaining larger subsystem work
|
|
79
|
+
|
|
80
|
+
- Consolidate subagent runtime into `src/subagents/*` or equivalent durable-first module.
|
|
81
|
+
- Move model routing transparency into persisted task/subagent records: requested model, selected model, fallback chain, fallback reason.
|
|
82
|
+
- Add real integration smoke scripts for Windows process visibility, async restart recovery, and multi-shard fanout.
|
|
83
|
+
- Add adaptive planner repair/retry for invalid JSON instead of immediate block when safe.
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-vie
|
|
|
19
19
|
import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
|
|
20
20
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
21
21
|
import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
|
|
22
|
-
import { SubagentManager, type SubagentRecord, type SubagentSpawnOptions } from "../runtime/subagent-manager.ts";
|
|
22
|
+
import { readPersistedSubagentRecord, savePersistedSubagentRecord, SubagentManager, type SubagentRecord, type SubagentSpawnOptions } from "../runtime/subagent-manager.ts";
|
|
23
23
|
|
|
24
24
|
function parseRunArgs(args: string): TeamToolParamsValue {
|
|
25
25
|
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -109,6 +109,18 @@ function sendFollowUp(pi: ExtensionAPI, content: string): void {
|
|
|
109
109
|
sender.call(pi, { customType: "pi-crew-subagent-notification", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function refreshPersistedSubagentRecord(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): SubagentRecord {
|
|
113
|
+
if (!record.runId) return record;
|
|
114
|
+
const loaded = loadRunManifestById(ctx.cwd, record.runId);
|
|
115
|
+
if (!loaded) return record;
|
|
116
|
+
if (loaded.manifest.status === "completed" || loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled" || loaded.manifest.status === "blocked") {
|
|
117
|
+
const refreshed = { ...record, status: loaded.manifest.status === "completed" ? "completed" as const : loaded.manifest.status === "cancelled" ? "cancelled" as const : "failed" as const, error: loaded.manifest.status === "completed" ? undefined : loaded.manifest.summary, completedAt: record.completedAt ?? Date.now() };
|
|
118
|
+
savePersistedSubagentRecord(ctx.cwd, refreshed);
|
|
119
|
+
return refreshed;
|
|
120
|
+
}
|
|
121
|
+
return record;
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
function formatSubagentRecord(record: SubagentRecord): string {
|
|
113
125
|
const duration = record.completedAt ? `${Math.round((record.completedAt - record.startedAt) / 1000)}s` : "running";
|
|
114
126
|
return [
|
|
@@ -338,9 +350,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
338
350
|
agent: spawnOptions.type,
|
|
339
351
|
goal: spawnOptions.prompt,
|
|
340
352
|
model: spawnOptions.model,
|
|
341
|
-
async:
|
|
353
|
+
async: spawnOptions.background,
|
|
342
354
|
config: spawnOptions.maxTurns ? { runtime: { maxTurns: spawnOptions.maxTurns } } : undefined,
|
|
343
|
-
}, spawnOptions.background ? { ...ctx, signal: childSignal
|
|
355
|
+
}, spawnOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
|
|
344
356
|
const record = subagentManager.spawn(options, runner, options.background ? undefined : signal);
|
|
345
357
|
if (options.background || record.status === "queued") {
|
|
346
358
|
return subagentToolResult([`Agent ${record.status === "queued" ? "queued" : "started"}.`, `Agent ID: ${record.id}`, `Type: ${record.type}`, `Description: ${record.description}`, "Use get_subagent_result to retrieve output. Do not duplicate this agent's work."].join("\n"), { agentId: record.id, status: record.status });
|
|
@@ -360,18 +372,40 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
360
372
|
wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })),
|
|
361
373
|
verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })),
|
|
362
374
|
}) as never,
|
|
363
|
-
async execute(_id, params,
|
|
375
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
364
376
|
const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
|
|
365
377
|
if (!p.agent_id) return subagentToolResult("get_subagent_result requires agent_id.", {}, true);
|
|
366
|
-
const
|
|
378
|
+
const inMemory = subagentManager.getRecord(p.agent_id);
|
|
379
|
+
const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
|
|
367
380
|
if (!record) return subagentToolResult(`Agent not found: ${p.agent_id}`, {}, true);
|
|
368
|
-
let current = record;
|
|
381
|
+
let current = refreshPersistedSubagentRecord(ctx, record);
|
|
382
|
+
if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
|
|
383
|
+
current = { ...current, status: "error", error: "Subagent was interrupted before its durable run id was recorded; it cannot be recovered after restart.", completedAt: current.completedAt ?? Date.now() };
|
|
384
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
385
|
+
}
|
|
369
386
|
if (p.wait && (current.status === "running" || current.status === "queued")) {
|
|
370
387
|
current.resultConsumed = true;
|
|
371
|
-
|
|
388
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
389
|
+
const waited = await subagentManager.waitForRecord(current.id);
|
|
390
|
+
if (waited) current = waited;
|
|
391
|
+
else {
|
|
392
|
+
while (current.status === "running" || current.status === "queued") {
|
|
393
|
+
if (signal?.aborted) {
|
|
394
|
+
current = { ...current, status: "error", error: "Waiting for subagent result was aborted.", completedAt: Date.now() };
|
|
395
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
399
|
+
current = refreshPersistedSubagentRecord(ctx, current);
|
|
400
|
+
if (!current.runId) break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
372
403
|
}
|
|
373
404
|
const output = readSubagentRunResult(ctx, current);
|
|
374
|
-
if (current.status !== "running" && current.status !== "queued")
|
|
405
|
+
if (current.status !== "running" && current.status !== "queued") {
|
|
406
|
+
current.resultConsumed = true;
|
|
407
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
408
|
+
}
|
|
375
409
|
const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "Agent is still running. Use wait=true or check again later." : current.error ?? "No output."].filter((line): line is string => Boolean(line)).join("\n");
|
|
376
410
|
return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
|
|
377
411
|
},
|
|
@@ -382,9 +416,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
382
416
|
label: "Steer Agent",
|
|
383
417
|
description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
|
|
384
418
|
parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
|
|
385
|
-
async execute(_id, params) {
|
|
419
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
386
420
|
const p = params as { agent_id?: string; message?: string };
|
|
387
|
-
const record = p.agent_id ? subagentManager.getRecord(p.agent_id) : undefined;
|
|
421
|
+
const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
|
|
388
422
|
if (!record) return subagentToolResult(`Agent not found: ${p.agent_id ?? ""}`, {}, true);
|
|
389
423
|
return subagentToolResult([`Steering request noted for ${record.id}.`, "Current default pi-crew backend is child-process, so mid-turn session.steer is not available yet.", record.runId ? `Use team cancel runId=${record.runId} if the agent must be interrupted.` : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
|
|
390
424
|
},
|
|
@@ -5,7 +5,7 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
|
5
5
|
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
6
6
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
7
7
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
8
|
-
import type { WorkflowConfig
|
|
8
|
+
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
9
9
|
import { effectiveAutonomousConfig, loadConfig, updateAutonomousConfig, updateConfig, type PiTeamsAutonomousConfig, type PiTeamsConfig } from "../config/config.ts";
|
|
10
10
|
import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
|
|
11
11
|
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
@@ -46,6 +46,8 @@ import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "
|
|
|
46
46
|
import { appendLiveAgentControlRequest } from "../runtime/live-agent-control.ts";
|
|
47
47
|
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
48
48
|
import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
|
|
49
|
+
import { directTeamAndWorkflowFromRun } from "../runtime/direct-run.ts";
|
|
50
|
+
import { expandParallelResearchWorkflow } from "../runtime/parallel-research.ts";
|
|
49
51
|
|
|
50
52
|
export interface TeamToolDetails {
|
|
51
53
|
action: string;
|
|
@@ -171,48 +173,6 @@ function commandExists(command: string, args: string[]): { ok: boolean; detail:
|
|
|
171
173
|
return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
function sourcePiProjects(cwd: string): string[] {
|
|
175
|
-
const sourceDir = path.join(cwd, "Source");
|
|
176
|
-
try {
|
|
177
|
-
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
178
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
|
|
179
|
-
.map((entry) => `Source/${entry.name}`)
|
|
180
|
-
.sort();
|
|
181
|
-
} catch {
|
|
182
|
-
return [];
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function chunkProjects(projects: string[], target = 4): string[][] {
|
|
187
|
-
const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
|
|
188
|
-
projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
|
|
189
|
-
return chunks.filter((chunk) => chunk.length > 0);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
|
|
193
|
-
if (workflow.name !== "parallel-research") return workflow;
|
|
194
|
-
const projects = sourcePiProjects(cwd);
|
|
195
|
-
if (projects.length === 0) return workflow;
|
|
196
|
-
const chunks = chunkProjects(projects, Math.min(6, Math.max(4, Math.ceil(projects.length / 4))));
|
|
197
|
-
const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
|
|
198
|
-
id: `explore-shard-${index + 1}`,
|
|
199
|
-
role: "explorer",
|
|
200
|
-
dependsOn: ["discover"],
|
|
201
|
-
parallelGroup: "explore",
|
|
202
|
-
reads: paths,
|
|
203
|
-
task: [`Explore this dynamic shard for: {goal}`, "", "Paths:", ...paths.map((item) => `- ${item}`), "", "Focus on purpose, architecture, runtime/UI patterns, package config, docs, and lessons for pi-crew."].join("\n"),
|
|
204
|
-
}));
|
|
205
|
-
return {
|
|
206
|
-
...workflow,
|
|
207
|
-
steps: [
|
|
208
|
-
{ id: "discover", role: "explorer", task: `Discover and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}` },
|
|
209
|
-
...exploreSteps,
|
|
210
|
-
{ id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations." },
|
|
211
|
-
{ id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
|
|
212
|
-
],
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
176
|
function effectiveRunConfig(base: PiTeamsConfig, rawOverride: unknown): PiTeamsConfig {
|
|
217
177
|
const patch = configPatchFromConfig(rawOverride);
|
|
218
178
|
return {
|
|
@@ -513,9 +473,11 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
513
473
|
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
514
474
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "resume", status: "error" }, true);
|
|
515
475
|
if (!loaded.manifest.workflow) return result(`Run '${params.runId}' has no workflow to resume.`, { action: "resume", status: "error" }, true);
|
|
516
|
-
const
|
|
476
|
+
const agents = allAgents(discoverAgents(ctx.cwd));
|
|
477
|
+
const direct = directTeamAndWorkflowFromRun(loaded.manifest, loaded.tasks, agents);
|
|
478
|
+
const team = direct?.team ?? allTeams(discoverTeams(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.team);
|
|
517
479
|
if (!team) return result(`Team '${loaded.manifest.team}' not found.`, { action: "resume", status: "error" }, true);
|
|
518
|
-
const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.workflow);
|
|
480
|
+
const workflow = direct?.workflow ?? allWorkflows(discoverWorkflows(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.workflow);
|
|
519
481
|
if (!workflow) return result(`Workflow '${loaded.manifest.workflow}' not found.`, { action: "resume", status: "error" }, true);
|
|
520
482
|
return await withRunLock(loaded.manifest, async () => {
|
|
521
483
|
const resetTasks = loaded.tasks.map((task) => task.status === "failed" || task.status === "cancelled" || task.status === "skipped" || task.status === "running" ? { ...task, status: "queued" as const, error: undefined, startedAt: undefined, finishedAt: undefined, claim: undefined } : task);
|
|
@@ -524,7 +486,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
524
486
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
525
487
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
526
488
|
const executeWorkers = runtime.kind !== "scaffold";
|
|
527
|
-
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents
|
|
489
|
+
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
|
|
528
490
|
return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
529
491
|
});
|
|
530
492
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, type SpawnOptions } from "node:child_process";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
@@ -36,6 +36,16 @@ export interface SpawnBackgroundTeamRunResult {
|
|
|
36
36
|
logPath: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function buildBackgroundSpawnOptions(manifest: TeamRunManifest, logFd: number): SpawnOptions {
|
|
40
|
+
return {
|
|
41
|
+
cwd: manifest.cwd,
|
|
42
|
+
detached: true,
|
|
43
|
+
stdio: ["ignore", logFd, logFd],
|
|
44
|
+
env: { ...process.env },
|
|
45
|
+
windowsHide: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
export function spawnBackgroundTeamRun(manifest: TeamRunManifest): SpawnBackgroundTeamRunResult {
|
|
40
50
|
const runnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "background-runner.ts");
|
|
41
51
|
const logPath = path.join(manifest.stateRoot, "background.log");
|
|
@@ -43,12 +53,7 @@ export function spawnBackgroundTeamRun(manifest: TeamRunManifest): SpawnBackgrou
|
|
|
43
53
|
const logFd = fs.openSync(logPath, "a");
|
|
44
54
|
const command = getBackgroundRunnerCommand(runnerPath, manifest.cwd, manifest.runId);
|
|
45
55
|
fs.appendFileSync(logPath, `[pi-crew] background loader=${command.loader}\n`, "utf-8");
|
|
46
|
-
const child = spawn(process.execPath, command.args,
|
|
47
|
-
cwd: manifest.cwd,
|
|
48
|
-
detached: true,
|
|
49
|
-
stdio: ["ignore", logFd, logFd],
|
|
50
|
-
env: { ...process.env },
|
|
51
|
-
});
|
|
56
|
+
const child = spawn(process.execPath, command.args, buildBackgroundSpawnOptions(manifest, logFd));
|
|
52
57
|
child.unref();
|
|
53
58
|
fs.closeSync(logFd);
|
|
54
59
|
return { pid: child.pid, logPath };
|
|
@@ -6,6 +6,8 @@ import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows
|
|
|
6
6
|
import { loadConfig } from "../config/config.ts";
|
|
7
7
|
import { executeTeamRun } from "./team-runner.ts";
|
|
8
8
|
import { resolveCrewRuntime } from "./runtime-resolver.ts";
|
|
9
|
+
import { directTeamAndWorkflowFromRun } from "./direct-run.ts";
|
|
10
|
+
import { expandParallelResearchWorkflow } from "./parallel-research.ts";
|
|
9
11
|
|
|
10
12
|
function argValue(name: string): string | undefined {
|
|
11
13
|
const index = process.argv.indexOf(name);
|
|
@@ -24,14 +26,16 @@ async function main(): Promise<void> {
|
|
|
24
26
|
appendEvent(manifest.eventsPath, { type: "async.started", runId: manifest.runId, data: { pid: process.pid } });
|
|
25
27
|
|
|
26
28
|
try {
|
|
27
|
-
const team = allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
|
|
28
|
-
if (!team) throw new Error(`Team '${manifest.team}' not found.`);
|
|
29
|
-
const workflow = allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
|
|
30
|
-
if (!workflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
|
|
31
29
|
const agents = allAgents(discoverAgents(cwd));
|
|
30
|
+
const direct = directTeamAndWorkflowFromRun(manifest, tasks, agents);
|
|
31
|
+
const team = direct?.team ?? allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
|
|
32
|
+
if (!team) throw new Error(`Team '${manifest.team}' not found.`);
|
|
33
|
+
const baseWorkflow = direct?.workflow ?? allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
|
|
34
|
+
if (!baseWorkflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
|
|
35
|
+
const workflow = expandParallelResearchWorkflow(baseWorkflow, cwd);
|
|
32
36
|
const loadedConfig = loadConfig(cwd);
|
|
33
37
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
34
|
-
const executeWorkers = runtime.kind
|
|
38
|
+
const executeWorkers = runtime.kind !== "scaffold";
|
|
35
39
|
const result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime });
|
|
36
40
|
manifest = result.manifest;
|
|
37
41
|
tasks = result.tasks;
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
1
|
+
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
@@ -66,6 +66,16 @@ export interface ChildPiRunResult {
|
|
|
66
66
|
error?: string;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv): SpawnOptions {
|
|
70
|
+
return {
|
|
71
|
+
cwd,
|
|
72
|
+
env,
|
|
73
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
74
|
+
detached: process.platform !== "win32",
|
|
75
|
+
windowsHide: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
function appendTranscript(input: ChildPiRunInput, line: string): void {
|
|
70
80
|
if (!input.transcriptPath) return;
|
|
71
81
|
fs.mkdirSync(path.dirname(input.transcriptPath), { recursive: true });
|
|
@@ -225,12 +235,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
225
235
|
const spawnSpec = getPiSpawnCommand(built.args);
|
|
226
236
|
try {
|
|
227
237
|
return await new Promise<ChildPiRunResult>((resolve) => {
|
|
228
|
-
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
229
|
-
cwd: input.cwd,
|
|
230
|
-
env: { ...process.env, ...built.env },
|
|
231
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
232
|
-
detached: process.platform !== "win32",
|
|
233
|
-
});
|
|
238
|
+
const child = spawn(spawnSpec.command, spawnSpec.args, buildChildPiSpawnOptions(input.cwd, { ...process.env, ...built.env }));
|
|
234
239
|
if (child.pid) activeChildProcesses.set(child.pid, child);
|
|
235
240
|
let stdout = "";
|
|
236
241
|
let stderr = "";
|
|
@@ -15,7 +15,7 @@ export interface BatchConcurrencyDecision {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function defaultWorkflowConcurrency(workflowName: string): number {
|
|
18
|
-
if (workflowName === "parallel-research") return
|
|
18
|
+
if (workflowName === "parallel-research") return 6;
|
|
19
19
|
if (workflowName === "research") return 2;
|
|
20
20
|
if (workflowName === "implementation" || workflowName === "review" || workflowName === "default") return 2;
|
|
21
21
|
return 1;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
+
import type { TeamConfig } from "../teams/team-config.ts";
|
|
4
|
+
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
5
|
+
|
|
6
|
+
export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
|
|
7
|
+
return manifest.workflow === "direct-agent";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
|
|
11
|
+
if (!isDirectRun(manifest)) return undefined;
|
|
12
|
+
const firstTask = tasks[0];
|
|
13
|
+
const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
|
|
14
|
+
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
15
|
+
const role = firstTask?.role ?? "agent";
|
|
16
|
+
const stepId = firstTask?.stepId ?? "01_agent";
|
|
17
|
+
return {
|
|
18
|
+
team: {
|
|
19
|
+
name: manifest.team,
|
|
20
|
+
description: `Direct subagent run for ${agentName}`,
|
|
21
|
+
source: "builtin",
|
|
22
|
+
filePath: "<generated>",
|
|
23
|
+
roles: [{ name: role, agent: agentName, description: agent?.description }],
|
|
24
|
+
defaultWorkflow: "direct-agent",
|
|
25
|
+
workspaceMode: manifest.workspaceMode,
|
|
26
|
+
},
|
|
27
|
+
workflow: {
|
|
28
|
+
name: manifest.workflow ?? "direct-agent",
|
|
29
|
+
description: `Direct task for ${agentName}`,
|
|
30
|
+
source: "builtin",
|
|
31
|
+
filePath: "<generated>",
|
|
32
|
+
steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
4
|
+
|
|
5
|
+
export function sourcePiProjects(cwd: string): string[] {
|
|
6
|
+
const sourceDir = path.join(cwd, "Source");
|
|
7
|
+
try {
|
|
8
|
+
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
9
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
|
|
10
|
+
.map((entry) => `Source/${entry.name}`)
|
|
11
|
+
.sort();
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function chunkProjects(projects: string[], target = 6): string[][] {
|
|
18
|
+
const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
|
|
19
|
+
projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
|
|
20
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
|
|
24
|
+
if (workflow.name !== "parallel-research") return workflow;
|
|
25
|
+
const projects = sourcePiProjects(cwd);
|
|
26
|
+
if (projects.length === 0) return workflow;
|
|
27
|
+
const chunks = chunkProjects(projects, Math.min(8, Math.max(4, Math.ceil(projects.length / 3))));
|
|
28
|
+
const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
|
|
29
|
+
id: `explore-shard-${index + 1}`,
|
|
30
|
+
role: "explorer",
|
|
31
|
+
parallelGroup: "explore",
|
|
32
|
+
reads: paths,
|
|
33
|
+
task: [`Explore this dynamic shard for: {goal}`, "", "Paths:", ...paths.map((item) => `- ${item}`), "", "Focus on purpose, architecture, runtime/UI patterns, package config, docs, and lessons for pi-crew."].join("\n"),
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
...workflow,
|
|
37
|
+
steps: [
|
|
38
|
+
{ id: "discover", role: "explorer", parallelGroup: "inventory", task: `Quickly inventory and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}\n\nDo not block shard work; summarize routing notes only.` },
|
|
39
|
+
...exploreSteps,
|
|
40
|
+
{ id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations. Use discover output if available, but prioritize completed shard outputs." },
|
|
41
|
+
{ id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
2
4
|
import type { PiTeamsToolResult } from "../extension/tool-result.ts";
|
|
5
|
+
import { projectPiRoot } from "../utils/paths.ts";
|
|
3
6
|
|
|
4
7
|
export type SubagentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "error" | "stopped";
|
|
5
8
|
|
|
@@ -42,6 +45,32 @@ interface QueuedSpawn {
|
|
|
42
45
|
|
|
43
46
|
const TERMINAL_RUN_STATUS = new Set(["completed", "failed", "cancelled", "blocked"]);
|
|
44
47
|
|
|
48
|
+
function persistedSubagentPath(cwd: string, id: string): string {
|
|
49
|
+
return path.join(projectPiRoot(cwd), "teams", "state", "subagents", `${id}.json`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function serializableRecord(record: SubagentRecord): SubagentRecord {
|
|
53
|
+
const { promise: _promise, ...rest } = record;
|
|
54
|
+
return rest;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function savePersistedSubagentRecord(cwd: string, record: SubagentRecord): void {
|
|
58
|
+
try {
|
|
59
|
+
const filePath = persistedSubagentPath(cwd, record.id);
|
|
60
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
61
|
+
fs.writeFileSync(filePath, `${JSON.stringify(serializableRecord(record), null, 2)}\n`, "utf-8");
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readPersistedSubagentRecord(cwd: string, id: string): SubagentRecord | undefined {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(fs.readFileSync(persistedSubagentPath(cwd, id), "utf-8"));
|
|
68
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as SubagentRecord : undefined;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
function resultText(result: PiTeamsToolResult): string {
|
|
46
75
|
return result.content?.map((item) => item.type === "text" ? item.text : "").filter(Boolean).join("\n") ?? "";
|
|
47
76
|
}
|
|
@@ -78,6 +107,7 @@ export class SubagentManager {
|
|
|
78
107
|
background: options.background,
|
|
79
108
|
};
|
|
80
109
|
this.records.set(record.id, record);
|
|
110
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
81
111
|
if (record.status === "queued") {
|
|
82
112
|
this.queue.push({ record, options, runner, signal });
|
|
83
113
|
return record;
|
|
@@ -155,11 +185,13 @@ export class SubagentManager {
|
|
|
155
185
|
if (options.background) this.runningBackground++;
|
|
156
186
|
record.status = "running";
|
|
157
187
|
record.startedAt = Date.now();
|
|
188
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
158
189
|
record.promise = (async () => {
|
|
159
190
|
try {
|
|
160
191
|
const result = await runner(options, signal);
|
|
161
192
|
record.runId = detailsRunId(result);
|
|
162
193
|
record.result = resultText(result);
|
|
194
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
163
195
|
if (result.isError) {
|
|
164
196
|
record.status = "error";
|
|
165
197
|
record.error = record.result;
|
|
@@ -173,6 +205,7 @@ export class SubagentManager {
|
|
|
173
205
|
} finally {
|
|
174
206
|
if (options.background) this.runningBackground = Math.max(0, this.runningBackground - 1);
|
|
175
207
|
record.completedAt = record.completedAt ?? Date.now();
|
|
208
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
176
209
|
this.onComplete?.(record);
|
|
177
210
|
this.drainQueue();
|
|
178
211
|
}
|
|
@@ -194,6 +227,7 @@ export class SubagentManager {
|
|
|
194
227
|
record.status = loaded.manifest.status === "completed" ? "completed" : loaded.manifest.status === "cancelled" ? "cancelled" : "failed";
|
|
195
228
|
record.error = record.status === "completed" ? undefined : loaded.manifest.summary;
|
|
196
229
|
record.completedAt = Date.now();
|
|
230
|
+
savePersistedSubagentRecord(cwd, record);
|
|
197
231
|
return;
|
|
198
232
|
}
|
|
199
233
|
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
@@ -63,15 +63,25 @@ function mergeArtifacts(items: ArtifactDescriptor[]): ArtifactDescriptor[] {
|
|
|
63
63
|
return [...byPath.values()];
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function
|
|
66
|
+
function isNonTerminalTaskStatus(status: TeamTaskState["status"]): boolean {
|
|
67
|
+
return status === "queued" || status === "running";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shouldMergeTaskUpdate(current: TeamTaskState, updated: TeamTaskState): boolean {
|
|
71
|
+
// Parallel workers receive the same input snapshot. A later result may still
|
|
72
|
+
// contain stale queued/running copies of tasks that another worker already
|
|
73
|
+
// completed. Never let those stale snapshots regress durable task state.
|
|
74
|
+
if (!isNonTerminalTaskStatus(current.status) && isNonTerminalTaskStatus(updated.status)) return false;
|
|
75
|
+
return updated.status !== current.status || updated.finishedAt !== current.finishedAt || updated.startedAt !== current.startedAt || Boolean(updated.resultArtifact) || Boolean(updated.error) || Boolean(updated.modelAttempts?.length) || Boolean(updated.usage);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function __test__mergeTaskUpdates(base: TeamTaskState[], results: Array<{ tasks: TeamTaskState[] }>): TeamTaskState[] {
|
|
67
79
|
let merged = base;
|
|
68
80
|
for (const result of results) {
|
|
69
81
|
for (const updated of result.tasks) {
|
|
70
82
|
const current = merged.find((task) => task.id === updated.id);
|
|
71
|
-
if (!current) continue;
|
|
72
|
-
|
|
73
|
-
merged = merged.map((task) => task.id === updated.id ? updated : task);
|
|
74
|
-
}
|
|
83
|
+
if (!current || !shouldMergeTaskUpdate(current, updated)) continue;
|
|
84
|
+
merged = merged.map((task) => task.id === updated.id ? updated : task);
|
|
75
85
|
}
|
|
76
86
|
}
|
|
77
87
|
return refreshTaskGraphQueues(merged);
|
|
@@ -341,7 +351,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
341
351
|
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, limits: input.limits });
|
|
342
352
|
}));
|
|
343
353
|
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
344
|
-
tasks =
|
|
354
|
+
tasks = __test__mergeTaskUpdates(tasks, results);
|
|
345
355
|
const injectedAfterBatch = injectAdaptivePlanIfReady({ manifest, tasks, workflow, team: input.team });
|
|
346
356
|
if (injectedAfterBatch.missingPlan) {
|
|
347
357
|
tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
|
package/src/state/state-store.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function createTasksFromWorkflow(runId: string, workflow: WorkflowConfig,
|
|
|
55
55
|
status: "queued",
|
|
56
56
|
dependsOn: dependencies,
|
|
57
57
|
cwd,
|
|
58
|
+
model: step.model,
|
|
58
59
|
graph: {
|
|
59
60
|
taskId: id,
|
|
60
61
|
parentId: dependencies[0] ? stepToTaskId.get(dependencies[0]) : undefined,
|
package/src/state/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ name: parallel-research
|
|
|
3
3
|
description: Parallel research team for multi-project/source audits
|
|
4
4
|
workspaceMode: single
|
|
5
5
|
defaultWorkflow: parallel-research
|
|
6
|
-
maxConcurrency:
|
|
6
|
+
maxConcurrency: 6
|
|
7
7
|
triggers: đọc sâu, deep read, deep research, source audit, multiple projects, parallel research, pi-*
|
|
8
8
|
category: research
|
|
9
9
|
cost: cheap
|
|
@@ -35,4 +35,4 @@ Rules:
|
|
|
35
35
|
- Use parallel tasks in the same phase only when their work is independent.
|
|
36
36
|
- Later phases depend on all tasks in the previous phase.
|
|
37
37
|
- Include verification/review tasks when implementation is requested.
|
|
38
|
-
- Do not include more than 12 total subagents
|
|
38
|
+
- Do not include more than 12 total subagents; split or summarize oversized plans instead.
|
|
@@ -10,28 +10,24 @@ Discover the relevant files/projects for: {goal}. Return a shard plan with paths
|
|
|
10
10
|
|
|
11
11
|
## explore-core
|
|
12
12
|
role: explorer
|
|
13
|
-
dependsOn: discover
|
|
14
13
|
parallelGroup: explore
|
|
15
14
|
|
|
16
15
|
Explore the core/runtime shard from the discover output. Focus on architecture, package config, docs, and reusable patterns for: {goal}
|
|
17
16
|
|
|
18
17
|
## explore-ui
|
|
19
18
|
role: explorer
|
|
20
|
-
dependsOn: discover
|
|
21
19
|
parallelGroup: explore
|
|
22
20
|
|
|
23
21
|
Explore the UI/TUI/extension-interface shard from the discover output. Focus on widgets, overlays, commands, status bars, package config, docs, and reusable patterns for: {goal}
|
|
24
22
|
|
|
25
23
|
## explore-runtime
|
|
26
24
|
role: explorer
|
|
27
|
-
dependsOn: discover
|
|
28
25
|
parallelGroup: explore
|
|
29
26
|
|
|
30
27
|
Explore the worker/runtime/subagent/runtime-control shard from the discover output. Focus on process/session/runtime orchestration, event streams, logs, package config, docs, and reusable patterns for: {goal}
|
|
31
28
|
|
|
32
29
|
## explore-extensions
|
|
33
30
|
role: explorer
|
|
34
|
-
dependsOn: discover
|
|
35
31
|
parallelGroup: explore
|
|
36
32
|
|
|
37
33
|
Explore the extension bundle/small-package shard from the discover output. Focus on package config, extension registration, commands/tools, docs, and reusable patterns for: {goal}
|
|
@@ -40,7 +36,7 @@ Explore the extension bundle/small-package shard from the discover output. Focus
|
|
|
40
36
|
role: analyst
|
|
41
37
|
dependsOn: explore-core, explore-ui, explore-runtime, explore-extensions
|
|
42
38
|
|
|
43
|
-
Synthesize all shard findings. Identify common patterns, gaps, and concrete recommendations.
|
|
39
|
+
Synthesize all shard findings. Use discover output if available, but do not require it. Identify common patterns, gaps, and concrete recommendations.
|
|
44
40
|
|
|
45
41
|
## write
|
|
46
42
|
role: writer
|