pi-messenger 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +81 -6
  3. package/config-overlay.ts +11 -12
  4. package/config.ts +11 -5
  5. package/crew/agents/crew-worker.md +3 -3
  6. package/crew/agents.ts +39 -22
  7. package/crew/handlers/coordination.ts +72 -15
  8. package/crew/handlers/plan.ts +104 -38
  9. package/crew/handlers/review.ts +20 -11
  10. package/crew/handlers/revise.ts +15 -13
  11. package/crew/handlers/status.ts +10 -10
  12. package/crew/handlers/sync.ts +11 -10
  13. package/crew/handlers/task.ts +173 -26
  14. package/crew/handlers/team.ts +235 -0
  15. package/crew/handlers/work.ts +71 -29
  16. package/crew/index.ts +44 -29
  17. package/crew/live-progress.ts +1 -1
  18. package/crew/lobby.ts +18 -15
  19. package/crew/prompt.ts +115 -19
  20. package/crew/registry.ts +1 -1
  21. package/crew/spawn.ts +21 -16
  22. package/crew/state-autonomous.ts +1 -1
  23. package/crew/state-planning.ts +1 -1
  24. package/crew/state.ts +2 -2
  25. package/crew/store.ts +39 -10
  26. package/crew/task-actions.ts +13 -5
  27. package/crew/team/store.ts +479 -0
  28. package/crew/team/subagent-roles.ts +87 -0
  29. package/crew/team/types.ts +50 -0
  30. package/crew/types.ts +25 -5
  31. package/crew/utils/artifacts.ts +11 -1
  32. package/crew/utils/config.ts +1 -1
  33. package/crew/utils/discover.ts +1 -1
  34. package/crew/utils/progress.ts +73 -6
  35. package/crew/utils/risk-labels.ts +7 -0
  36. package/crew/utils/task-format.ts +14 -0
  37. package/crew/utils/team-roles.ts +28 -0
  38. package/feed.ts +52 -11
  39. package/handlers.ts +34 -30
  40. package/index.ts +183 -114
  41. package/install.mjs +30 -2
  42. package/overlay-actions.ts +12 -12
  43. package/overlay-coordinator.ts +1 -1
  44. package/overlay-render.ts +54 -30
  45. package/overlay.ts +76 -43
  46. package/package.json +14 -3
  47. package/skills/pi-messenger-crew/SKILL.md +38 -4
  48. package/store.ts +4 -4
package/CHANGELOG.md CHANGED
@@ -1,6 +1,36 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.15.0] - 2026-07-30
4
+
5
+ ### Added
6
+ - Added an optional Team layer with reusable profiles, project charter/memory, role-aware tasks, approval gates, worker context, and overlay signals.
7
+ - Added `team.setup` for first-run Team setup with profile activation, starter charter creation, and next-step guidance.
8
+ - Surfaced rejected approval-gated tasks separately with `task.revise` / `task.revise-tree` guidance.
9
+
10
+ ### Fixed
11
+ - Migrated extension tool schemas from `@sinclair/typebox` to `typebox` 1.x and updated the local test/tsconfig references to the new package entry.
12
+ - Guarded status heartbeat updates against stale pi session contexts after reloads or session replacement while preserving non-stale errors, fixing #26, #25, and #18. Thanks to Kire Howard (`MatrixJockey`), `MartinMayday`, and Hydro (`HydroToxin`) for the reports, and to Riwut Libinuko (`cakriwut`) for #19.
13
+ - Excluded the orchestrator-only `pi-messenger-crew` skill from worker prompts to avoid irrelevant skill loading during Crew task execution. Thanks to Varun Maliwal (`vmaliwal`) for #15.
14
+ - Fixed auto-opened Crew overlays after planning to use the live session cwd.
15
+ - Restored `.js` extension path handling in Crew worker and lobby tool configuration.
16
+ - Made enum schemas load under typebox compatibility shims that do not expose `Type.Unsafe`, fixing #23. Thanks to David Moshal (`davidmoshal`) for #24.
17
+ - Enforced `crew.concurrency.max` in overlay-driven worker spawning paths. Thanks to Martin Hátaš (`MartinHatas`) for #17.
18
+ - Prevented the legacy `npx pi-messenger` installer from creating a duplicate extension copy when the native `pi install npm:pi-messenger` package flow is already configured, fixing #22. Thanks to David Moshal (`davidmoshal`) for the report.
19
+ - Crew planner, worker, reviewer, analyst, lobby, and revision agents now fall back to the host session model when no task, request, role, or config model override is set, fixing #20. Thanks to Velinus (`velinussage`) for the report.
20
+ - Crew subprocess launches use `pi.cmd` on Windows and task reset paths now refuse active workers before making tasks startable again, addressing #11. Thanks to Logan Laughlin (`llaughlin`) for the report.
21
+ - Compacted streaming `message_update` artifacts and stopped retaining every parsed event in memory, fixing the transcript amplification in #27. Thanks to Hugo Ruíz (`hugotown`) and `aeturnal` for the measurements, and to `suse-coder` (#13) and Tom (`monotykamary`) (#4) for the earlier reports of the resulting stalls and jank.
22
+ - Crew subprocesses now fail fast on terminal provider usage/quota/auth 4xx JSON events instead of waiting through retry loops, fixing #21. Thanks to Velinus (`velinussage`) for the report.
23
+
24
+ ### Changed
25
+ - `readFeedEvents` now reads only a bounded tail of `feed.jsonl` in backward 64KB chunks instead of parsing the whole file, making overlay feed reads on large histories dramatically faster (~1,800x on a 100k-event feed). Non-positive limits now return no events.
26
+ - Overlay rendering, auto-spawn/refill checks, and stuck-agent heartbeats now reuse a single Crew task snapshot per frame instead of rescanning the task directory per section, roughly quadrupling render-helper throughput on large task lists.
27
+ - Aligned Team built-in roles and sample profiles with the packaged `pi-subagents` role vocabulary while keeping Crew as the execution engine.
28
+ - Moved pi core packages and `typebox` to peer dependencies for packaged installs.
29
+
30
+ ## [0.14.1] - 2026-04-04
31
+
32
+ ### Changed
33
+ - Added a `promptSnippet` for `pi_messenger` so Pi 0.59+ includes it in the default tool prompt section and reliably surfaces Crew/coordination workflows.
4
34
 
5
35
  ## [0.14.0] - 2026-04-03
6
36
 
package/README.md CHANGED
@@ -154,6 +154,59 @@ Error responses use `{ error: { code, message, details? } }` shape.
154
154
 
155
155
  Any skills you already have in `~/.pi/agent/skills/` are automatically available to crew workers — no setup needed.
156
156
 
157
+ ### Team Layer
158
+
159
+ Team is an optional layer around Crew. Crew still plans and executes tasks; Team adds project-local roles, a charter, durable memory, reusable JSON profiles, and high-risk approval gates. Active Team state lives in `.pi/messenger/team/`. Reusable profiles live in `~/.pi/agent/messenger/team-profiles/`.
160
+
161
+ Most users should talk to their agent in plain language:
162
+
163
+ ```text
164
+ Use the review squad for this cleanup.
165
+ Use a migration team and pause before risky database changes.
166
+ Research this first, then plan the implementation.
167
+ Approve the auth API task.
168
+ Reject the migration task; it needs rollback tests.
169
+ ```
170
+
171
+ The agent maps those requests to Team actions. If a task needs approval, the agent should ask in plain language and continue after you approve. The tool calls are mainly for agents and power users:
172
+
173
+ ```typescript
174
+ pi_messenger({ action: "team.setup", name: "migration-squad" })
175
+ pi_messenger({ action: "team.memory.note", type: "decision", message: "Auth API changes require reviewer sign-off." })
176
+ pi_messenger({ action: "team.roles" })
177
+ pi_messenger({ action: "team.status" })
178
+ ```
179
+
180
+ `team.setup` activates the profile, saves an editable JSON copy if needed, creates a starter charter when the project does not have one, and returns the next planning/status commands.
181
+
182
+ When Team is active, planner task JSON may include `role` and `riskLabels`. Tasks persist those as `role`, `risk_labels`, and `approval`; existing tasks without those fields still work. Workers receive bounded Team role, charter, memory, and approval context. `work` skips tasks that require approval but are not approved and returns pending approvals under `needsApproval`; rejected tasks are surfaced separately with `task.revise` / `task.revise-tree` guidance.
183
+
184
+ Team's built-in role names follow the packaged `pi-subagents` vocabulary where possible: `context-builder`, `delegate`, `oracle`, `planner`, `researcher`, `reviewer`, `scout`, and `worker`. Roles resolve from those built-in defaults, the active profile, and optional filesystem metadata from `pi-subagents` markdown files when present. `pi-messenger` only reads those files; it does not require or call the subagent extension, and Crew still uses its own Crew agents for execution.
185
+
186
+ Built-in sample profiles are available immediately and are saved as editable JSON the first time you activate them:
187
+
188
+ ```typescript
189
+ pi_messenger({ action: "team.setup", name: "migration-squad" }) // migrations with approval gates
190
+ pi_messenger({ action: "team.setup", name: "review-squad" }) // scout/reviewer/worker cleanup flow
191
+ pi_messenger({ action: "team.setup", name: "research-squad" }) // research-first planning flow
192
+ ```
193
+
194
+ A saved profile looks like this:
195
+
196
+ ```json
197
+ {
198
+ "name": "migration-squad",
199
+ "description": "Scout, implement, and review high-risk migrations with lead approval gates",
200
+ "roles": {
201
+ "scout": { "description": "Map affected schemas, APIs, and rollback paths before implementation" },
202
+ "worker": { "description": "Implement the approved migration in small, reversible steps" },
203
+ "reviewer": { "description": "Review migration safety, compatibility, rollback, and tests" }
204
+ },
205
+ "approval": { "mode": "risk-labels", "labels": ["database", "migration", "destructive", "api-contract"] },
206
+ "memory": { "inject": ["decision", "interface", "risk", "handoff"], "maxCharsPerType": 4000 }
207
+ }
208
+ ```
209
+
157
210
  ### Crew Configuration
158
211
 
159
212
  Crew spawns multiple LLM sessions in parallel — it can burn tokens fast. Start with a cheap worker model and scale up once you've seen the workflow. Add this to `~/.pi/agent/pi-messenger.json`:
@@ -162,7 +215,7 @@ Crew spawns multiple LLM sessions in parallel — it can burn tokens fast. Start
162
215
  { "crew": { "models": { "worker": "claude-haiku-4-5" } } }
163
216
  ```
164
217
 
165
- The planner and reviewer keep their frontmatter defaults; only workers (the bulk of the spend) get the cheap model. Override per-role as needed:
218
+ By default, Crew agents inherit the host session model unless a task, request, role, config, or agent frontmatter model says otherwise. Override per-role as needed:
166
219
 
167
220
  ```json
168
221
  {
@@ -216,10 +269,10 @@ Full config reference (all fields optional — only set what you want to change)
216
269
  | `dependencies` | Dependency scheduling mode: `advisory` or `strict` | `"advisory"` |
217
270
  | `coordination` | Worker coordination level: `none`, `minimal`, `moderate`, `chatty` | `"chatty"` |
218
271
  | `messageBudgets` | Max outgoing messages per worker per level (sends rejected after limit) | `{ none: 0, minimal: 2, moderate: 5, chatty: 10 }` |
219
- | `models.planner` | Model for planner agent | `anthropic/claude-opus-4-6` |
220
- | `models.worker` | Model for workers (overridden by per-task or per-wave `model` param) | `anthropic/claude-haiku-4-5` |
221
- | `models.reviewer` | Model for reviewer agent | `anthropic/claude-opus-4-6` |
222
- | `models.analyst` | Model for analyst (plan-sync) agent | `anthropic/claude-haiku-4-5` |
272
+ | `models.planner` | Model for planner agent | host session model, then agent frontmatter |
273
+ | `models.worker` | Model for workers (overridden by per-task or per-wave `model` param) | host session model, then agent frontmatter |
274
+ | `models.reviewer` | Model for reviewer agent | host session model, then agent frontmatter |
275
+ | `models.analyst` | Model for analyst (plan-sync) agent | host session model, then agent frontmatter |
223
276
  | `thinking.planner` | Thinking level for planner agent | (from frontmatter) |
224
277
  | `thinking.worker` | Thinking level for worker agents | (from frontmatter) |
225
278
  | `thinking.reviewer` | Thinking level for reviewer agents | (from frontmatter) |
@@ -231,10 +284,12 @@ Full config reference (all fields optional — only set what you want to change)
231
284
  | `work.maxWaves` | Max autonomous waves | `50` |
232
285
  | `work.shutdownGracePeriodMs` | Grace period before SIGTERM on abort | `30000` |
233
286
  | `work.env` | Environment variables passed to spawned workers | `{}` |
287
+ | `artifacts.enabled` | Write compact Crew debug artifacts | `true` |
288
+ | `artifacts.cleanupDays` | Retention setting for Crew artifacts | `7` |
234
289
 
235
290
  ### Default Agent Models
236
291
 
237
- Each crew agent ships with a default model in its frontmatter. Override any of these via `crew.models.<role>` in config:
292
+ Each crew agent ships with a fallback model in its frontmatter. Override any role via `crew.models.<role>` in config:
238
293
 
239
294
  | Agent | Role | Default Model |
240
295
  |-------|------|---------------|
@@ -275,6 +330,8 @@ Agent definitions live in `crew/agents/` within the extension. To customize one
275
330
  | `task.list` | List all tasks |
276
331
  | `task.show` | Show task details (`id` required) |
277
332
  | `task.start` | Start a task (`id` required) |
333
+ | `task.approve` | Approve an approval-gated task (`id` required) |
334
+ | `task.reject` | Reject an approval-gated task (`id` required, `reason` optional) |
278
335
  | `task.done` | Complete a task (`id` required, `summary` optional) |
279
336
  | `task.block` | Block a task (`id` + `reason` required) |
280
337
  | `task.unblock` | Unblock a task (`id` required) |
@@ -286,6 +343,24 @@ Agent definitions live in `crew/agents/` within the extension. To customize one
286
343
  | `crew.install` | Show discovered crew agents and their sources |
287
344
  | `crew.uninstall` | Remove stale shared-directory crew agent copies |
288
345
 
346
+ ### Team
347
+
348
+ | Action | Description |
349
+ |--------|-------------|
350
+ | `team.setup` | Activate a profile, create a starter charter if missing, and show next steps (`name` optional, defaults to `migration-squad`) |
351
+ | `team.profile.list` | List built-in samples and saved reusable JSON team profiles |
352
+ | `team.profile.use` | Activate a profile (`name` required; saves a sample/default profile if missing) |
353
+ | `team.profile.save` | Save the active profile under `name` |
354
+ | `team.charter.show` | Show the project team charter |
355
+ | `team.charter.create` | Create or replace the charter (`name` + `message` required) |
356
+ | `team.charter.update` | Append a charter update (`message` required) |
357
+ | `team.memory.note` | Append team memory (`type`: `decision`, `interface`, `risk`, or `handoff`; `message` required) |
358
+ | `team.memory.list` | List team memory (`type` and `limit` optional) |
359
+ | `team.roles` | Resolve Team roles from packaged-vocabulary defaults, profile config, and optional subagent metadata |
360
+ | `team.status` | Summarize team/profile/charter, roles, memory counts, and needs-lead tasks |
361
+
362
+ Approval-gated tasks use the Crew task commands `task.approve` and `task.reject`. Rejected tasks stay blocked from work and are surfaced with `task.revise` / `task.revise-tree` next steps.
363
+
289
364
  ### Swarm (Spec-Based)
290
365
 
291
366
  | Action | Description |
package/config-overlay.ts CHANGED
@@ -2,10 +2,10 @@
2
2
  * Pi Messenger - Config Overlay Component
3
3
  */
4
4
 
5
- import type { Component, Focusable, TUI } from "@mariozechner/pi-tui";
6
- import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
7
- import type { Theme } from "@mariozechner/pi-coding-agent";
8
- import { getAutoRegisterPaths, saveAutoRegisterPaths, matchesAutoRegisterPath } from "./config.js";
5
+ import type { Component, Focusable, TUI } from "@earendil-works/pi-tui";
6
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
7
+ import type { Theme } from "@earendil-works/pi-coding-agent";
8
+ import { getAutoRegisterPaths, saveAutoRegisterPaths, matchesAutoRegisterPath } from "./config.ts";
9
9
 
10
10
  export class MessengerConfigOverlay implements Component, Focusable {
11
11
  readonly width = 60;
@@ -19,7 +19,8 @@ export class MessengerConfigOverlay implements Component, Focusable {
19
19
  constructor(
20
20
  private tui: TUI,
21
21
  private theme: Theme,
22
- private done: () => void
22
+ private done: () => void,
23
+ private cwd: string,
23
24
  ) {
24
25
  this.paths = getAutoRegisterPaths();
25
26
  }
@@ -63,12 +64,11 @@ export class MessengerConfigOverlay implements Component, Focusable {
63
64
  }
64
65
 
65
66
  private addCurrentPath(): void {
66
- const cwd = process.cwd();
67
- if (this.paths.includes(cwd)) {
67
+ if (this.paths.includes(this.cwd)) {
68
68
  this.statusMessage = "Already in list";
69
69
  return;
70
70
  }
71
- this.paths.push(cwd);
71
+ this.paths.push(this.cwd);
72
72
  this.selectedIndex = this.paths.length - 1;
73
73
  this.dirty = true;
74
74
  this.statusMessage = "Added current folder";
@@ -88,8 +88,7 @@ export class MessengerConfigOverlay implements Component, Focusable {
88
88
  const w = this.width;
89
89
  const innerW = w - 2;
90
90
  const lines: string[] = [];
91
- const cwd = process.cwd();
92
- const isCurrentInList = matchesAutoRegisterPath(cwd, this.paths);
91
+ const isCurrentInList = matchesAutoRegisterPath(this.cwd, this.paths);
93
92
 
94
93
  const border = (s: string) => this.theme.fg("dim", s);
95
94
  const pad = (s: string, len: number) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
@@ -106,7 +105,7 @@ export class MessengerConfigOverlay implements Component, Focusable {
106
105
  lines.push(emptyRow());
107
106
 
108
107
  // Current folder status
109
- const cwdDisplay = truncateToWidth(cwd, Math.max(10, innerW - 20));
108
+ const cwdDisplay = truncateToWidth(this.cwd, Math.max(10, innerW - 20));
110
109
  lines.push(row(`Current folder: ${cwdDisplay}`));
111
110
  const statusColor = isCurrentInList ? "accent" : "dim";
112
111
  lines.push(row(`Auto-register: ${this.theme.fg(statusColor, isCurrentInList ? "YES" : "NO")}`));
@@ -126,7 +125,7 @@ export class MessengerConfigOverlay implements Component, Focusable {
126
125
  for (let i = 0; i < this.paths.length; i++) {
127
126
  const path = this.paths[i];
128
127
  const isSelected = i === this.selectedIndex;
129
- const isCurrent = path === cwd;
128
+ const isCurrent = path === this.cwd;
130
129
 
131
130
  const marker = isSelected ? this.theme.fg("accent", "▸") : " ";
132
131
  const suffix = isCurrent ? this.theme.fg("dim", " (current)") : "";
package/config.ts CHANGED
@@ -125,8 +125,7 @@ export function getAutoRegisterPaths(): string[] {
125
125
  }
126
126
  }
127
127
 
128
- export function loadConfig(cwd: string): MessengerConfig {
129
- const projectPath = join(cwd, ".pi", "pi-messenger.json");
128
+ function buildConfig(projectConfig?: Partial<MessengerConfig> | null): MessengerConfig {
130
129
  const extensionGlobalPath = join(homedir(), ".pi", "agent", "pi-messenger.json");
131
130
  const mainSettingsPath = join(homedir(), ".pi", "agent", "settings.json");
132
131
 
@@ -140,9 +139,6 @@ export function loadConfig(cwd: string): MessengerConfig {
140
139
  // Load extension-specific global config
141
140
  const extensionConfig = readJsonFile(extensionGlobalPath) as Partial<MessengerConfig> | null;
142
141
 
143
- // Load project config (highest priority)
144
- const projectConfig = readJsonFile(projectPath) as Partial<MessengerConfig> | null;
145
-
146
142
  const merged = {
147
143
  ...DEFAULT_CONFIG,
148
144
  ...settingsConfig,
@@ -201,3 +197,13 @@ export function loadConfig(cwd: string): MessengerConfig {
201
197
  ...sharedFields,
202
198
  };
203
199
  }
200
+
201
+ export function loadGlobalConfig(): MessengerConfig {
202
+ return buildConfig();
203
+ }
204
+
205
+ export function loadConfig(cwd: string): MessengerConfig {
206
+ const projectPath = join(cwd, ".pi", "pi-messenger.json");
207
+ const projectConfig = readJsonFile(projectPath) as Partial<MessengerConfig> | null;
208
+ return buildConfig(projectConfig);
209
+ }
@@ -37,15 +37,15 @@ read({ path: ".pi/messenger/crew/tasks/<TASK_ID>.md" })
37
37
 
38
38
  ## Phase 2.5: Load Relevant Skills
39
39
 
40
- If your task prompt includes an **Available Skills** section, read any that match what you're building before starting implementation.
40
+ If your task prompt includes an **Available Skills** section, read skills that match what you're building (for example a framework, testing library, or domain-specific tool) before starting implementation.
41
41
 
42
- If skills are marked **Recommended for this task**, read those first.
42
+ You already have every `pi_messenger` action you need in this prompt, so do not load Crew orchestration references. If skills are marked **Recommended for this task**, read those first.
43
43
 
44
44
  ```typescript
45
45
  read({ path: "<skill-path-from-the-list>" })
46
46
  ```
47
47
 
48
- Skip this phase if no Available Skills section is present.
48
+ Skip this phase if no Available Skills section is present or no skills match your implementation work.
49
49
 
50
50
  ## Phase 3: Start Task & Reserve Files
51
51
 
package/crew/agents.ts CHANGED
@@ -10,28 +10,30 @@ import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
- import { discoverCrewAgents, type CrewAgentConfig } from "./utils/discover.js";
14
- import { truncateOutput } from "./utils/truncate.js";
13
+ import { discoverCrewAgents, type CrewAgentConfig } from "./utils/discover.ts";
14
+ import { truncateOutput } from "./utils/truncate.ts";
15
15
  import {
16
16
  createProgress,
17
17
  parseJsonlLine,
18
18
  updateProgress,
19
- getFinalOutput,
19
+ getAssistantText,
20
+ compactEventForArtifact,
21
+ getTerminalProviderError,
20
22
  type PiEvent,
21
- } from "./utils/progress.js";
23
+ } from "./utils/progress.ts";
22
24
  import {
23
25
  getArtifactPaths,
24
26
  ensureArtifactsDir,
25
27
  writeArtifact,
26
28
  writeMetadata,
27
29
  appendJsonl
28
- } from "./utils/artifacts.js";
29
- import { loadCrewConfig, getTruncationForRole, type CrewConfig } from "./utils/config.js";
30
- import { removeLiveWorker, updateLiveWorker } from "./live-progress.js";
31
- import { autonomousState, waitForConcurrencyChange } from "./state.js";
32
- import { registerWorker, unregisterWorker, killAll } from "./registry.js";
33
- import type { AgentTask, AgentResult } from "./types.js";
34
- import { generateMemorableName } from "../lib.js";
30
+ } from "./utils/artifacts.ts";
31
+ import { loadCrewConfig, getTruncationForRole, type CrewConfig } from "./utils/config.ts";
32
+ import { removeLiveWorker, updateLiveWorker } from "./live-progress.ts";
33
+ import { autonomousState, waitForConcurrencyChange } from "./state.ts";
34
+ import { registerWorker, unregisterWorker, killAll } from "./registry.ts";
35
+ import type { AgentTask, AgentResult } from "./types.ts";
36
+ import { generateMemorableName } from "../lib.ts";
35
37
 
36
38
  const __filename = fileURLToPath(import.meta.url);
37
39
  const __dirname = path.dirname(__filename);
@@ -52,10 +54,16 @@ export function shutdownAllWorkers(): void {
52
54
  export function resolveModel(
53
55
  taskModel?: string,
54
56
  paramModel?: string,
57
+ roleModel?: string,
55
58
  configModel?: string,
59
+ sessionModel?: string,
56
60
  agentModel?: string,
57
61
  ): string | undefined {
58
- return taskModel ?? paramModel ?? configModel ?? agentModel;
62
+ return taskModel ?? paramModel ?? roleModel ?? configModel ?? sessionModel ?? agentModel;
63
+ }
64
+
65
+ export function getPiCommand(): string {
66
+ return process.platform === "win32" ? "pi.cmd" : "pi";
59
67
  }
60
68
 
61
69
  export function pushModelArgs(args: string[], model: string): void {
@@ -138,7 +146,7 @@ export async function spawnAgents(
138
146
  // Setup artifacts directory if enabled
139
147
  const artifactsDir = path.join(crewDir, "artifacts");
140
148
  if (config.artifacts.enabled) {
141
- ensureArtifactsDir(artifactsDir);
149
+ ensureArtifactsDir(artifactsDir, config.artifacts.cleanupDays);
142
150
  }
143
151
 
144
152
  const results: AgentResult[] = [];
@@ -254,7 +262,7 @@ async function runAgent(
254
262
  ? { ...process.env, ...envOverrides, ...workerFlag }
255
263
  : undefined;
256
264
 
257
- const proc = spawn("pi", args, {
265
+ const proc = spawn(getPiCommand(), args, {
258
266
  cwd,
259
267
  stdio: ["ignore", "pipe", "pipe"],
260
268
  ...(env ? { env } : {}),
@@ -266,7 +274,8 @@ async function runAgent(
266
274
  let discoveredWorkerName: string | null = null;
267
275
 
268
276
  let jsonlBuffer = "";
269
- const events: PiEvent[] = [];
277
+ let fullOutput = "";
278
+ let terminalProviderError: string | null = null;
270
279
 
271
280
  proc.stdout?.on("data", (data) => {
272
281
  try {
@@ -277,12 +286,19 @@ async function runAgent(
277
286
  for (const line of lines) {
278
287
  const event = parseJsonlLine(line);
279
288
  if (event) {
280
- events.push(event);
281
289
  updateProgress(progress, event, startTime);
290
+ const assistantText = getAssistantText(event);
291
+ if (assistantText) fullOutput = assistantText;
282
292
  if (artifactPaths) {
283
- try { appendJsonl(artifactPaths.jsonlPath, line); }
293
+ try { appendJsonl(artifactPaths.jsonlPath, JSON.stringify(compactEventForArtifact(event))); }
284
294
  catch { artifactPaths = undefined; }
285
295
  }
296
+ const providerError = getTerminalProviderError(event);
297
+ if (providerError && !terminalProviderError) {
298
+ terminalProviderError = providerError;
299
+ progress.error = providerError;
300
+ proc.kill("SIGTERM");
301
+ }
286
302
  if (task.taskId) {
287
303
  updateLiveWorker(cwd, task.taskId, {
288
304
  taskId: task.taskId,
@@ -308,11 +324,12 @@ async function runAgent(
308
324
  removeLiveWorker(cwd, task.taskId);
309
325
  unregisterWorker(cwd, task.taskId);
310
326
  }
311
- progress.status = code === 0 ? "completed" : "failed";
327
+ const exitCode = terminalProviderError ? 1 : code ?? 1;
328
+ progress.status = exitCode === 0 ? "completed" : "failed";
312
329
  progress.durationMs = Date.now() - startTime;
313
- if (stderr && code !== 0) progress.error = stderr;
330
+ if (terminalProviderError) progress.error = terminalProviderError;
331
+ else if (stderr && exitCode !== 0) progress.error = stderr;
314
332
 
315
- const fullOutput = getFinalOutput(events);
316
333
  const truncation = truncateOutput(fullOutput, maxOutput, artifactPaths?.outputPath);
317
334
 
318
335
  if (artifactPaths) {
@@ -322,7 +339,7 @@ async function runAgent(
322
339
  runId,
323
340
  agent: task.agent,
324
341
  index,
325
- exitCode: code ?? 1,
342
+ exitCode,
326
343
  durationMs: progress.durationMs,
327
344
  tokens: progress.tokens,
328
345
  truncated: truncation.truncated,
@@ -337,7 +354,7 @@ async function runAgent(
337
354
 
338
355
  resolve({
339
356
  agent: task.agent,
340
- exitCode: code ?? 1,
357
+ exitCode,
341
358
  output: truncation.text,
342
359
  truncated: truncation.truncated,
343
360
  progress,
@@ -7,16 +7,21 @@
7
7
  * time — no runtime worker state.
8
8
  */
9
9
 
10
- import type { Task } from "../types.js";
11
- import type { CrewConfig } from "../utils/config.js";
12
- import { readFeedEvents, type FeedEvent } from "../../feed.js";
13
- import * as store from "../store.js";
10
+ import type { Task } from "../types.ts";
11
+ import type { CrewConfig } from "../utils/config.ts";
12
+ import { readFeedEvents, type FeedEvent } from "../../feed.ts";
13
+ import * as store from "../store.ts";
14
+ import * as teamStore from "../team/store.ts";
15
+
16
+ interface CoordinationPromptOptions {
17
+ readOnly?: boolean;
18
+ }
14
19
 
15
20
  // =============================================================================
16
21
  // Dependency Section
17
22
  // =============================================================================
18
23
 
19
- export function buildDependencySection(cwd: string, task: Task, config: CrewConfig): string {
24
+ export function buildDependencySection(cwd: string, task: Task, config: CrewConfig, options: CoordinationPromptOptions = {}): string {
20
25
  if (config.dependencies === "advisory") {
21
26
  const lines: string[] = [
22
27
  "## Dependency Status\n",
@@ -51,10 +56,12 @@ export function buildDependencySection(cwd: string, task: Task, config: CrewConf
51
56
  if (config.coordination !== "none") {
52
57
  lines.push("- DM in-progress workers for API details they're building.");
53
58
  }
59
+ if (!options.readOnly) lines.push("- Reserve your files before editing to prevent conflicts.");
54
60
  lines.push(
55
- "- Reserve your files before editing to prevent conflicts.",
56
61
  "- Do NOT block yourself because a dependency isn't done. Work around it.",
57
- "- Log any local definitions in your progress for later reconciliation.",
62
+ options.readOnly
63
+ ? "- Log findings, assumptions, and handoff notes in your progress for later reconciliation."
64
+ : "- Log any local definitions in your progress for later reconciliation.",
58
65
  );
59
66
 
60
67
  return lines.join("\n") + "\n\n";
@@ -130,6 +137,7 @@ export function buildCoordinationContext(
130
137
  task: Task,
131
138
  config: CrewConfig,
132
139
  concurrentTasks: Task[],
140
+ options: CoordinationPromptOptions = {},
133
141
  ): string {
134
142
  if (config.coordination === "none") return "";
135
143
 
@@ -164,17 +172,36 @@ These tasks are being worked on by other workers in this wave. Discover their ag
164
172
  const concurrentIds = new Set(concurrentTasks.map(t => t.id));
165
173
  const ready = store.getReadyTasks(cwd, { advisory: config.dependencies === "advisory" })
166
174
  .filter(t => t.id !== task.id && !concurrentIds.has(t.id));
167
- if (ready.length > 0) {
175
+ const claimable = ready.filter(t => !teamStore.taskNeedsApproval(t));
176
+ const needsApproval = ready.filter(teamStore.taskPendingApproval);
177
+ const rejected = ready.filter(teamStore.taskNeedsRevision);
178
+ if (claimable.length > 0) {
168
179
  out += `## Ready Tasks
169
180
 
170
- After completing your current task, you can claim one of these:
181
+ ${options.readOnly ? "After completing your current task, only claim one of these if it fits your read-only assignment:" : "After completing your current task, you can claim one of these:"}
171
182
  `;
172
- for (const t of ready) {
183
+ for (const t of claimable) {
173
184
  const deps = t.depends_on.length > 0 ? ` (deps: ${t.depends_on.join(", ")})` : "";
174
185
  out += `- ${t.id}: ${t.title}${deps}\n`;
175
186
  }
176
187
  out += "\n";
177
188
  }
189
+ if (needsApproval.length > 0) {
190
+ out += `## Ready Tasks Needing Approval
191
+
192
+ These tasks are ready but require lead approval before anyone can claim them:
193
+ `;
194
+ for (const t of needsApproval) out += `- ${t.id}: ${t.title} — approve with \`pi_messenger({ action: "task.approve", id: "${t.id}" })\`\n`;
195
+ out += "\n";
196
+ }
197
+ if (rejected.length > 0) {
198
+ out += `## Rejected Tasks Needing Revision
199
+
200
+ These tasks need revision before they can be approved or claimed:
201
+ `;
202
+ for (const t of rejected) out += `- ${t.id}: ${t.title}${t.approval?.feedback ? ` — ${t.approval.feedback}` : ""} — revise with \`pi_messenger({ action: "task.revise", id: "${t.id}", prompt: "Address approval feedback" })\`\n`;
203
+ out += "\n";
204
+ }
178
205
  }
179
206
 
180
207
  return out;
@@ -184,13 +211,23 @@ After completing your current task, you can claim one of these:
184
211
  // Coordination Instructions
185
212
  // =============================================================================
186
213
 
187
- export function buildCoordinationInstructions(config: CrewConfig): string {
214
+ export function buildCoordinationInstructions(config: CrewConfig, options: CoordinationPromptOptions = {}): string {
188
215
  const level = config.coordination;
189
216
  if (level === "none") return "";
190
217
 
191
218
  const budget = config.messageBudgets?.[level] ?? (level === "chatty" ? 10 : 5);
192
219
 
193
220
  if (level === "minimal") {
221
+ if (options.readOnly) {
222
+ return `## Coordination
223
+
224
+ **Message budget: ${budget} messages this session.** The system enforces this.
225
+
226
+ Use \`pi_messenger({ action: "list" })\` to see active workers if your investigation overlaps with their work. Message a worker only when you need concrete context or a handoff detail.
227
+
228
+ `;
229
+ }
230
+
194
231
  return `## Coordination
195
232
 
196
233
  **Message budget: ${budget} messages this session.** The system enforces this.
@@ -220,7 +257,16 @@ Do NOT edit files reserved by another worker without coordinating first.
220
257
 
221
258
  `;
222
259
 
223
- out += `### Announce yourself
260
+ out += options.readOnly
261
+ ? `### Announce yourself
262
+ After joining the mesh and starting your task, announce what you are investigating:
263
+
264
+ \`\`\`typescript
265
+ pi_messenger({ action: "broadcast", message: "Starting <task-id> (<title>) — investigating <scope>" })
266
+ \`\`\`
267
+
268
+ `
269
+ : `### Announce yourself
224
270
  After joining the mesh and starting your task, announce what you're working on:
225
271
 
226
272
  \`\`\`typescript
@@ -243,7 +289,16 @@ If a peer asks you a direct question, reply briefly. Ignore messages that don't
243
289
  `;
244
290
  }
245
291
 
246
- out += `### On completion
292
+ out += options.readOnly
293
+ ? `### On completion
294
+ Announce what you found or recommend:
295
+
296
+ \`\`\`typescript
297
+ pi_messenger({ action: "broadcast", message: "Completed <task-id>: found <summary>" })
298
+ \`\`\`
299
+
300
+ `
301
+ : `### On completion
247
302
  Announce what you built:
248
303
 
249
304
  \`\`\`typescript
@@ -253,7 +308,9 @@ pi_messenger({ action: "broadcast", message: "Completed <task-id>: <file> export
253
308
  ### Reservations
254
309
  Before editing files, check if another worker has reserved them via \`pi_messenger({ action: "list" })\`. If a file you need is reserved, message the owner to coordinate. Do NOT edit reserved files without coordinating first.
255
310
 
256
- ### Questions about dependencies
311
+ `;
312
+
313
+ out += `### Questions about dependencies
257
314
  If your task depends on a completed task and something about its implementation is unclear, read the code and the task's progress log at \`.pi/messenger/crew/tasks/<task-id>.progress.md\`. Dependency authors are from previous waves and are no longer in the mesh.
258
315
 
259
316
  `;
@@ -266,7 +323,7 @@ After completing your assigned task, check if there are ready tasks you can pick
266
323
  pi_messenger({ action: "task.ready" })
267
324
  \`\`\`
268
325
 
269
- If a task is ready, claim and implement it. If \`task.start\` fails (another worker claimed it first), check for other ready tasks. Only claim if your current task completed cleanly and quickly.
326
+ If a task is ready, claim it only if it matches your assignment mode. If \`task.start\` fails (another worker claimed it first or the task needs approval), check for other ready tasks. Only claim if your current task completed cleanly and quickly.
270
327
 
271
328
  `;
272
329
  }