pi-crew 0.9.14 → 0.9.16

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/CHANGELOG.md CHANGED
@@ -1,5 +1,131 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.9.16] — cancel wipes the trace (2026-06-29)
4
+
5
+ User-driven refinement of the cancellation policy. Previously, cancelled / stopped subagents and crew-agent records were preserved on disk (`agent_*.json` files in `.crew/state/subagents/`, plus `agents.json` + per-task `status.json` in the run state directory) and continued to appear in the UI dashboard / widget / transcript viewer. The user explicitly asked: *"một khi đã cancel thì phải xóa luôn dấu vết về subagent đó chứ để lại làm gì nữa"* — once cancelled, the trace must be wiped. This release implements that policy.
6
+
7
+ ### Changed — terminal-status deletion
8
+
9
+ - **Subagent records** (`.crew/state/subagents/<id>.json`) — added `removePersistedSubagentRecord(cwd, id)` in `src/runtime/subagent-manager.ts`. On terminal status `cancelled` / `stopped` / `terminated:true`, the persisted file is **deleted immediately** (no linger). The file is saved first so any concurrent reader sees the final status, then `unlink`'d within microseconds — the next widget/dashboard tick sees no entry.
10
+
11
+ - **Crew-agent records** (per-run `agents.json` index + per-task `status.json`) — added `removeCrewAgent(manifest, taskId)` in `src/runtime/crew-agent-records.ts`. `upsertCrewAgent()` now checks `shouldDeleteCrewAgentOnTerminalStatus(record)` and dispatches to `removeCrewAgent` instead of `saveCrewAgents` for `cancelled` / `stopped` records. Both the index entry AND the per-task status.json are wiped.
12
+
13
+ ### What is preserved vs wiped
14
+
15
+ | Status | Persisted? | Rationale |
16
+ |---|---|---|
17
+ | `completed` | ✅ yes | Successful work — audit value |
18
+ | `failed` | ✅ yes | Agent errored — debugging value |
19
+ | `error` | ✅ yes | Unexpected exception — debugging value |
20
+ | `cancelled` | ❌ **wiped** | User-initiated cancellation — no trace |
21
+ | `stopped` | ❌ **wiped** | External abort — no trace |
22
+ | `terminated: true` (any status) | ❌ **wiped** | Any termination event — no trace |
23
+
24
+ ### In-memory handles
25
+
26
+ `terminateLiveAgent` in `src/runtime/live-agent-manager.ts:169` already deletes from the `liveAgents` Map immediately after abort — no change needed. The `evictStaleLiveAgentHandles` fallback (10-min linger for orphan terminal handles) remains as a safety net.
27
+
28
+ ### New helper predicates
29
+
30
+ - `shouldDeleteOnTerminalStatus(record)` — for `.crew/state/subagents/` records
31
+ - `shouldDeleteCrewAgentOnTerminalStatus(record)` — for per-run `agents.json` records
32
+
33
+ Both exported for downstream callers and tests.
34
+
35
+ ### Tests
36
+
37
+ - **NEW `test/unit/cancellation-trace-wipe.test.ts`** — 15 cases covering:
38
+ - `shouldDeleteOnTerminalStatus` / `shouldDeleteCrewAgentOnTerminalStatus` predicates for all 6 statuses
39
+ - `removePersistedSubagentRecord` happy path + ENOENT safe-fail
40
+ - `removeCrewAgent` removes from `agents.json` index AND per-task `status.json`
41
+ - `upsertCrewAgent` integration: cancelled → wiped, completed/failed → preserved
42
+
43
+ - **Existing tests still pass** — 118/118 across 9 test files (subagent-manager-cov, subagent-tools-integration, live-manager-cov, direct-agent-run, team-runner-merge, process-lifecycle, preflight-validator, topology-analyzer, cancellation-trace-wipe). 0 regressions.
44
+
45
+ ### Verification
46
+
47
+ - `tsc --noEmit` → EXIT 0
48
+ - `node scripts/check-lazy-imports.mjs` → "All dynamic imports have `// LAZY:` marker"
49
+ - `npm test` (focused 9-file suite) → 118/118 pass
50
+
51
+ ### Honesty discipline
52
+
53
+ - **Failed subagents are NOT wiped** — they may carry useful debugging info (error stack, partial output). Different from cancel. If the user later wants failed→wipe too, it's a 1-line predicate change.
54
+ - **No tombstone** — the cancellation event is in `events.jsonl` (the run's audit log) and `notifications/`. Disk-level agent files are intentionally gone after cancel.
55
+ - **Filesystem race window** is microseconds (save then unlink); under heavy concurrent access a reader might briefly see the final status before the file disappears. Acceptable trade-off for the "no trace" policy.
56
+
57
+ ### Files touched
58
+
59
+ | File | Change |
60
+ |---|---|
61
+ | `src/runtime/subagent-manager.ts` | +2 functions (`removePersistedSubagentRecord`, `shouldDeleteOnTerminalStatus`); modified `markStopped` + `runSubagent` finally block to wipe on cancel |
62
+ | `src/runtime/crew-agent-records.ts` | +2 functions (`removeCrewAgent`, `shouldDeleteCrewAgentOnTerminalStatus`); modified `upsertCrewAgent` to dispatch to remove on cancel |
63
+ | `test/unit/cancellation-trace-wipe.test.ts` | NEW (15 cases, 8.3 KB) |
64
+ | `CHANGELOG.md` | this entry |
65
+ | `package.json` | 0.9.15 → 0.9.16 |
66
+
67
+ ## [v0.9.15] — workflow topology advisory (2026-06-29)
68
+
69
+ After a parallel-research assessment of v0.9.13 + v0.9.14 (`research-findings/pi-crew-performance-quality-assessment.md`, effectiveness evaluation at `.crew/research/pi-crew-effectiveness-evaluation.md`), a user-driven refinement showed that the original BLOCK-on-misuse design was too aggressive — **agents know their context better than the orchestrator**. This release ships an **advisory-only** topology classifier that prints measured-cost notes and proceeds either way. The agent (caller) decides.
70
+
71
+ Full rationale: `.crew/research/pi-crew-effectiveness-evaluation.md` §4.4 + the run-history evidence showing `fast-fix` (3-step sequential DAG) measured **5.7× slower and 1.9× costlier** than 3 raw `Agent` calls (`team_20260629092440_6b8538e31ba7616a`).
72
+
73
+ ### Added — workflow topology advisory
74
+
75
+ - **New `src/workflows/topology-analyzer.ts`** — pure classifier that parses a `WorkflowConfig`, builds the DAG (Kahn-style longest-path depth), detects `parallelGroup:` declarations, and returns `{topology, stepCount, parallelGroupCount, fanOutDegree, dagDepth, recommendation, reason}`. Topologies: `single` (1 step, no concurrency), `sequential` (linear chain, no parallelGroup), `concurrent` (≥3 truly parallel agents via `parallelGroup`), `complex-dag` (4+ steps with branching — `≥2 deps` on ≥1 node), `dynamic` (`.dwf.ts` script). Honors explicit `topology:` frontmatter override when present.
76
+
77
+ - **New `src/workflows/preflight-validator.ts`** — applies the topology rule from `.crew/knowledge.md` "pi-crew USAGE THRESHOLD RULE" (CONVENTIONS section). Returns `{level: "info" | "note" | "warn", message, suggestion, topology, stepCount, recommendation}`. **Never blocks.** Three severity levels:
78
+ - `info` — context-only (dynamic workflow, `force:true` acknowledged)
79
+ - `note` — validated use case (concurrent / complex-dag); "✅ proceeding"
80
+ - `warn` — potential inefficiency (single / sequential 2-3 / 4+); measured cost evidence + "Proceeding anyway"
81
+
82
+ - **Top-level integration** (`src/extension/team-tool/run.ts`, called BEFORE `executeTeamRun`):
83
+ - Extension-layer guard logs the advisory via `console.warn` with icon (`⚠️ ` / `✅ ` / `ℹ️ `) and continues the run.
84
+ - Defense-in-depth in `src/runtime/team-runner.ts:447-468` also logs (catches direct API callers — CLI, tests, scheduler — that bypass the extension layer).
85
+ - Both layers **never throw / never short-circuit**. The agent is always in charge.
86
+
87
+ ### Changed — agent awareness surfaces
88
+
89
+ - **`team` tool description** (`src/extension/registration/team-tool.ts`) now includes an explicit "ℹ️ ADVISORY NOTE (preflight, never blocks)" paragraph + 4 case-by-case notes. Agents reading the tool definition see the rule BEFORE deciding to call.
90
+ - **`team` prompt snippet** shortened and re-toned: "Use the team tool for multi-agent orchestration when you need ≥3 concurrent agents or a complex DAG. For single tasks or 2–3 sequential steps, the raw Agent tool is usually faster. **pi-crew notes the topology (informational only) but proceeds either way — you decide.**"
91
+ - **Workflow YAML frontmatter** — 8 builtin workflows now declare `topology:` (`single` / `sequential` / `concurrent` / `complex-dag` / `dynamic`) so the analyzer honors explicit classification when present and falls back to auto-detection when absent. New `WorkflowConfig.topology?` field in `src/workflows/workflow-config.ts`. `src/workflows/discover-workflows.ts:parseWorkflowFile` parses the field; invalid values are silently dropped (fall-through to auto).
92
+
93
+ ### Tests
94
+
95
+ - **`test/unit/topology-analyzer.test.ts`** — 13 cases: each topology + `parallelGroupsFromSteps`, `fanOutDegreeFromSteps`, `dagDepthFromSteps` helpers + edge cases (empty deps, unknown deps, defensive cycle handling).
96
+ - **`test/unit/preflight-validator.test.ts`** — 11 cases: each topology × level combo, force-bypass acknowledgement, all-3-severities-reachable check, "validator never throws" contract.
97
+ - `test/unit/direct-agent-run.test.ts` + `test/unit/team-runner-merge.test.ts` — re-verified to ensure defense-in-depth guard does not break synthetic-1-step direct-agent runs (skip condition: `workflow.filePath === "<generated>"`).
98
+
99
+ ### Verification
100
+
101
+ - `tsc --noEmit` → EXIT 0 (no type errors)
102
+ - `test:unit` → 5800+ tests, 0 fail attributable to this change (1 pre-existing env-related fail on `resolveNpmGlobalRoot` is unrelated and pre-dates this release)
103
+ - Live smoke test (`/tmp/preflight-smoke.ts`): all 6 topology cases emit the expected advisory level — `single` → WARN, `sequential 2/3/4` → WARN with measured-cost evidence, `concurrent` → NOTE, `force:true` → INFO
104
+ - Live integration test (`team_20260629152312_ca139c8540e938a1`): ran a 3-step sequential `fast-fix` workflow; advisory note appeared in console, workflow completed normally (no block), agent independently verified the rule via source + smoke test + unit tests
105
+
106
+ ### Honesty discipline
107
+
108
+ - **No BLOCK** — the original v0.9.15 design had `level: "block"` that hard-rejected single-task runs; user feedback refined this to advisory-only after observing that the orchestrator doesn't know the agent's full context (audit needs, team coordination reasons, etc.). The agent is in charge.
109
+ - **`force:true`** parameter still works (acknowledged as `info` level) but is no longer required — runs always proceed.
110
+
111
+ ### Files touched
112
+
113
+ | File | Change |
114
+ |---|---|
115
+ | `src/workflows/topology-analyzer.ts` | NEW (182 LOC) |
116
+ | `src/workflows/preflight-validator.ts` | NEW (177 LOC) |
117
+ | `src/workflows/workflow-config.ts` | +8 LOC (topology? field) |
118
+ | `src/workflows/discover-workflows.ts` | +12 LOC (parse frontmatter.topology + parseTopology helper) |
119
+ | `src/extension/team-tool/run.ts` | +18 LOC (extension-layer advisory log) |
120
+ | `src/runtime/team-runner.ts` | +18 LOC (defense-in-depth advisory log) |
121
+ | `src/extension/registration/team-tool.ts` | ~30 LOC (tool description + prompt snippet re-toned) |
122
+ | `src/config/types.ts` | +5 LOC (CrewReliabilityConfig.forcePreflight? field — reserved, currently unused) |
123
+ | `workflows/*.workflow.md` (8 files) | +1 line each (topology: frontmatter) |
124
+ | `test/unit/topology-analyzer.test.ts` | NEW (181 LOC, 13 cases) |
125
+ | `test/unit/preflight-validator.test.ts` | NEW (163 LOC, 11 cases) |
126
+ | `README.md` | +75 LOC ("Workflow topology advisory" section) |
127
+ | `CHANGELOG.md` | this entry |
128
+
3
129
  ## [v0.9.14] — reliability & UX fixes from the v0.9.13 performance/quality assessment (2026-06-29)
4
130
 
5
131
  A parallel-research assessment of v0.9.13 measured three operational gaps and produced ten prioritized recommendations (effort × impact). This release ships **8 of 10 fixes + 2 UX bug fixes from bug reports**, with the remaining 2 honestly deferred (need product input). The unifying theme: **stop the silent lies** — retry that was built but never enabled, a "completed" status that hid zero work, and UI that read failed runs as still-running.
package/README.md CHANGED
@@ -43,6 +43,7 @@ repo: https://github.com/baphuongna/pi-crew
43
43
 
44
44
  ## Features
45
45
 
46
+ - **Workflow topology advisory** (v0.9.15) — before each run, pi-crew classifies the workflow's shape (`single` / `sequential` / `concurrent` / `complex-dag`) and prints an **advisory note** with measured cost evidence (e.g. "3-step sequential: measured 5.7× slower than 3 raw Agent calls — proceeding anyway"). Never blocks — the agent decides. Tool description and prompt-snippet carry the same guidance up-front, so agents know the trade-off before calling. New files: `src/workflows/topology-analyzer.ts`, `src/workflows/preflight-validator.ts`. See [Workflow topology advisory](#workflow-topology-advisory) below.
46
47
  - **One Pi tool** — `team` handles routing, planning, execution, review, and cleanup
47
48
  - **Autonomous delegation** — policy injection decides when/how to delegate based on task complexity
48
49
  - **needs_attention status** — tasks that complete without calling `submit_result` get `needs_attention` (terminal) instead of `completed`; allows retry/re-run without blocking downstream phases
@@ -213,6 +214,71 @@ When unsure which team/workflow fits:
213
214
  | `research` | explore → analyze → write | Research and documentation |
214
215
  | `parallel-research` | Parallel shards → synthesize → write | Multi-source research |
215
216
 
217
+ ---
218
+
219
+ ## Workflow topology advisory
220
+
221
+ Before every `team action='run'`, pi-crew classifies the workflow shape and prints an informational note. **It never blocks** — agents decide whether to proceed, refactor, or override.
222
+
223
+ ### How it works
224
+
225
+ ```text
226
+ team action='run', workflow='fast-fix', goal='...'
227
+
228
+ pi-crew analyzes topology: 3-step sequential
229
+
230
+ ⚠️ [team-tool.preflight] WARN: 3-step sequential chain: measured 5.7× slower
231
+ and 1.9× costlier than 3 raw Agent calls (Run #3 in .crew/state/runs/).
232
+ Proceeding anyway.
233
+
234
+ Workflow runs to completion. Agent sees the note, decides for next time.
235
+ ```
236
+
237
+ ### Topology → advisory level
238
+
239
+ | Topology | When | Level | What pi-crew prints |
240
+ |---|---|---|---|
241
+ | `single` | 1 step, no concurrency | `warn` | "raw Agent tool would be ~30× faster and ~5× cheaper. Proceeding anyway." |
242
+ | `sequential` (2-3 steps) | Linear chain, no fan-out | `warn` | "measured 5.7× slower than raw Agent calls. Proceeding anyway." |
243
+ | `sequential` (4+ steps) | Linear chain, longer | `warn` | "audit trail may justify pi-crew overhead. Proceeding anyway." |
244
+ | `concurrent` | ≥3 truly parallel agents (parallelGroup) | `note` | "✅ Validated use case: N-way parallel fan-out. pi-crew's parallelism wins." |
245
+ | `complex-dag` | 4+ steps with data dependencies | `note` | "✅ Validated use case: complex DAG with adaptive plan." |
246
+ | `dynamic` | `.dwf.ts` script | `info` | "Runtime decides topology." |
247
+
248
+ ### When to prefer raw `Agent` over `team`
249
+
250
+ Use the raw `Agent` tool when:
251
+ - You have a single task or quick question (1-step)
252
+ - You have 2–3 sequential independent steps (no DAG branching, no concurrency)
253
+
254
+ Use `team` when:
255
+ - You have ≥3 agents running TRULY CONCURRENTLY (`parallelGroup`)
256
+ - You have a COMPLEX DAG (4+ steps with data dependencies, branching)
257
+ - You need an audit trail, team coordination, or worktree isolation that justifies pi-crew's overhead
258
+
259
+ ### How agents learn the rule
260
+
261
+ The guidance is available in three places agents see:
262
+
263
+ 1. **`team` tool description** — the LLM reads this when considering whether to call the tool. Includes an explicit "ℹ️ ADVISORY NOTE (preflight, never blocks)" section.
264
+ 2. **`team` prompt snippet** — rendered in agent context when the tool is relevant. Single-line summary of the rule.
265
+ 3. **`.crew/knowledge.md` CONVENTIONS section** — always injected into every worker session's context. Contains the full 4-question self-check.
266
+
267
+ ### How to silence the advisory
268
+
269
+ The advisory is **informational only** — there is no `force:true` flag needed (the run proceeds regardless). If you want to silence the `console.warn` output for cleaner logs, set `PI_CREW_QUIET_PREFLIGHT=1` in your environment.
270
+
271
+ ### Implementation
272
+
273
+ - `src/workflows/topology-analyzer.ts` — pure classifier (parses workflow YAML, builds DAG, detects parallelGroups)
274
+ - `src/workflows/preflight-validator.ts` — returns `{level: info|note|warn, message, suggestion}` (never throws)
275
+ - Integration: `src/extension/team-tool/run.ts` (extension layer, prints advisory) + `src/runtime/team-runner.ts` (defense-in-depth, also logs)
276
+
277
+ ### Tests
278
+
279
+ - `test/unit/topology-analyzer.test.ts` — 13 cases (each topology + edge cases)
280
+ - `test/unit/preflight-validator.test.ts` — 11 cases (each level + advisory contract)
281
+
216
282
  ## Builtin Agents
217
283
 
218
284
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -188,6 +188,11 @@ export interface CrewReliabilityConfig {
188
188
  autoRepairIntervalMs?: number;
189
189
  /** Remove /tmp/pi-crew-* directories after their orphaned runs are reconciled. Default: true. */
190
190
  cleanupOrphanedTempDirs?: boolean;
191
+ /** Bypass the preflight topology validator (workflow threshold rule, .crew/knowledge.md
192
+ * 'pi-crew USAGE THRESHOLD RULE'). When true, runs that would be BLOCKED or WARNED
193
+ * proceed without intervention. Audit-trail the override via events.jsonl.
194
+ * Default: false (enforce). Use only for legitimate audit/debug sessions. */
195
+ forcePreflight?: boolean;
191
196
  /** Inject a compact ambient crew-status note into the agent's context on every LLM call while crew runs are in-flight, so the agent stays continuously aware of active runs without calling the `team` tool. No-op when no runs are active. Default: true. */
192
197
  ambientStatusInjection?: boolean;
193
198
  /**
@@ -1,20 +1,31 @@
1
1
  import { statSync } from "node:fs";
2
- import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionContext,
5
+ ToolDefinition,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import { Text } from "@earendil-works/pi-tui";
3
8
  import { loadConfig } from "../../config/config.ts";
4
- import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
- import type { CrewWidgetState } from "../../ui/widget/index.ts";
6
- import { updateCrewWidget } from "../../ui/widget/index.ts";
7
- import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
9
+ import type { MetricRegistry } from "../../observability/metric-registry.ts";
8
10
  import type { createManifestCache } from "../../runtime/manifest-cache.ts";
11
+ import {
12
+ TeamToolParams,
13
+ type TeamToolParamsValue,
14
+ } from "../../schema/team-tool-schema.ts";
15
+ import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
9
16
  import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
10
- import type { MetricRegistry } from "../../observability/metric-registry.ts";
17
+ import { statusIcon, teamToolRenderer } from "../../ui/tool-renderers/index.ts";
18
+ import type { CrewWidgetState } from "../../ui/widget/index.ts";
19
+ import { updateCrewWidget } from "../../ui/widget/index.ts";
11
20
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
12
- import { Text } from "@earendil-works/pi-tui";
13
- import { teamToolRenderer, statusIcon } from "../../ui/tool-renderers/index.ts";
14
21
  // Team tool handler — lazy-loaded because team-tool.ts imports many modules
15
22
  import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
23
+
16
24
  let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
17
- async function handleTeamTool(params: Parameters<typeof HandleTeamToolFn>[0], ctx: Parameters<typeof HandleTeamToolFn>[1]): Promise<ReturnType<typeof HandleTeamToolFn>> {
25
+ async function handleTeamTool(
26
+ params: Parameters<typeof HandleTeamToolFn>[0],
27
+ ctx: Parameters<typeof HandleTeamToolFn>[1],
28
+ ): Promise<ReturnType<typeof HandleTeamToolFn>> {
18
29
  if (!_cachedHandleTeamTool) {
19
30
  // LAZY: team-tool.ts imports many modules — defer until first use.
20
31
  const mod = await import("../team-tool.ts");
@@ -22,12 +33,13 @@ async function handleTeamTool(params: Parameters<typeof HandleTeamToolFn>[0], ct
22
33
  }
23
34
  return _cachedHandleTeamTool(params, ctx);
24
35
  }
25
- import { withSessionId } from "../team-tool/context.ts";
26
- import { toolResult } from "../tool-result.ts";
27
- import { loadRunManifestById } from "../../state/state-store.ts";
36
+
28
37
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
38
+ import { loadRunManifestById } from "../../state/state-store.ts";
29
39
  import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
30
40
  import { logInternalError } from "../../utils/internal-error.ts";
41
+ import { withSessionId } from "../team-tool/context.ts";
42
+ import { toolResult } from "../tool-result.ts";
31
43
 
32
44
  const TEAM_TOOL_PROGRESS_TICK_MS = 1000;
33
45
 
@@ -35,22 +47,35 @@ type OnUpdate = (chunk: { content: { type: "text"; text: string }[] }) => void;
35
47
 
36
48
  export interface RegisterTeamToolDeps {
37
49
  foregroundControllers: Map<string | symbol, AbortController>;
38
- startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
50
+ startForegroundRun: (
51
+ ctx: ExtensionContext,
52
+ runner: (signal?: AbortSignal) => Promise<void>,
53
+ runId?: string,
54
+ ) => void;
39
55
  abortForegroundRun: (runId: string) => boolean;
40
56
  openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
41
57
  getManifestCache: (cwd: string) => ReturnType<typeof createManifestCache>;
42
- getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
58
+ getRunSnapshotCache?: (
59
+ cwd: string,
60
+ ) => ReturnType<typeof createRunSnapshotCache>;
43
61
  getMetricRegistry?: () => MetricRegistry | undefined;
44
62
  widgetState: CrewWidgetState;
45
63
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
46
64
  }
47
65
 
48
- export function resolveCwdOverride(baseCwd: string, override: string | undefined): { ok: true; cwd: string } | { ok: false; error: string } {
66
+ export function resolveCwdOverride(
67
+ baseCwd: string,
68
+ override: string | undefined,
69
+ ): { ok: true; cwd: string } | { ok: false; error: string } {
49
70
  if (!override) return { ok: true, cwd: baseCwd };
50
71
  try {
51
72
  const resolved = resolveRealContainedPath(baseCwd, override);
52
73
  const stat = statSync(resolved);
53
- if (!stat.isDirectory()) return { ok: false, error: `cwd override is not a directory: ${resolved}` };
74
+ if (!stat.isDirectory())
75
+ return {
76
+ ok: false,
77
+ error: `cwd override is not a directory: ${resolved}`,
78
+ };
54
79
  return { ok: true, cwd: resolved };
55
80
  } catch (error) {
56
81
  const message = error instanceof Error ? error.message : String(error);
@@ -58,12 +83,28 @@ export function resolveCwdOverride(baseCwd: string, override: string | undefined
58
83
  }
59
84
  }
60
85
 
61
- export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
86
+ export function registerTeamTool(
87
+ pi: ExtensionAPI,
88
+ deps: RegisterTeamToolDeps,
89
+ ): void {
62
90
  const tool: ToolDefinition = {
63
91
  name: "team",
64
92
  label: "Team",
65
- description: "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
66
- promptSnippet: "Use the team tool proactively for coordinated multi-agent work. If unsure, call { action: 'recommend', goal } first, then run or plan with the suggested team/workflow.",
93
+ description: [
94
+ "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
95
+ "",
96
+ "ℹ️ ADVISORY NOTE (preflight, never blocks): pi-crew prints informational notes about workflow topology. Review the note, then decide. There is no BLOCK — agents always exercise judgment.",
97
+ "- Single-task run: raw `Agent` tool would be ~30× faster and ~5× cheaper. pi-crew notes this and proceeds anyway.",
98
+ "- 2–3 step sequential run: measured 5.7× slower than raw `Agent` calls in Run #3. pi-crew notes this and proceeds anyway.",
99
+ "- ≥3 concurrent or complex DAG: validated good use case, pi-crew notes 'validated use case'.",
100
+ "If unsure, call { action: 'recommend', goal } first to get a team/workflow suggestion.",
101
+ ].join("\n"),
102
+ promptSnippet: [
103
+ "Use the team tool for multi-agent orchestration when you need ≥3 concurrent agents or a complex DAG.",
104
+ "For single tasks or 2–3 sequential steps, the raw Agent tool is usually faster (raw calls = ~5× faster for 3-step chains).",
105
+ "pi-crew notes the topology (informational only) but proceeds either way — you decide based on your context (audit trail, team coordination, etc.).",
106
+ "If unsure, call { action: 'recommend', goal } first.",
107
+ ].join("\n"),
67
108
  parameters: TeamToolParams as never,
68
109
  async execute(_id, params, signal, onUpdate, ctx) {
69
110
  const controller = new AbortController();
@@ -71,19 +112,50 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
71
112
  deps.foregroundControllers.set(toolKey, controller);
72
113
  const abort = (): void => controller.abort();
73
114
  signal?.addEventListener("abort", abort, { once: true });
74
- const stopProgress = startTeamToolProgressBinder(onUpdate as OnUpdate | undefined);
115
+ const stopProgress = startTeamToolProgressBinder(
116
+ onUpdate as OnUpdate | undefined,
117
+ );
75
118
  try {
76
119
  const resolved = params as TeamToolParamsValue;
77
120
  const cwdOverride = resolveCwdOverride(ctx.cwd, resolved.cwd);
78
- if (!cwdOverride.ok) return toolResult(cwdOverride.error, { action: resolved.action ?? "list", status: "error" }, true);
121
+ if (!cwdOverride.ok)
122
+ return toolResult(
123
+ cwdOverride.error,
124
+ { action: resolved.action ?? "list", status: "error" },
125
+ true,
126
+ );
79
127
  const toolCtx = withSessionId({ ...ctx, cwd: cwdOverride.cwd });
80
128
  // Phase 1.5: Auto-set session name from team run context
81
- if (resolved.action === "run" && resolved.goal && !pi.getSessionName()) {
82
- const runLabel = resolved.team ?? resolved.agent ?? "direct";
83
- pi.setSessionName(`pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`);
129
+ if (
130
+ resolved.action === "run" &&
131
+ resolved.goal &&
132
+ !pi.getSessionName()
133
+ ) {
134
+ const runLabel =
135
+ resolved.team ?? resolved.agent ?? "direct";
136
+ pi.setSessionName(
137
+ `pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`,
138
+ );
84
139
  }
85
- const output = await handleTeamTool(resolved, { ...toolCtx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(toolCtx, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: (runId) => { stopProgress.attach(toolCtx.cwd, runId); deps.openLiveSidebar(toolCtx, runId); }, onJsonEvent: deps.onJsonEvent, getRunSnapshotCache: deps.getRunSnapshotCache });
86
- if (resolved.action === "run" && !output.isError && typeof output.details?.runId === "string") {
140
+ const output = await handleTeamTool(resolved, {
141
+ ...toolCtx,
142
+ signal: controller.signal,
143
+ metricRegistry: deps.getMetricRegistry?.(),
144
+ startForegroundRun: (runner, runId) =>
145
+ deps.startForegroundRun(toolCtx, runner, runId),
146
+ abortForegroundRun: deps.abortForegroundRun,
147
+ onRunStarted: (runId) => {
148
+ stopProgress.attach(toolCtx.cwd, runId);
149
+ deps.openLiveSidebar(toolCtx, runId);
150
+ },
151
+ onJsonEvent: deps.onJsonEvent,
152
+ getRunSnapshotCache: deps.getRunSnapshotCache,
153
+ });
154
+ if (
155
+ resolved.action === "run" &&
156
+ !output.isError &&
157
+ typeof output.details?.runId === "string"
158
+ ) {
87
159
  pi.appendEntry("crew:run-started", {
88
160
  runId: output.details.runId,
89
161
  team: resolved.team,
@@ -97,8 +169,21 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
97
169
  const config = loadConfig(toolCtx.cwd).config.ui;
98
170
  const cache = deps.getManifestCache(toolCtx.cwd);
99
171
  const snapshotCache = deps.getRunSnapshotCache?.(toolCtx.cwd);
100
- updateCrewWidget(toolCtx, deps.widgetState, config, cache, snapshotCache);
101
- updatePiCrewPowerbar(pi.events, toolCtx.cwd, config, cache, snapshotCache, toolCtx);
172
+ updateCrewWidget(
173
+ toolCtx,
174
+ deps.widgetState,
175
+ config,
176
+ cache,
177
+ snapshotCache,
178
+ );
179
+ updatePiCrewPowerbar(
180
+ pi.events,
181
+ toolCtx.cwd,
182
+ config,
183
+ cache,
184
+ snapshotCache,
185
+ toolCtx,
186
+ );
102
187
  return output;
103
188
  } finally {
104
189
  signal?.removeEventListener("abort", abort);
@@ -113,7 +198,12 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
113
198
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
199
  renderResult(result: any, options: any, theme: any, context: any): any {
115
200
  try {
116
- return teamToolRenderer.renderResult(result, options, theme, context);
201
+ return teamToolRenderer.renderResult(
202
+ result,
203
+ options,
204
+ theme,
205
+ context,
206
+ );
117
207
  } catch {
118
208
  return new Text(statusIcon("completed", theme) + " done", 0, 0);
119
209
  }
@@ -127,7 +217,9 @@ interface TeamToolProgressBinder {
127
217
  stop: () => void;
128
218
  }
129
219
 
130
- function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolProgressBinder {
220
+ function startTeamToolProgressBinder(
221
+ onUpdate: OnUpdate | undefined,
222
+ ): TeamToolProgressBinder {
131
223
  if (!onUpdate) {
132
224
  return { attach: () => {}, stop: () => {} };
133
225
  }
@@ -137,20 +229,30 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
137
229
  const tick = (): void => {
138
230
  try {
139
231
  if (!cwd || !runId) {
140
- const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
232
+ const elapsed = Math.max(
233
+ 0,
234
+ Math.round((Date.now() - startedAt) / 1000),
235
+ );
141
236
  const msg = `team status=starting elapsed=${elapsed}s`;
142
237
  onUpdate({ content: [{ type: "text", text: msg }] });
143
238
  return;
144
239
  }
145
240
  const loaded = loadRunManifestById(cwd, runId);
146
241
  if (!loaded) {
147
- const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
242
+ const elapsed = Math.max(
243
+ 0,
244
+ Math.round((Date.now() - startedAt) / 1000),
245
+ );
148
246
  const msg = `team run=${runId} elapsed=${elapsed}s (manifest pending)`;
149
247
  onUpdate({ content: [{ type: "text", text: msg }] });
150
248
  return;
151
249
  }
152
250
  let agents;
153
- try { agents = readCrewAgents(loaded.manifest); } catch { /* ignore */ }
251
+ try {
252
+ agents = readCrewAgents(loaded.manifest);
253
+ } catch {
254
+ /* ignore */
255
+ }
154
256
  const text = formatCompactToolProgress({
155
257
  agentId: runId,
156
258
  status: loaded.manifest.status,
@@ -162,14 +264,22 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
162
264
  });
163
265
  onUpdate({ content: [{ type: "text", text }] });
164
266
  } catch (error) {
165
- logInternalError("team-tool.progress", error, `runId=${runId ?? ""}`);
267
+ logInternalError(
268
+ "team-tool.progress",
269
+ error,
270
+ `runId=${runId ?? ""}`,
271
+ );
166
272
  }
167
273
  };
168
274
  tick();
169
275
  const timer = setInterval(tick, TEAM_TOOL_PROGRESS_TICK_MS);
170
276
  if (typeof timer.unref === "function") timer.unref();
171
277
  return {
172
- attach: (boundCwd: string, boundRunId: string) => { cwd = boundCwd; runId = boundRunId; tick(); },
278
+ attach: (boundCwd: string, boundRunId: string) => {
279
+ cwd = boundCwd;
280
+ runId = boundRunId;
281
+ tick();
282
+ },
173
283
  stop: () => clearInterval(timer),
174
284
  };
175
285
  }