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.
- package/CHANGELOG.md +34 -0
- package/README.md +50 -4
- package/docs/usage.md +11 -0
- package/package.json +1 -1
- package/schema.json +4 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config.ts +87 -2
- package/src/extension/async-notifier.ts +26 -4
- package/src/extension/notification-sink.ts +1 -1
- package/src/extension/register.ts +23 -7
- package/src/extension/registration/subagent-tools.ts +11 -6
- package/src/extension/result-watcher.ts +38 -8
- package/src/extension/team-tool/api.ts +61 -2
- package/src/extension/team-tool/doctor.ts +31 -0
- package/src/extension/team-tool/status.ts +23 -3
- package/src/observability/metric-sink.ts +1 -1
- package/src/runtime/agent-control.ts +4 -5
- package/src/runtime/attention-events.ts +23 -0
- package/src/runtime/child-pi.ts +2 -1
- package/src/runtime/completion-guard.ts +99 -0
- package/src/runtime/crew-agent-records.ts +5 -4
- package/src/runtime/crew-agent-runtime.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -16
- package/src/runtime/group-join.ts +22 -4
- package/src/runtime/live-session-runtime.ts +12 -6
- package/src/runtime/sidechain-output.ts +2 -1
- package/src/runtime/subagent-manager.ts +6 -1
- package/src/runtime/task-runner/live-executor.ts +3 -0
- package/src/runtime/task-runner.ts +25 -2
- package/src/runtime/team-runner.ts +131 -6
- package/src/schema/config-schema.ts +3 -0
- package/src/schema/team-tool-schema.ts +12 -3
- package/src/state/artifact-store.ts +4 -2
- package/src/state/event-log.ts +2 -1
- package/src/state/jsonl-writer.ts +3 -1
- package/src/state/mailbox.ts +15 -4
- package/src/state/types.ts +25 -0
- package/src/teams/discover-teams.ts +1 -1
- package/src/ui/dashboard-panes/progress-pane.ts +3 -0
- package/src/ui/run-snapshot-cache.ts +29 -1
- package/src/ui/snapshot-types.ts +8 -0
- package/src/utils/fs-watch.ts +3 -3
- package/src/utils/redaction.ts +41 -0
- package/src/workflows/discover-workflows.ts +1 -1
package/src/state/mailbox.ts
CHANGED
|
@@ -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 });
|
package/src/state/types.ts
CHANGED
|
@@ -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.
|
|
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
|
};
|
package/src/ui/snapshot-types.ts
CHANGED
|
@@ -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
|
}
|
package/src/utils/fs-watch.ts
CHANGED
|
@@ -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.
|
|
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));
|