pi-crew 0.1.21 → 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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -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, WorkflowStep } from "../workflows/workflow-config.ts";
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";
@@ -47,6 +47,7 @@ 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
49
  import { directTeamAndWorkflowFromRun } from "../runtime/direct-run.ts";
50
+ import { expandParallelResearchWorkflow } from "../runtime/parallel-research.ts";
50
51
 
51
52
  export interface TeamToolDetails {
52
53
  action: string;
@@ -172,48 +173,6 @@ function commandExists(command: string, args: string[]): { ok: boolean; detail:
172
173
  return { ok: false, detail: output.error?.message ?? firstOutputLine(output.stdout, output.stderr) };
173
174
  }
174
175
 
175
- function sourcePiProjects(cwd: string): string[] {
176
- const sourceDir = path.join(cwd, "Source");
177
- try {
178
- return fs.readdirSync(sourceDir, { withFileTypes: true })
179
- .filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
180
- .map((entry) => `Source/${entry.name}`)
181
- .sort();
182
- } catch {
183
- return [];
184
- }
185
- }
186
-
187
- function chunkProjects(projects: string[], target = 4): string[][] {
188
- const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
189
- projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
190
- return chunks.filter((chunk) => chunk.length > 0);
191
- }
192
-
193
- function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
194
- if (workflow.name !== "parallel-research") return workflow;
195
- const projects = sourcePiProjects(cwd);
196
- if (projects.length === 0) return workflow;
197
- const chunks = chunkProjects(projects, Math.min(6, Math.max(4, Math.ceil(projects.length / 4))));
198
- const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
199
- id: `explore-shard-${index + 1}`,
200
- role: "explorer",
201
- dependsOn: ["discover"],
202
- parallelGroup: "explore",
203
- reads: paths,
204
- 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"),
205
- }));
206
- return {
207
- ...workflow,
208
- steps: [
209
- { id: "discover", role: "explorer", task: `Discover and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}` },
210
- ...exploreSteps,
211
- { id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations." },
212
- { id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
213
- ],
214
- };
215
- }
216
-
217
176
  function effectiveRunConfig(base: PiTeamsConfig, rawOverride: unknown): PiTeamsConfig {
218
177
  const patch = configPatchFromConfig(rawOverride);
219
178
  return {
@@ -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 };
@@ -7,6 +7,7 @@ import { loadConfig } from "../config/config.ts";
7
7
  import { executeTeamRun } from "./team-runner.ts";
8
8
  import { resolveCrewRuntime } from "./runtime-resolver.ts";
9
9
  import { directTeamAndWorkflowFromRun } from "./direct-run.ts";
10
+ import { expandParallelResearchWorkflow } from "./parallel-research.ts";
10
11
 
11
12
  function argValue(name: string): string | undefined {
12
13
  const index = process.argv.indexOf(name);
@@ -29,8 +30,9 @@ async function main(): Promise<void> {
29
30
  const direct = directTeamAndWorkflowFromRun(manifest, tasks, agents);
30
31
  const team = direct?.team ?? allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
31
32
  if (!team) throw new Error(`Team '${manifest.team}' not found.`);
32
- const workflow = direct?.workflow ?? allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
33
- if (!workflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' 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);
34
36
  const loadedConfig = loadConfig(cwd);
35
37
  const runtime = await resolveCrewRuntime(loadedConfig.config);
36
38
  const executeWorkers = runtime.kind !== "scaffold";
@@ -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 4;
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,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
+ }
@@ -63,15 +63,25 @@ function mergeArtifacts(items: ArtifactDescriptor[]): ArtifactDescriptor[] {
63
63
  return [...byPath.values()];
64
64
  }
65
65
 
66
- function mergeTaskUpdates(base: TeamTaskState[], results: Array<{ tasks: TeamTaskState[] }>): TeamTaskState[] {
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
- if (updated.status !== current.status || updated.finishedAt || updated.startedAt || updated.resultArtifact || updated.error) {
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 = mergeTaskUpdates(tasks, results);
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.");
@@ -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: 4
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
@@ -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