pi-crew 0.5.2 → 0.5.5
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 +67 -0
- package/docs/bugs/cross-session-notification-leakage.md +82 -0
- package/docs/coding-agent-optimization.md +268 -0
- package/docs/deep-review-report.md +384 -0
- package/docs/distillation/cybersecurity-patterns.md +294 -0
- package/docs/migration-v0.4-v0.5.md +191 -0
- package/docs/optimization-plan.md +642 -0
- package/docs/pi-mono-opportunities.md +969 -0
- package/docs/pi-mono-review.md +291 -0
- package/docs/skills/REFERENCE.md +144 -0
- package/package.json +7 -6
- package/skills/artifact-analysis-loop/SKILL.md +302 -0
- package/skills/async-worker-recovery/SKILL.md +19 -1
- package/skills/child-pi-spawning/SKILL.md +19 -6
- package/skills/context-artifact-hygiene/SKILL.md +19 -2
- package/skills/delegation-patterns/SKILL.md +68 -3
- package/skills/detection-pipeline-design/SKILL.md +285 -0
- package/skills/event-log-tracing/SKILL.md +20 -6
- package/skills/git-master/SKILL.md +20 -6
- package/skills/hunting-investigation-loop/SKILL.md +401 -0
- package/skills/incident-playbook-construction/SKILL.md +383 -0
- package/skills/live-agent-lifecycle/SKILL.md +20 -6
- package/skills/mailbox-interactive/SKILL.md +19 -6
- package/skills/model-routing-context/SKILL.md +19 -1
- package/skills/multi-perspective-review/SKILL.md +19 -4
- package/skills/observability-reliability/SKILL.md +19 -2
- package/skills/orchestration/SKILL.md +20 -2
- package/skills/ownership-session-security/SKILL.md +20 -2
- package/skills/pi-extension-lifecycle/SKILL.md +20 -2
- package/skills/post-mortem/SKILL.md +7 -2
- package/skills/read-only-explorer/SKILL.md +20 -6
- package/skills/requirements-to-task-packet/SKILL.md +23 -3
- package/skills/resource-discovery-config/SKILL.md +20 -2
- package/skills/runtime-state-reader/SKILL.md +20 -2
- package/skills/safe-bash/SKILL.md +21 -6
- package/skills/scrutinize/SKILL.md +20 -2
- package/skills/secure-agent-orchestration-review/SKILL.md +29 -2
- package/skills/security-review/SKILL.md +560 -0
- package/skills/state-mutation-locking/SKILL.md +22 -2
- package/skills/systematic-debugging/SKILL.md +8 -6
- package/skills/threat-hypothesis-framework/SKILL.md +175 -0
- package/skills/ui-render-performance/SKILL.md +20 -2
- package/skills/verification-before-done/SKILL.md +17 -2
- package/skills/widget-rendering/SKILL.md +21 -6
- package/skills/workspace-isolation/SKILL.md +20 -6
- package/skills/worktree-isolation/SKILL.md +20 -6
- package/src/agents/agent-config.ts +40 -1
- package/src/config/config.ts +22 -5
- package/src/config/role-tools.ts +82 -0
- package/src/config/types.ts +4 -0
- package/src/extension/crew-cleanup.ts +114 -0
- package/src/extension/register.ts +15 -3
- package/src/extension/team-tool/run.ts +7 -7
- package/src/observability/event-bus.ts +60 -0
- package/src/runtime/background-runner.ts +8 -2
- package/src/runtime/child-pi.ts +122 -34
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/foreground-control.ts +87 -17
- package/src/runtime/pi-args.ts +11 -1
- package/src/runtime/pi-json-output.ts +31 -0
- package/src/runtime/progress-tracker.ts +124 -0
- package/src/runtime/skill-effectiveness.ts +473 -0
- package/src/runtime/skill-instructions.ts +37 -3
- package/src/runtime/task-runner.ts +91 -17
- package/src/runtime/team-runner.ts +11 -11
- package/src/runtime/tool-progress.ts +10 -3
- package/src/runtime/verification-gates.ts +367 -0
- package/src/schema/team-tool-schema.ts +7 -0
- package/src/state/decision-ledger.ts +92 -43
- package/src/state/event-log.ts +136 -10
- package/src/state/hook-instinct-bridge.ts +5 -5
- package/src/state/state-store.ts +3 -1
- package/src/state/types.ts +4 -0
- package/src/types/new-api-types.ts +34 -0
- package/src/ui/agent-management-overlay.ts +5 -1
- package/src/ui/crew-widget.ts +29 -15
- package/src/ui/powerbar-publisher.ts +100 -7
- package/src/ui/tool-render.ts +15 -15
- package/src/utils/session-utils.ts +52 -0
- package/src/worktree/worktree-manager.ts +32 -13
|
@@ -4,6 +4,8 @@ import { appendEvent } from "../state/event-log.ts";
|
|
|
4
4
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
5
|
import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
|
|
6
6
|
import { readCrewAgents } from "./crew-agent-records.ts";
|
|
7
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
+
import { sleepSync } from "../utils/sleep.ts";
|
|
7
9
|
|
|
8
10
|
export type ForegroundControlRequestType = "interrupt" | "status";
|
|
9
11
|
|
|
@@ -59,24 +61,92 @@ export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: Te
|
|
|
59
61
|
|
|
60
62
|
export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
|
|
61
63
|
const controlPath = foregroundControlPath(manifest);
|
|
64
|
+
const lockDir = `${controlPath}.lock`;
|
|
65
|
+
const pidFile = path.join(lockDir, "pid");
|
|
62
66
|
let requests: ForegroundControlRequest[] = [];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
|
|
68
|
+
// FIX: Use file locking to prevent race condition in read-modify-write
|
|
69
|
+
// Added stale lock detection similar to event-log.ts
|
|
70
|
+
const acquireLock = (): void => {
|
|
71
|
+
const timeout = 5000;
|
|
72
|
+
const staleMs = 10000;
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
while (true) {
|
|
75
|
+
try {
|
|
76
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
77
|
+
try { fs.writeFileSync(pidFile, String(process.pid), "utf-8"); } catch { /* best-effort */ }
|
|
78
|
+
break;
|
|
79
|
+
} catch {
|
|
80
|
+
if (Date.now() - start > timeout) {
|
|
81
|
+
// Check if lock is stale before giving up
|
|
82
|
+
try {
|
|
83
|
+
const raw = fs.readFileSync(pidFile, "utf-8").trim();
|
|
84
|
+
const ownerPid = Number.parseInt(raw, 10);
|
|
85
|
+
if (!Number.isNaN(ownerPid) && ownerPid !== process.pid) {
|
|
86
|
+
let alive = false;
|
|
87
|
+
try { process.kill(ownerPid, 0); alive = true; } catch { /* dead */ }
|
|
88
|
+
if (!alive) {
|
|
89
|
+
// Lock is stale — clear it and retry
|
|
90
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Lock held by live process — throw
|
|
94
|
+
const err = new Error(`Foreground control lock timeout for ${controlPath}`);
|
|
95
|
+
logInternalError("foreground-control.lock-timeout", err, `controlPath=${controlPath}`);
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
} catch { /* no pid file — continue to throw */ }
|
|
99
|
+
const err = new Error(`Foreground control lock timeout for ${controlPath}`);
|
|
100
|
+
logInternalError("foreground-control.lock-timeout", err, `controlPath=${controlPath}`);
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
// Stale detection: if the owning process is dead, remove the stale lock
|
|
104
|
+
try {
|
|
105
|
+
const raw = fs.readFileSync(pidFile, "utf-8").trim();
|
|
106
|
+
const ownerPid = Number.parseInt(raw, 10);
|
|
107
|
+
if (!Number.isNaN(ownerPid) && ownerPid !== process.pid) {
|
|
108
|
+
let alive = false;
|
|
109
|
+
try { process.kill(ownerPid, 0); alive = true; } catch { /* dead */ }
|
|
110
|
+
if (!alive) {
|
|
111
|
+
const stat = fs.statSync(lockDir);
|
|
112
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
113
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch { /* no pid file — fall through to sleep */ }
|
|
119
|
+
// Brief sleep to avoid CPU spinning
|
|
120
|
+
sleepSync(10);
|
|
121
|
+
}
|
|
69
122
|
}
|
|
70
|
-
}
|
|
71
|
-
const request: ForegroundControlRequest = {
|
|
72
|
-
id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
|
|
73
|
-
type: "interrupt",
|
|
74
|
-
createdAt: new Date().toISOString(),
|
|
75
|
-
reason,
|
|
76
|
-
acknowledged: false,
|
|
77
123
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
124
|
+
const releaseLock = (): void => {
|
|
125
|
+
try { fs.rmSync(lockDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
acquireLock();
|
|
129
|
+
try {
|
|
130
|
+
if (fs.existsSync(controlPath)) {
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
|
|
133
|
+
requests = Array.isArray(parsed.requests) ? parsed.requests : [];
|
|
134
|
+
} catch {
|
|
135
|
+
requests = [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const request: ForegroundControlRequest = {
|
|
139
|
+
id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
|
|
140
|
+
type: "interrupt",
|
|
141
|
+
createdAt: new Date().toISOString(),
|
|
142
|
+
reason,
|
|
143
|
+
acknowledged: false,
|
|
144
|
+
};
|
|
145
|
+
fs.mkdirSync(path.dirname(controlPath), { recursive: true });
|
|
146
|
+
fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
|
|
147
|
+
appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
|
|
148
|
+
return request;
|
|
149
|
+
} finally {
|
|
150
|
+
releaseLock();
|
|
151
|
+
}
|
|
82
152
|
}
|
package/src/runtime/pi-args.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
6
|
+
import { getAgentSessionOptions } from "../agents/agent-config.ts";
|
|
6
7
|
|
|
7
8
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
8
9
|
const PROMPT_RUNTIME_EXTENSION_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "prompt", "prompt-runtime.ts");
|
|
@@ -17,6 +18,8 @@ export interface BuildPiWorkerArgsInput {
|
|
|
17
18
|
maxDepth?: number;
|
|
18
19
|
skillPaths?: string[];
|
|
19
20
|
env?: NodeJS.ProcessEnv;
|
|
21
|
+
/** Role for tool restrictions (uses role-tools.ts config) */
|
|
22
|
+
role?: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export interface BuildPiWorkerArgsResult {
|
|
@@ -99,7 +102,14 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
99
102
|
args.push("--thinking", input.agent.thinking);
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
// Apply role-based tool restrictions (from role-tools.ts)
|
|
106
|
+
// Role-specific config takes precedence over agent-defined tools
|
|
107
|
+
const toolConfig = input.role ? getAgentSessionOptions(input.role) : {};
|
|
108
|
+
const explicitTools = toolConfig.tools ?? input.agent.tools;
|
|
109
|
+
const excludeTools = toolConfig.excludeTools;
|
|
110
|
+
|
|
111
|
+
if (explicitTools?.length) args.push("--tools", explicitTools.join(","));
|
|
112
|
+
if (excludeTools?.length) args.push("--exclude-tools", excludeTools.join(","));
|
|
103
113
|
// Always add --no-extensions before --extension to prevent user extensions from being auto-loaded.
|
|
104
114
|
// User extensions in ~/.pi/agent/extensions/ may fail due to missing dependencies.
|
|
105
115
|
args.push("--no-extensions");
|
|
@@ -12,6 +12,8 @@ export interface ParsedPiJsonOutput {
|
|
|
12
12
|
textEvents: string[];
|
|
13
13
|
finalText?: string;
|
|
14
14
|
usage?: ParsedPiUsage;
|
|
15
|
+
/** Unified patches extracted from tool_result events (edit tool patch field) */
|
|
16
|
+
patches?: string[];
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -87,6 +89,7 @@ function extractText(value: unknown): string[] {
|
|
|
87
89
|
export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
|
|
88
90
|
let jsonEvents = 0;
|
|
89
91
|
const textEvents: string[] = [];
|
|
92
|
+
const patches: string[] = [];
|
|
90
93
|
let usage: ParsedPiUsage | undefined;
|
|
91
94
|
for (const line of stdout.split("\n")) {
|
|
92
95
|
const trimmed = line.trim();
|
|
@@ -99,6 +102,8 @@ export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
|
|
|
99
102
|
}
|
|
100
103
|
jsonEvents++;
|
|
101
104
|
textEvents.push(...extractText(event));
|
|
105
|
+
// Extract unified patches from tool_result events
|
|
106
|
+
extractPatch(event, patches);
|
|
102
107
|
const eventUsage = extractUsage(event);
|
|
103
108
|
if (eventUsage) usage = mergeUsage(usage ?? {}, eventUsage);
|
|
104
109
|
}
|
|
@@ -107,5 +112,31 @@ export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
|
|
|
107
112
|
textEvents,
|
|
108
113
|
finalText: textEvents.length > 0 ? textEvents[textEvents.length - 1] : undefined,
|
|
109
114
|
usage,
|
|
115
|
+
patches: patches.length > 0 ? patches : undefined,
|
|
110
116
|
};
|
|
111
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract unified patches from a tool_result event.
|
|
121
|
+
* pi's edit tool now includes a `patch` field (standard unified diff format).
|
|
122
|
+
* We detect it by looking for lines starting with "---" or "+++" which indicate
|
|
123
|
+
* unified diff format.
|
|
124
|
+
*/
|
|
125
|
+
function extractPatch(event: unknown, patches: string[]): void {
|
|
126
|
+
const obj = asRecord(event);
|
|
127
|
+
if (!obj || obj.type !== "tool_result") return;
|
|
128
|
+
|
|
129
|
+
const content = obj.content;
|
|
130
|
+
if (!Array.isArray(content)) return;
|
|
131
|
+
|
|
132
|
+
for (const item of content) {
|
|
133
|
+
const part = asRecord(item);
|
|
134
|
+
if (!part || part.type !== "text") continue;
|
|
135
|
+
const text = typeof part.text === "string" ? part.text : "";
|
|
136
|
+
|
|
137
|
+
// Check if this looks like a unified patch (starts with "---" or "+++")
|
|
138
|
+
if (text.includes("--- a/") || text.includes("diff ---")) {
|
|
139
|
+
patches.push(text);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { crewEventBus } from "../observability/event-bus.ts";
|
|
3
|
+
|
|
4
|
+
export interface AgentProgress {
|
|
5
|
+
toolCalls: number;
|
|
6
|
+
currentTool: string | null;
|
|
7
|
+
toolStartTime: number | null;
|
|
8
|
+
errors: string[];
|
|
9
|
+
turns: number;
|
|
10
|
+
tokens: { input: number; output: number };
|
|
11
|
+
status: "idle" | "running" | "completed" | "error";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ProgressTracker {
|
|
15
|
+
private sessions = new Map<string, {
|
|
16
|
+
unsubscribe: () => void;
|
|
17
|
+
progress: AgentProgress;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
track(
|
|
21
|
+
session: { subscribe: (listener: (event: AgentSessionEvent) => void) => () => void },
|
|
22
|
+
agentId: string,
|
|
23
|
+
runId: string
|
|
24
|
+
): AgentProgress {
|
|
25
|
+
if (this.sessions.has(agentId)) {
|
|
26
|
+
return this.sessions.get(agentId)!.progress;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const progress: AgentProgress = {
|
|
30
|
+
toolCalls: 0,
|
|
31
|
+
currentTool: null,
|
|
32
|
+
toolStartTime: null,
|
|
33
|
+
errors: [],
|
|
34
|
+
turns: 0,
|
|
35
|
+
tokens: { input: 0, output: 0 },
|
|
36
|
+
status: "running",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
40
|
+
this.handleEvent(event, progress, agentId, runId);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.sessions.set(agentId, { unsubscribe, progress });
|
|
44
|
+
return progress;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private handleEvent(
|
|
48
|
+
event: AgentSessionEvent,
|
|
49
|
+
progress: AgentProgress,
|
|
50
|
+
agentId: string,
|
|
51
|
+
runId: string
|
|
52
|
+
): void {
|
|
53
|
+
switch (event.type) {
|
|
54
|
+
case "tool_execution_start":
|
|
55
|
+
progress.toolCalls++;
|
|
56
|
+
progress.currentTool = event.toolName;
|
|
57
|
+
progress.toolStartTime = Date.now();
|
|
58
|
+
crewEventBus.emit({
|
|
59
|
+
type: "agent:progress",
|
|
60
|
+
runId,
|
|
61
|
+
agentId,
|
|
62
|
+
payload: { ...progress },
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
});
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case "tool_execution_end":
|
|
68
|
+
progress.currentTool = null;
|
|
69
|
+
progress.toolStartTime = null;
|
|
70
|
+
if (event.isError) {
|
|
71
|
+
progress.errors.push(String(event.result ?? "Unknown error"));
|
|
72
|
+
crewEventBus.emit({
|
|
73
|
+
type: "agent:error",
|
|
74
|
+
runId,
|
|
75
|
+
agentId,
|
|
76
|
+
payload: String(event.result ?? "Unknown error"),
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
crewEventBus.emit({
|
|
81
|
+
type: "agent:progress",
|
|
82
|
+
runId,
|
|
83
|
+
agentId,
|
|
84
|
+
payload: { ...progress },
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case "turn_start":
|
|
90
|
+
progress.turns++;
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case "agent_end":
|
|
94
|
+
progress.status = "completed";
|
|
95
|
+
crewEventBus.emit({
|
|
96
|
+
type: "agent:complete",
|
|
97
|
+
runId,
|
|
98
|
+
agentId,
|
|
99
|
+
payload: { ...progress },
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
});
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case "agent_start":
|
|
105
|
+
progress.status = "running";
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
untrack(agentId: string): void {
|
|
111
|
+
const tracked = this.sessions.get(agentId);
|
|
112
|
+
if (tracked) {
|
|
113
|
+
tracked.unsubscribe();
|
|
114
|
+
this.sessions.delete(agentId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getProgress(agentId: string): AgentProgress | undefined {
|
|
119
|
+
return this.sessions.get(agentId)?.progress;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Export singleton instance
|
|
124
|
+
export const globalProgressTracker = new ProgressTracker();
|