pi-crew 0.1.39 → 0.1.41

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 (44) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +50 -4
  3. package/docs/usage.md +11 -0
  4. package/package.json +1 -1
  5. package/schema.json +4 -1
  6. package/src/agents/discover-agents.ts +1 -1
  7. package/src/config/config.ts +87 -2
  8. package/src/extension/async-notifier.ts +26 -4
  9. package/src/extension/notification-sink.ts +1 -1
  10. package/src/extension/register.ts +23 -7
  11. package/src/extension/registration/subagent-tools.ts +11 -6
  12. package/src/extension/result-watcher.ts +38 -8
  13. package/src/extension/team-tool/api.ts +61 -2
  14. package/src/extension/team-tool/doctor.ts +31 -0
  15. package/src/extension/team-tool/status.ts +23 -3
  16. package/src/observability/metric-sink.ts +1 -1
  17. package/src/runtime/agent-control.ts +4 -5
  18. package/src/runtime/attention-events.ts +23 -0
  19. package/src/runtime/child-pi.ts +2 -1
  20. package/src/runtime/completion-guard.ts +99 -0
  21. package/src/runtime/crew-agent-records.ts +5 -4
  22. package/src/runtime/crew-agent-runtime.ts +2 -2
  23. package/src/runtime/diagnostic-export.ts +2 -16
  24. package/src/runtime/group-join.ts +22 -4
  25. package/src/runtime/live-session-runtime.ts +12 -6
  26. package/src/runtime/sidechain-output.ts +2 -1
  27. package/src/runtime/subagent-manager.ts +6 -1
  28. package/src/runtime/task-runner/live-executor.ts +3 -0
  29. package/src/runtime/task-runner.ts +25 -2
  30. package/src/runtime/team-runner.ts +131 -6
  31. package/src/schema/config-schema.ts +3 -0
  32. package/src/schema/team-tool-schema.ts +12 -3
  33. package/src/state/artifact-store.ts +4 -2
  34. package/src/state/event-log.ts +2 -1
  35. package/src/state/jsonl-writer.ts +3 -1
  36. package/src/state/mailbox.ts +15 -4
  37. package/src/state/types.ts +25 -0
  38. package/src/teams/discover-teams.ts +1 -1
  39. package/src/ui/dashboard-panes/progress-pane.ts +3 -0
  40. package/src/ui/run-snapshot-cache.ts +29 -1
  41. package/src/ui/snapshot-types.ts +8 -0
  42. package/src/utils/fs-watch.ts +3 -3
  43. package/src/utils/redaction.ts +41 -0
  44. package/src/workflows/discover-workflows.ts +1 -1
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { TeamRunManifest } from "./types.ts";
4
4
  import { resolveRealContainedPath } from "../utils/safe-paths.ts";
5
+ import { redactSecrets } from "../utils/redaction.ts";
5
6
 
6
7
  export type MailboxDirection = "inbox" | "outbox";
7
8
  export type MailboxMessageStatus = "queued" | "delivered" | "acknowledged";
@@ -17,6 +18,7 @@ export interface MailboxMessage {
17
18
  status: MailboxMessageStatus;
18
19
  taskId?: string;
19
20
  acknowledgedAt?: string;
21
+ data?: Record<string, unknown>;
20
22
  }
21
23
 
22
24
  export interface MailboxDeliveryState {
@@ -133,7 +135,7 @@ function parseMailboxMessage(raw: unknown, expectedDirection: MailboxDirection):
133
135
  const obj = raw as Record<string, unknown>;
134
136
  if (typeof obj.id !== "string" || typeof obj.runId !== "string" || !isDirection(obj.direction) || typeof obj.from !== "string" || typeof obj.to !== "string" || typeof obj.body !== "string" || typeof obj.createdAt !== "string" || !isStatus(obj.status)) return undefined;
135
137
  if (obj.direction !== expectedDirection) return undefined;
136
- return { id: obj.id, runId: obj.runId, direction: obj.direction, from: obj.from, to: obj.to, body: obj.body, createdAt: obj.createdAt, status: obj.status, taskId: typeof obj.taskId === "string" ? obj.taskId : undefined, acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined };
138
+ return { id: obj.id, runId: obj.runId, direction: obj.direction, from: obj.from, to: obj.to, body: obj.body, createdAt: obj.createdAt, status: obj.status, taskId: typeof obj.taskId === "string" ? obj.taskId : undefined, acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined, data: obj.data && typeof obj.data === "object" && !Array.isArray(obj.data) ? obj.data as Record<string, unknown> : undefined };
137
139
  }
138
140
 
139
141
  function readMailboxFile(filePath: string, direction: MailboxDirection): MailboxMessage[] {
@@ -190,7 +192,7 @@ export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliverySta
190
192
 
191
193
  function writeDeliveryState(manifest: TeamRunManifest, state: MailboxDeliveryState): void {
192
194
  ensureRunMailbox(manifest);
193
- fs.writeFileSync(deliveryFile(manifest, true), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
195
+ fs.writeFileSync(deliveryFile(manifest, true), `${JSON.stringify(redactSecrets(state), null, 2)}\n`, "utf-8");
194
196
  }
195
197
 
196
198
  export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<MailboxMessage, "id" | "runId" | "createdAt" | "status"> & { id?: string; status?: MailboxMessageStatus }): MailboxMessage {
@@ -207,8 +209,9 @@ export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<Ma
207
209
  createdAt,
208
210
  status: message.status ?? "queued",
209
211
  taskId: message.taskId,
212
+ data: message.data,
210
213
  };
211
- fs.appendFileSync(mailboxFile(manifest, complete.direction, complete.taskId), `${JSON.stringify(complete)}\n`, "utf-8");
214
+ fs.appendFileSync(mailboxFile(manifest, complete.direction, complete.taskId), `${JSON.stringify(redactSecrets(complete))}\n`, "utf-8");
212
215
  const delivery = readDeliveryState(manifest);
213
216
  delivery.messages[complete.id] = complete.status;
214
217
  delivery.updatedAt = createdAt;
@@ -216,6 +219,14 @@ export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<Ma
216
219
  return complete;
217
220
  }
218
221
 
222
+ export function findMailboxMessageByRequestId(manifest: TeamRunManifest, requestId: string): MailboxMessage | undefined {
223
+ return readMailbox(manifest).find((message) => message.data?.requestId === requestId);
224
+ }
225
+
226
+ export function readMailboxMessage(manifest: TeamRunManifest, messageId: string): MailboxMessage | undefined {
227
+ return readMailbox(manifest).find((message) => message.id === messageId);
228
+ }
229
+
219
230
  export function acknowledgeMailboxMessage(manifest: TeamRunManifest, messageId: string): MailboxDeliveryState {
220
231
  const delivery = readDeliveryState(manifest);
221
232
  if (!delivery.messages[messageId]) throw new Error(`Mailbox message '${messageId}' not found.`);
@@ -249,7 +260,7 @@ export function validateMailbox(manifest: TeamRunManifest, options: { repair?: b
249
260
  const parsed = JSON.parse(line) as unknown;
250
261
  const message = parseMailboxMessage(parsed, direction);
251
262
  if (!message) throw new Error("invalid message schema");
252
- validLines.push(JSON.stringify(message));
263
+ validLines.push(JSON.stringify(redactSecrets(message)));
253
264
  } catch (error) {
254
265
  const message = error instanceof Error ? error.message : String(error);
255
266
  issues.push({ level: "error", path: filePath, message });
@@ -81,6 +81,30 @@ export interface AsyncRunState {
81
81
  spawnedAt: string;
82
82
  }
83
83
 
84
+ export interface PlanApprovalState {
85
+ required: boolean;
86
+ status: "pending" | "approved" | "cancelled";
87
+ requestedAt: string;
88
+ updatedAt: string;
89
+ approvedAt?: string;
90
+ cancelledAt?: string;
91
+ planTaskId?: string;
92
+ planArtifactPath?: string;
93
+ }
94
+
95
+ export type CrewActivityState = "active" | "active_long_running" | "needs_attention" | "stale";
96
+ export type CrewAttentionReason = "idle" | "tool_failures" | "completion_guard" | "heartbeat_stale" | "plan_approval_pending";
97
+
98
+ export interface CrewAttentionEventData {
99
+ activityState: CrewActivityState;
100
+ reason: CrewAttentionReason;
101
+ elapsedMs?: number;
102
+ taskId?: string;
103
+ agentName?: string;
104
+ suggestedAction?: string;
105
+ observedTools?: string[];
106
+ }
107
+
84
108
  export interface TeamRunManifest {
85
109
  schemaVersion: 1;
86
110
  runId: string;
@@ -98,6 +122,7 @@ export interface TeamRunManifest {
98
122
  eventsPath: string;
99
123
  artifacts: ArtifactDescriptor[];
100
124
  async?: AsyncRunState;
125
+ planApproval?: PlanApprovalState;
101
126
  summary?: string;
102
127
  policyDecisions?: PolicyDecision[];
103
128
  }
@@ -109,7 +109,7 @@ export function discoverTeams(cwd: string): TeamDiscoveryResult {
109
109
 
110
110
  export function allTeams(discovery: TeamDiscoveryResult): TeamConfig[] {
111
111
  const byName = new Map<string, TeamConfig>();
112
- for (const team of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
112
+ for (const team of [...discovery.project, ...discovery.builtin, ...discovery.user]) {
113
113
  byName.set(team.name, team);
114
114
  }
115
115
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
@@ -3,8 +3,11 @@ import type { RunUiSnapshot } from "../snapshot-types.ts";
3
3
  export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[] {
4
4
  if (!snapshot) return ["Progress pane: snapshot unavailable"];
5
5
  const progress = snapshot.progress;
6
+ const groupJoins = snapshot.groupJoins ?? [];
7
+ const groupJoinLines = groupJoins.length ? groupJoins.map((item) => `group join ${item.partial ? "partial" : "completed"}: ${item.requestId} ack=${item.ack}`) : ["group joins: none"];
6
8
  return [
7
9
  `Progress pane: ${progress.completed}/${progress.total} completed · running=${progress.running} queued=${progress.queued} failed=${progress.failed}`,
10
+ ...groupJoinLines,
8
11
  ...snapshot.recentEvents.slice(-10).map((event) => {
9
12
  const seq = event.metadata?.seq !== undefined ? `#${event.metadata.seq}` : "#?";
10
13
  return `${seq} ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? ` · ${event.message}` : ""}`;
@@ -8,7 +8,7 @@ import { readEvents, type TeamEvent } from "../state/event-log.ts";
8
8
  import type { MailboxMessageStatus } from "../state/mailbox.ts";
9
9
  import { loadRunManifestById } from "../state/state-store.ts";
10
10
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
11
- import type { RunSnapshotCache, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
11
+ import type { RunSnapshotCache, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
12
12
 
13
13
  const DEFAULT_TTL_MS = 250;
14
14
  const DEFAULT_MAX_ENTRIES = 24;
@@ -195,6 +195,25 @@ function readDeliveryMessages(filePath: string): Record<string, MailboxMessageSt
195
195
  }
196
196
  }
197
197
 
198
+ function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
199
+ try {
200
+ return fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean).flatMap((line) => {
201
+ try {
202
+ const parsed = JSON.parse(line) as unknown;
203
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
204
+ const message = parsed as { id?: unknown; data?: unknown };
205
+ const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
206
+ if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return [];
207
+ return [{ requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const }];
208
+ } catch {
209
+ return [];
210
+ }
211
+ });
212
+ } catch {
213
+ return [];
214
+ }
215
+ }
216
+
198
217
  function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): number {
199
218
  try {
200
219
  return fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean).reduce((count, line) => {
@@ -213,6 +232,12 @@ function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMes
213
232
  }
214
233
  }
215
234
 
235
+ function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
236
+ const root = path.join(manifest.stateRoot, "mailbox");
237
+ const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
238
+ return readGroupJoinMailbox(path.join(root, "outbox.jsonl"), delivery).slice(-5);
239
+ }
240
+
216
241
  function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunUiMailbox {
217
242
  const root = path.join(manifest.stateRoot, "mailbox");
218
243
  const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
@@ -241,6 +266,7 @@ function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, sta
241
266
  progress: input.progress,
242
267
  usage: input.usage,
243
268
  mailbox: input.mailbox,
269
+ groupJoins: input.groupJoins,
244
270
  events: input.recentEvents.map((event) => [event.metadata?.seq, event.time, event.type, event.taskId, event.message]),
245
271
  output: input.recentOutputLines,
246
272
  stamps,
@@ -306,6 +332,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
306
332
  throw new Error(`Run '${runId}' could not be parsed.`);
307
333
  }
308
334
  const mailbox = mailboxFrom(loaded.manifest, agents);
335
+ const groupJoins = groupJoinsFrom(loaded.manifest);
309
336
  const base = {
310
337
  runId: loaded.manifest.runId,
311
338
  cwd: loaded.manifest.cwd,
@@ -315,6 +342,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
315
342
  progress: progressFromTasks(tasks),
316
343
  usage: usageFrom(tasks, agents),
317
344
  mailbox,
345
+ groupJoins,
318
346
  recentEvents: safeRecentEvents(loaded.manifest.eventsPath, recentEventsLimit),
319
347
  recentOutputLines: recentOutputLines(loaded.manifest, agents, recentOutputLimit),
320
348
  };
@@ -22,6 +22,13 @@ export interface RunUiMailbox {
22
22
  needsAttention: number;
23
23
  }
24
24
 
25
+ export interface RunUiGroupJoin {
26
+ requestId: string;
27
+ messageId: string;
28
+ partial: boolean;
29
+ ack: "pending" | "acknowledged";
30
+ }
31
+
25
32
  export interface RunUiSnapshot {
26
33
  runId: string;
27
34
  cwd: string;
@@ -33,6 +40,7 @@ export interface RunUiSnapshot {
33
40
  progress: RunUiProgress;
34
41
  usage: RunUiUsage;
35
42
  mailbox: RunUiMailbox;
43
+ groupJoins?: RunUiGroupJoin[];
36
44
  recentEvents: TeamEvent[];
37
45
  recentOutputLines: string[];
38
46
  }
@@ -18,14 +18,14 @@ export function closeWatcher(watcher: FSWatcher | null | undefined): void {
18
18
  export function watchWithErrorHandler(
19
19
  path: string,
20
20
  listener: WatchListener<string>,
21
- onError: () => void,
21
+ onError: (error?: unknown) => void,
22
22
  ): FSWatcher | null {
23
23
  try {
24
24
  const watcher = fs.watch(path, listener);
25
25
  watcher.on("error", onError);
26
26
  return watcher;
27
- } catch {
28
- onError();
27
+ } catch (error) {
28
+ onError(error);
29
29
  return null;
30
30
  }
31
31
  }
@@ -0,0 +1,41 @@
1
+ const SECRET_KEY_PATTERN = /(?:^|[_.-])(token|api[-_]?key|password|passwd|secret|credential|authorization|private[-_]?key)(?:$|[_.-])/i;
2
+ const INLINE_SECRET_PATTERN = /(^|[\s,{])(([A-Za-z0-9_.-]*(?:api[-_]?key|token|password|passwd|secret|credential|authorization|private[-_]?key)[A-Za-z0-9_.-]*)\s*[=:]\s*)([^\s,;"'}]+)/gi;
3
+ const AUTH_HEADER_PATTERN = /\b(Authorization\s*:\s*(?:Bearer|Basic|Token)?\s*)([^\r\n]+)/gi;
4
+ const BEARER_PATTERN = /\b(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})\b/g;
5
+ const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
6
+
7
+ function isRecord(value: unknown): value is Record<string, unknown> {
8
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+
11
+ function isSecretKey(keyName: string): boolean {
12
+ return SECRET_KEY_PATTERN.test(keyName) || /^(token|apiKey|api_key|password|secret|credential|authorization|privateKey|private_key)$/i.test(keyName);
13
+ }
14
+
15
+ export function redactSecretString(value: string): string {
16
+ return value
17
+ .replace(PEM_PRIVATE_KEY_PATTERN, "***")
18
+ .replace(AUTH_HEADER_PATTERN, "$1***")
19
+ .replace(BEARER_PATTERN, "$1***")
20
+ .replace(INLINE_SECRET_PATTERN, "$1$2***");
21
+ }
22
+
23
+ export function redactSecrets(value: unknown, keyName = ""): unknown {
24
+ if (keyName && isSecretKey(keyName)) return "***";
25
+ if (typeof value === "string") return redactSecretString(value);
26
+ if (Array.isArray(value)) return value.map((item) => redactSecrets(item));
27
+ if (isRecord(value)) {
28
+ const output: Record<string, unknown> = {};
29
+ for (const [key, entry] of Object.entries(value)) output[key] = redactSecrets(entry, key);
30
+ return output;
31
+ }
32
+ return value;
33
+ }
34
+
35
+ export function redactJsonLine(line: string): string {
36
+ try {
37
+ return JSON.stringify(redactSecrets(JSON.parse(line) as unknown));
38
+ } catch {
39
+ return redactSecretString(line);
40
+ }
41
+ }
@@ -129,7 +129,7 @@ export function discoverWorkflows(cwd: string): WorkflowDiscoveryResult {
129
129
 
130
130
  export function allWorkflows(discovery: WorkflowDiscoveryResult): WorkflowConfig[] {
131
131
  const byName = new Map<string, WorkflowConfig>();
132
- for (const workflow of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
132
+ for (const workflow of [...discovery.project, ...discovery.builtin, ...discovery.user]) {
133
133
  byName.set(workflow.name, workflow);
134
134
  }
135
135
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));