pi-crew 0.5.2 → 0.5.6
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 +183 -0
- package/README.md +17 -1
- package/docs/architecture.md +2 -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 +208 -0
- package/docs/optimization-plan.md +642 -0
- package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -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 +12 -9
- 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/benchmark/benchmark-runner.ts +45 -0
- package/src/benchmark/feedback-loop.ts +5 -0
- package/src/config/config.ts +32 -5
- package/src/config/role-tools.ts +82 -0
- package/src/config/suggestions.ts +8 -0
- package/src/config/types.ts +4 -0
- package/src/extension/async-notifier.ts +10 -1
- package/src/extension/crew-cleanup.ts +114 -0
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/notification-router.ts +18 -0
- package/src/extension/register.ts +27 -19
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/team-tool/anchor.ts +201 -0
- package/src/extension/team-tool/api.ts +2 -1
- package/src/extension/team-tool/auto-summarize.ts +154 -0
- package/src/extension/team-tool/run.ts +42 -7
- package/src/extension/team-tool.ts +44 -2
- package/src/hooks/registry.ts +1 -3
- package/src/observability/event-bus.ts +69 -0
- package/src/observability/event-to-metric.ts +0 -2
- package/src/runtime/anchor-manager.ts +473 -0
- package/src/runtime/async-runner.ts +8 -4
- package/src/runtime/auto-summarize.ts +350 -0
- package/src/runtime/background-runner.ts +10 -3
- package/src/runtime/budget-tracker.ts +354 -0
- package/src/runtime/chain-runner.ts +507 -0
- package/src/runtime/child-pi.ts +123 -35
- package/src/runtime/crash-recovery.ts +5 -4
- package/src/runtime/crew-agent-runtime.ts +1 -0
- package/src/runtime/custom-tools/irc-tool.ts +13 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
- package/src/runtime/delivery-coordinator.ts +10 -3
- package/src/runtime/dynamic-script-runner.ts +482 -0
- package/src/runtime/foreground-control.ts +87 -17
- package/src/runtime/handoff-manager.ts +589 -0
- package/src/runtime/hidden-handoff.ts +424 -0
- package/src/runtime/live-agent-manager.ts +20 -4
- package/src/runtime/live-session-runtime.ts +39 -4
- package/src/runtime/manifest-cache.ts +2 -1
- package/src/runtime/model-resolver.ts +16 -4
- package/src/runtime/phase-tracker.ts +373 -0
- package/src/runtime/pi-args.ts +11 -1
- package/src/runtime/pi-json-output.ts +31 -0
- package/src/runtime/pipeline-runner.ts +514 -0
- package/src/runtime/progress-tracker.ts +124 -0
- package/src/runtime/retry-runner.ts +354 -0
- package/src/runtime/sandbox.ts +252 -0
- package/src/runtime/scheduler.ts +7 -2
- package/src/runtime/skill-effectiveness.ts +473 -0
- package/src/runtime/skill-instructions.ts +37 -3
- package/src/runtime/subagent-manager.ts +1 -1
- package/src/runtime/task-graph.ts +11 -1
- package/src/runtime/task-runner.ts +92 -18
- package/src/runtime/team-runner.ts +13 -12
- package/src/runtime/tool-progress.ts +10 -3
- package/src/runtime/verification-gates.ts +367 -0
- package/src/schema/team-tool-schema.ts +37 -0
- package/src/skills/discover-skills.ts +5 -0
- package/src/state/active-run-registry.ts +9 -2
- package/src/state/contracts.ts +9 -0
- package/src/state/crew-init.ts +3 -3
- package/src/state/decision-ledger.ts +98 -55
- package/src/state/event-log-rotation.ts +2 -2
- package/src/state/event-log.ts +144 -10
- package/src/state/hook-instinct-bridge.ts +5 -5
- package/src/state/mailbox.ts +10 -0
- package/src/state/run-cache.ts +18 -8
- package/src/state/state-store.ts +3 -1
- package/src/state/types.ts +4 -0
- package/src/tools/safe-bash-extension.ts +1 -0
- package/src/tools/safe-bash.ts +152 -20
- 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/overlays/mailbox-detail-overlay.ts +13 -2
- package/src/ui/powerbar-publisher.ts +101 -7
- package/src/ui/tool-render.ts +15 -15
- package/src/ui/transcript-cache.ts +13 -0
- package/src/utils/bm25-search.ts +16 -8
- package/src/utils/env-filter.ts +8 -5
- package/src/utils/redaction.ts +169 -15
- package/src/utils/session-utils.ts +52 -0
- package/src/utils/sse-parser.ts +10 -1
- package/src/worktree/cleanup.ts +6 -1
- package/src/worktree/worktree-manager.ts +32 -13
- package/workflows/chain.workflow.md +252 -0
- package/workflows/pipeline.workflow.md +27 -0
package/src/tools/safe-bash.ts
CHANGED
|
@@ -1,50 +1,144 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Safe Bash Tool for pi-crew
|
|
3
3
|
* Wraps bash with dangerous command blocking
|
|
4
|
+
* Uses linear-time scanning to prevent ReDoS attacks
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { Type } from "@sinclair/typebox";
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
+
// Backward-compatible pattern array (kept for getPatterns API)
|
|
10
|
+
// IMPORTANT: Line 8 (rm pattern with nested quantifiers) has been replaced
|
|
11
|
+
// with linear-time checking in isDangerous() to prevent ReDoS attacks.
|
|
9
12
|
const DANGEROUS_PATTERNS = [
|
|
10
|
-
// rm
|
|
11
|
-
/\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*\s*)+(\/|~)(\s*$)/,
|
|
12
|
-
/\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*\s*)+(\/|~)($|\s)/,
|
|
13
|
-
// Privilege escalation
|
|
13
|
+
// NOTE: rm patterns handled by matchesDangerousRm() for linear-time safety
|
|
14
14
|
/\bsudo\b/,
|
|
15
15
|
/\bsu\s+root\b/,
|
|
16
|
-
// Filesystem destruction
|
|
17
16
|
/\bmkfs\b/,
|
|
18
17
|
/\bdd\s+if=/,
|
|
19
|
-
// Fork bomb
|
|
20
18
|
/^:\s*\(\s*\)\s*\{.*\|.*&.*\}\s*;.*$/,
|
|
21
|
-
// Device writing
|
|
22
19
|
/>\s*\/dev\/[sh]d[a-z]/,
|
|
23
20
|
/\bchmod\s+(-[a-zA-Z]+\s+)?777\s+\//,
|
|
24
21
|
/\bchown\s+(-[a-zA-Z]+\s+)?root/,
|
|
25
|
-
// Pipe to shell (download and execute)
|
|
26
22
|
/\bcurl\s.*\|\s*(ba)?sh/i,
|
|
27
23
|
/\bwget\s.*\|\s*(ba)?sh/i,
|
|
28
|
-
// System shutdown/reboot
|
|
29
24
|
/\bshutdown\b/,
|
|
30
25
|
/\breboot\b/,
|
|
31
26
|
/\binit\s+0\b/,
|
|
32
|
-
// Kill critical processes
|
|
33
27
|
/\bkill\s+-9\s+1\b/,
|
|
34
28
|
/\bkillall\b/,
|
|
35
|
-
// Encoded commands
|
|
36
29
|
/\|\s*base64\s+-d/,
|
|
37
30
|
/\|\s*python.*-c/,
|
|
38
31
|
/\|\s*perl.*-e/,
|
|
39
32
|
/\|\s*ruby.*-e/,
|
|
40
|
-
|
|
41
|
-
/\bbash\s+-i\s+>\s*\&/,
|
|
33
|
+
/\bbash\s+-i\s*>\s*\&/,
|
|
42
34
|
/\bexec\s+.*bash/,
|
|
43
|
-
// /etc/passwd manipulation
|
|
44
35
|
/\becho\s+.*>\s*\/etc\/passwd/,
|
|
45
36
|
/\bcat\s+.*>\s*\/etc\/passwd/,
|
|
46
37
|
];
|
|
47
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Linear-time check if command contains a dangerous rm pattern like "rm -rf /" or "rm -rf ~"
|
|
41
|
+
* Replaces O(n²) regex backtracking with O(n) string scanning
|
|
42
|
+
*/
|
|
43
|
+
function matchesDangerousRm(command: string): boolean {
|
|
44
|
+
let pos = 0;
|
|
45
|
+
const len = command.length;
|
|
46
|
+
// Find "rm" at word boundary
|
|
47
|
+
while (pos < len) {
|
|
48
|
+
const rmIdx = command.indexOf("rm", pos);
|
|
49
|
+
if (rmIdx === -1) return false;
|
|
50
|
+
// Check word boundary before "rm"
|
|
51
|
+
if (rmIdx > 0 && /\w/.test(command[rmIdx - 1])) {
|
|
52
|
+
pos = rmIdx + 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Must be followed by whitespace
|
|
56
|
+
const afterRm = rmIdx + 2;
|
|
57
|
+
if (afterRm >= len || /\s/.test(command[afterRm])) {
|
|
58
|
+
// Found "rm " - now check for -rf flags followed by / or ~
|
|
59
|
+
let p = afterRm + 1;
|
|
60
|
+
while (p < len) {
|
|
61
|
+
// Skip whitespace
|
|
62
|
+
while (p < len && /\s/.test(command[p])) p++;
|
|
63
|
+
if (p >= len) break;
|
|
64
|
+
// Check for flag
|
|
65
|
+
if (command[p] !== "-") break;
|
|
66
|
+
p++;
|
|
67
|
+
let hasR = false, hasF = false;
|
|
68
|
+
while (p < len && /[a-zA-Z]/.test(command[p])) {
|
|
69
|
+
if (command[p] === "r" || command[p] === "R") hasR = true;
|
|
70
|
+
if (command[p] === "f" || command[p] === "F") hasF = true;
|
|
71
|
+
p++;
|
|
72
|
+
}
|
|
73
|
+
if (!hasR && !hasF) break; // Flag must have r or f
|
|
74
|
+
// Skip whitespace after flag
|
|
75
|
+
while (p < len && /\s/.test(command[p])) p++;
|
|
76
|
+
}
|
|
77
|
+
// Now check if followed by / or ~ (end or whitespace)
|
|
78
|
+
if (p < len && (command[p] === "/" || command[p] === "~")) {
|
|
79
|
+
const afterSlash = p + 1;
|
|
80
|
+
if (afterSlash >= len || /\s/.test(command[afterSlash]) || command[afterSlash] === ";") {
|
|
81
|
+
return true; // Dangerous!
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
pos = rmIdx + 1;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Linear-time check for fork bomb pattern: :() { ... | ... & ... } ; ...
|
|
92
|
+
*/
|
|
93
|
+
function matchesForkBomb(command: string): boolean {
|
|
94
|
+
// Must start with :
|
|
95
|
+
const trimmed = command.trimStart();
|
|
96
|
+
if (!trimmed.startsWith(":")) return false;
|
|
97
|
+
// Find () after :
|
|
98
|
+
const parenIdx = trimmed.indexOf("()");
|
|
99
|
+
if (parenIdx === -1 || parenIdx > 10) return false; // : must be close to ()
|
|
100
|
+
// Find { after ()
|
|
101
|
+
const braceIdx = trimmed.indexOf("{", parenIdx);
|
|
102
|
+
if (braceIdx === -1 || braceIdx > parenIdx + 5) return false;
|
|
103
|
+
// Find } closing brace
|
|
104
|
+
const closeBrace = trimmed.indexOf("}", braceIdx);
|
|
105
|
+
if (closeBrace === -1) return false;
|
|
106
|
+
// Check content between braces for | and &
|
|
107
|
+
const content = trimmed.slice(braceIdx + 1, closeBrace);
|
|
108
|
+
if (content.includes("|") && content.includes("&")) return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check for encoded command patterns (pipe to shell)
|
|
114
|
+
*/
|
|
115
|
+
function matchesEncodedPipe(command: string): boolean {
|
|
116
|
+
const lower = command.toLowerCase();
|
|
117
|
+
const pipeIdx = lower.indexOf("|");
|
|
118
|
+
if (pipeIdx === -1) return false;
|
|
119
|
+
const afterPipe = lower.slice(pipeIdx + 1).trimStart();
|
|
120
|
+
if (afterPipe.startsWith("base64") || afterPipe.startsWith("python") || afterPipe.startsWith("perl") || afterPipe.startsWith("ruby")) {
|
|
121
|
+
// Check if followed by -d or -c or -e
|
|
122
|
+
const rest = afterPipe.slice(6).trimStart();
|
|
123
|
+
if (rest.startsWith("-d") || rest.startsWith("-c") || rest.startsWith("-e")) return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if command contains a specific dangerous substring
|
|
130
|
+
*/
|
|
131
|
+
function containsDangerous(command: string, pattern: string): boolean {
|
|
132
|
+
return command.indexOf(pattern) !== -1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if command starts with dangerous prefix
|
|
137
|
+
*/
|
|
138
|
+
function startsWithDangerous(command: string, pattern: string): boolean {
|
|
139
|
+
return command.trimStart().startsWith(pattern);
|
|
140
|
+
}
|
|
141
|
+
|
|
48
142
|
export interface SafeBashOptions {
|
|
49
143
|
/** Enable/disable safe mode. Default: true */
|
|
50
144
|
enabled?: boolean;
|
|
@@ -75,9 +169,47 @@ export function isDangerous(command: string, options: SafeBashOptions = {}): str
|
|
|
75
169
|
}
|
|
76
170
|
}
|
|
77
171
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
172
|
+
// Use linear-time scanning functions for critical patterns
|
|
173
|
+
if (matchesDangerousRm(normalized)) {
|
|
174
|
+
return "Command blocked by safe_bash: dangerous rm pattern detected";
|
|
175
|
+
}
|
|
176
|
+
if (matchesForkBomb(normalized)) {
|
|
177
|
+
return "Command blocked by safe_bash: fork bomb pattern detected";
|
|
178
|
+
}
|
|
179
|
+
if (matchesEncodedPipe(normalized)) {
|
|
180
|
+
return "Command blocked by safe_bash: encoded pipe to shell detected";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check remaining patterns using regex (these are safe from ReDoS)
|
|
184
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
185
|
+
if (pattern.test(normalized)) {
|
|
186
|
+
return `Command blocked by safe_bash: matches dangerous pattern \`${pattern}\``;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Additional shell injection checks using regex for non-critical patterns
|
|
191
|
+
// Block command substitution $(...)
|
|
192
|
+
if (/\$\([^)]*\)/.test(command)) {
|
|
193
|
+
return "Command blocked by safe_bash: command substitution $(...) is not allowed";
|
|
194
|
+
}
|
|
195
|
+
// Block backtick substitution
|
|
196
|
+
const backtickRe = /`[^`]*`/;
|
|
197
|
+
if (backtickRe.test(command)) {
|
|
198
|
+
return "Command blocked by safe_bash: backtick substitution is not allowed";
|
|
199
|
+
}
|
|
200
|
+
// Block here-docs <<
|
|
201
|
+
if (/<<\s*['"]?[\w-]+['"]?/.test(command) || /\$<<\s*['"]?[\w-]+['"]?/.test(command)) {
|
|
202
|
+
return "Command blocked by safe_bash: here-doc is not allowed";
|
|
203
|
+
}
|
|
204
|
+
// Block ${...} variable expansion containing shell metacharacters (pipes, redirects, &&/||)
|
|
205
|
+
const varExpRe = /\$\{([^}]*)\}/;
|
|
206
|
+
const varMatch = command.match(varExpRe);
|
|
207
|
+
if (varMatch && /[|&;<>]/.test(varMatch[1])) {
|
|
208
|
+
return "Command blocked by safe_bash: variable expansion with shell metacharacters is not allowed";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check additional patterns (user-provided regex)
|
|
212
|
+
for (const pattern of additionalPatterns) {
|
|
81
213
|
if (pattern.test(normalized)) {
|
|
82
214
|
return `Command blocked by safe_bash: matches dangerous pattern \`${pattern}\``;
|
|
83
215
|
}
|
|
@@ -142,8 +274,8 @@ export function createSafeBash(options: SafeBashOptions = {}) {
|
|
|
142
274
|
* These can be used in allowPatterns for specific use cases
|
|
143
275
|
*/
|
|
144
276
|
export const COMMON_SAFE_PATTERNS = {
|
|
145
|
-
// Safe rm with specific paths
|
|
146
|
-
safeRm:
|
|
277
|
+
// Safe rm with specific paths - uses simple contains check
|
|
278
|
+
safeRm: /rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?((?![\/~])\/)?(tmp|cache|node_modules|dist|build)\//,
|
|
147
279
|
// Safe git operations
|
|
148
280
|
safeGit: /\bgit\s+(clone|pull|push|commit|add|status|diff|log|branch|checkout|merge|rebase)/,
|
|
149
281
|
// Safe npm/yarn/pnpm
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type imports from pi v0.77.0
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
AgentSessionEvent,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
// Note: AgentEvent is not exported by pi-coding-agent v0.77.0
|
|
11
|
+
// Using AgentEndEvent and AgentStartEvent instead
|
|
12
|
+
|
|
13
|
+
// Type guards for pi-crew usage
|
|
14
|
+
export function isToolEvent(event: AgentSessionEvent): boolean {
|
|
15
|
+
return event.type === "tool_execution_start" ||
|
|
16
|
+
event.type === "tool_execution_update" ||
|
|
17
|
+
event.type === "tool_execution_end";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isAgentLifecycleEvent(event: AgentSessionEvent): boolean {
|
|
21
|
+
return event.type === "agent_start" || event.type === "agent_end";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isCompactionEvent(event: AgentSessionEvent): boolean {
|
|
25
|
+
return event.type === "compaction_start" || event.type === "compaction_end";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isRetryEvent(event: AgentSessionEvent): boolean {
|
|
29
|
+
return event.type === "auto_retry_start" || event.type === "auto_retry_end";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isQueueEvent(event: AgentSessionEvent): boolean {
|
|
33
|
+
return event.type === "queue_update";
|
|
34
|
+
}
|
|
@@ -38,6 +38,8 @@ function sourceIcon(source: ResourceSource): string {
|
|
|
38
38
|
case "user": return "👤";
|
|
39
39
|
case "project": return "📂";
|
|
40
40
|
case "git": return "🔗";
|
|
41
|
+
case "dynamic": return "⚡";
|
|
42
|
+
default: return "❓";
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -47,6 +49,8 @@ function sourceLabel(source: ResourceSource): string {
|
|
|
47
49
|
case "user": return "user";
|
|
48
50
|
case "project": return "project";
|
|
49
51
|
case "git": return "git";
|
|
52
|
+
case "dynamic": return "dynamic";
|
|
53
|
+
default: return "unknown";
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -61,7 +65,7 @@ export interface AgentOverlayState {
|
|
|
61
65
|
export function createAgentOverlayState(entries: AgentEntry[], maxVisible = 20): AgentOverlayState {
|
|
62
66
|
return {
|
|
63
67
|
entries: entries.sort((a, b) => {
|
|
64
|
-
const order: Record<ResourceSource, number> = { project: 0, user: 1, git: 2, builtin: 3 };
|
|
68
|
+
const order: Record<ResourceSource, number> = { project: 0, user: 1, git: 2, builtin: 3, dynamic: 4 };
|
|
65
69
|
const diff = (order[a.source] ?? 4) - (order[b.source] ?? 4);
|
|
66
70
|
return diff !== 0 ? diff : a.name.localeCompare(b.name);
|
|
67
71
|
}),
|
package/src/ui/crew-widget.ts
CHANGED
|
@@ -23,14 +23,25 @@ import { SUBAGENT_SPINNER_FRAMES, spinnerBucket, spinnerFrame } from "./spinner.
|
|
|
23
23
|
|
|
24
24
|
const SPINNER = SUBAGENT_SPINNER_FRAMES;
|
|
25
25
|
const TOOL_LABELS: Record<string, string> = {
|
|
26
|
-
|
|
26
|
+
head: "reading",
|
|
27
27
|
bash: "running command",
|
|
28
28
|
edit: "editing",
|
|
29
29
|
write: "writing",
|
|
30
|
-
|
|
30
|
+
grep: "searching",
|
|
31
31
|
find: "finding files",
|
|
32
32
|
ls: "listing",
|
|
33
33
|
};
|
|
34
|
+
|
|
35
|
+
const TOOL_ICONS: Record<string, string> = {
|
|
36
|
+
read: "📖",
|
|
37
|
+
bash: ">",
|
|
38
|
+
edit: "✏",
|
|
39
|
+
write: "📝",
|
|
40
|
+
grep: "🔍",
|
|
41
|
+
find: "📁",
|
|
42
|
+
ls: "📋",
|
|
43
|
+
agent: "🤖",
|
|
44
|
+
};
|
|
34
45
|
const LEGACY_WIDGET_KEY = "pi-crew";
|
|
35
46
|
const WIDGET_KEY = "pi-crew-active";
|
|
36
47
|
const STATUS_KEY = "pi-crew";
|
|
@@ -90,16 +101,16 @@ function describeLiveActivity(handle: LiveAgentHandle): string {
|
|
|
90
101
|
if (act.activeTools.size > 0) {
|
|
91
102
|
const groups = new Map<string, number>();
|
|
92
103
|
for (const toolName of act.activeTools.values()) {
|
|
93
|
-
|
|
94
|
-
groups.set(label, (groups.get(label) ?? 0) + 1);
|
|
104
|
+
groups.set(toolName, (groups.get(toolName) ?? 0) + 1);
|
|
95
105
|
}
|
|
96
106
|
const parts: string[] = [];
|
|
97
|
-
for (const [
|
|
107
|
+
for (const [toolName, count] of groups) {
|
|
108
|
+
const icon = TOOL_ICONS[toolName] ?? "?";
|
|
109
|
+
const label = TOOL_LABELS[toolName] ?? toolName;
|
|
98
110
|
if (count > 1) {
|
|
99
|
-
|
|
100
|
-
parts.push(`${label} ${count} ${noun}`);
|
|
111
|
+
parts.push(`${icon}${count} ${label}s`);
|
|
101
112
|
} else {
|
|
102
|
-
parts.push(label);
|
|
113
|
+
parts.push(`${icon} ${label}`);
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
return parts.join(", ") + "…";
|
|
@@ -241,14 +252,17 @@ export function activeWidgetRuns(cwd: string, manifestCache?: ManifestCache, sna
|
|
|
241
252
|
function statusSummary(runs: WidgetRun[]): string {
|
|
242
253
|
const agents = runs.flatMap((item) => item.agents);
|
|
243
254
|
const runningAgents = agents.filter((agent) => agent.status === "running").length;
|
|
244
|
-
const queuedAgents = agents.filter((agent) => agent.status === "queued").length;
|
|
245
|
-
const waitingAgents = agents.filter((agent) => agent.status === "waiting").length;
|
|
255
|
+
const queuedAgents = agents.filter((agent) => agent.status === "queued" || agent.status === "waiting").length;
|
|
246
256
|
const completedAgents = agents.filter((agent) => agent.status === "completed").length;
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
const totalAgents = agents.length;
|
|
258
|
+
const totalRuns = runs.length;
|
|
259
|
+
const model = agents.find((a) => a.model)?.model?.split("/").at(-1);
|
|
260
|
+
const parts = [`⚙ ${runningAgents}r`];
|
|
261
|
+
if (queuedAgents > 0) parts.push(`${queuedAgents}q`);
|
|
262
|
+
if (completedAgents > 0) parts.push(`${completedAgents}/${totalAgents}done`);
|
|
263
|
+
if (totalRuns > 1) parts.push(`${totalRuns}runs`);
|
|
264
|
+
if (model) parts.push(model);
|
|
265
|
+
return parts.join(" · ");
|
|
252
266
|
}
|
|
253
267
|
|
|
254
268
|
export function notificationBadge(count: number | undefined, env: NodeJS.ProcessEnv = process.env): string {
|
|
@@ -20,6 +20,8 @@ export class MailboxDetailOverlay {
|
|
|
20
20
|
private side: "inbox" | "outbox" = "inbox";
|
|
21
21
|
private selected = 0;
|
|
22
22
|
private expanded = false;
|
|
23
|
+
private lastRefreshedTaskCount = 0;
|
|
24
|
+
private needsRefresh = true;
|
|
23
25
|
|
|
24
26
|
constructor(opts: { runId: string; cwd: string; done: (action: MailboxAction | undefined) => void; theme?: unknown }) {
|
|
25
27
|
this.runId = opts.runId;
|
|
@@ -32,6 +34,12 @@ export class MailboxDetailOverlay {
|
|
|
32
34
|
private refresh(): void {
|
|
33
35
|
const loaded = loadRunManifestById(this.cwd, this.runId);
|
|
34
36
|
if (!loaded) return;
|
|
37
|
+
// Track task count changes to trigger re-render
|
|
38
|
+
const taskCount = loaded.tasks.length;
|
|
39
|
+
if (taskCount !== this.lastRefreshedTaskCount) {
|
|
40
|
+
this.lastRefreshedTaskCount = taskCount;
|
|
41
|
+
this.needsRefresh = true;
|
|
42
|
+
}
|
|
35
43
|
const delivery = readDeliveryState(loaded.manifest).messages;
|
|
36
44
|
const applyDelivery = (message: MailboxMessage): MailboxMessage => ({ ...message, status: delivery[message.id] ?? message.status });
|
|
37
45
|
const taskIds = loaded.tasks.map((task) => task.id);
|
|
@@ -49,11 +57,14 @@ export class MailboxDetailOverlay {
|
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
invalidate(): void {
|
|
52
|
-
this.
|
|
60
|
+
this.needsRefresh = true;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
render(width: number): string[] {
|
|
56
|
-
this.
|
|
64
|
+
if (this.needsRefresh) {
|
|
65
|
+
this.refresh();
|
|
66
|
+
this.needsRefresh = false;
|
|
67
|
+
}
|
|
57
68
|
const inner = Math.max(40, width - 4);
|
|
58
69
|
const col = Math.max(18, Math.floor((inner - 3) / 2));
|
|
59
70
|
const lines = [
|
|
@@ -12,6 +12,8 @@ import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
|
12
12
|
import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
|
|
13
13
|
import { notificationBadge } from "./crew-widget.ts";
|
|
14
14
|
import { RenderCoalescer } from "./render-coalescer.ts";
|
|
15
|
+
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
16
|
+
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
15
17
|
|
|
16
18
|
type EventBus = { emit?: (event: string, data: unknown) => void; listenerCount?: (event: string) => number } | undefined;
|
|
17
19
|
type StatusContext = { hasUI?: boolean; ui?: { setStatus?: (key: string, text: string | undefined) => void } } | undefined;
|
|
@@ -63,6 +65,7 @@ export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUi
|
|
|
63
65
|
if (config?.powerbar === false) return;
|
|
64
66
|
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-active", label: "pi-crew active agents" });
|
|
65
67
|
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-progress", label: "pi-crew run progress" });
|
|
68
|
+
safeEmit(events, "powerbar:register-segment", { id: "pi-crew-steps", label: "pi-crew workflow steps" });
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: CrewUiConfig, manifestCache?: ManifestCache, snapshotCache?: RunSnapshotCache, ctx?: StatusContext, notificationCount = 0, preloadedManifests?: TeamRunManifest[]): void {
|
|
@@ -90,9 +93,10 @@ export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: Cre
|
|
|
90
93
|
if (!active.length) {
|
|
91
94
|
lastActiveKey = undefined;
|
|
92
95
|
lastProgressKey = undefined;
|
|
96
|
+
lastStepsKey = undefined;
|
|
93
97
|
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
94
98
|
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
95
|
-
|
|
99
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
|
|
96
100
|
return;
|
|
97
101
|
}
|
|
98
102
|
const agents = active.flatMap((item) => item.agents);
|
|
@@ -108,13 +112,33 @@ export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: Cre
|
|
|
108
112
|
const model = config?.showModel === false ? undefined : agents.find((agent) => agent.model)?.model?.split("/").at(-1);
|
|
109
113
|
const tokenText = config?.showTokens === false || !tokenTotal ? undefined : compactTokens(tokenTotal);
|
|
110
114
|
const liveRunning = listLiveAgents().filter((a) => a.status === "running").length;
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
// Always show consistent status: running count + queued count from live tasks only
|
|
116
|
+
// Avoid snapshot cache for counts to prevent UI jumping
|
|
117
|
+
const runningCount = agents.filter((a) => a.status === "running").length;
|
|
118
|
+
// Count queued/waiting tasks directly from tasks array (not snapshot) for consistency
|
|
119
|
+
const queuedCount = active.reduce((sum, item) => sum + item.tasks.reduce((s, t) => s + (t.status === "queued" || t.status === "waiting" ? 1 : 0), 0), 0);
|
|
120
|
+
// Format: "1 running", "2 running · 1 queued", "3 queued", "idle"
|
|
121
|
+
const runningLabel = runningCount === 1 ? "1 running" : `${runningCount} running`;
|
|
122
|
+
const queuedLabel = queuedCount === 1 ? "1 queued" : `${queuedCount} queued`;
|
|
123
|
+
const crewStatus = runningCount > 0 && queuedCount > 0 ? `${runningLabel} · ${queuedLabel}` : runningCount > 0 ? runningLabel : queuedCount > 0 ? queuedLabel : "idle";
|
|
124
|
+
const liveSuffix = liveRunning > 0 ? ` (${liveRunning} live)` : "";
|
|
125
|
+
const notificationText = notificationBadge(notificationCount);
|
|
126
|
+
// Always show model + tokens as suffix when available (for activePayload consistency)
|
|
127
|
+
const suffixParts = [model, tokenText].filter(Boolean);
|
|
128
|
+
const activeSuffix = suffixParts.length > 0 ? suffixParts.join(" · ") : undefined;
|
|
129
|
+
// Progress always includes token count for consistency
|
|
113
130
|
const progressSuffix = `${completed}/${total}${tokenText ? ` · ${tokenText}` : ""}`;
|
|
131
|
+
// Build complete, always-consistent fallback text AND event payload to prevent UI flickering
|
|
132
|
+
// Both fallback and events must use the SAME format - no conditional display
|
|
133
|
+
// Format: "⚙ 1 running · 1 queued · model · 30k · 0/1" (never changes based on availability)
|
|
134
|
+
const progressPart = `${completed}/${total}`;
|
|
135
|
+
const allParts = [`⚙ ${crewStatus}`, model ?? "", tokenText ?? "", progressPart].filter(Boolean);
|
|
136
|
+
const unifiedText = allParts.join(" · ");
|
|
137
|
+
// activePayload.text includes notification badge for event payload
|
|
114
138
|
const activePayload = {
|
|
115
139
|
id: "pi-crew-active",
|
|
116
140
|
icon: "⚙",
|
|
117
|
-
text:
|
|
141
|
+
text: `⚙ ${crewStatus}${liveSuffix}${notificationText}${activeSuffix ? ` · ${activeSuffix}` : ""}`,
|
|
118
142
|
suffix: activeSuffix,
|
|
119
143
|
color: running ? "accent" : "warning",
|
|
120
144
|
} as const;
|
|
@@ -126,12 +150,15 @@ export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: Cre
|
|
|
126
150
|
color: completed === total ? "success" : "accent",
|
|
127
151
|
barSegments: 8,
|
|
128
152
|
} as const;
|
|
153
|
+
// Build step progress: "explorer > planner > executor > verifier" with current step highlighted
|
|
154
|
+
const stepsPayload = buildStepsPayload(active, tasks);
|
|
129
155
|
// 1.8: dedup per segment using a key over every visible field. Previously
|
|
130
156
|
// the dedup string only carried text/suffix/running, so changes to `bar`
|
|
131
157
|
// (progress %) or `color` could be swallowed and stale UI emitted again
|
|
132
158
|
// later as a single noisy burst.
|
|
133
159
|
const activeKey = powerbarKey(activePayload);
|
|
134
160
|
const progressKey = powerbarKey(progressPayload);
|
|
161
|
+
const stepsKey = powerbarKey(stepsPayload);
|
|
135
162
|
if (activeKey !== lastActiveKey) {
|
|
136
163
|
lastActiveKey = activeKey;
|
|
137
164
|
safeEmit(events, "powerbar:update", activePayload);
|
|
@@ -140,14 +167,21 @@ export function updatePiCrewPowerbar(events: EventBus, cwd: string, config?: Cre
|
|
|
140
167
|
lastProgressKey = progressKey;
|
|
141
168
|
safeEmit(events, "powerbar:update", progressPayload);
|
|
142
169
|
}
|
|
143
|
-
if (
|
|
170
|
+
if (stepsKey !== lastStepsKey) {
|
|
171
|
+
lastStepsKey = stepsKey;
|
|
172
|
+
safeEmit(events, "powerbar:update", stepsPayload);
|
|
173
|
+
}
|
|
174
|
+
// Never call setStatusFallback - crew-widget manages "pi-crew" status with its own widget format
|
|
175
|
+
// Powerbar only emits events; it does not set status directly
|
|
144
176
|
}
|
|
145
177
|
|
|
146
178
|
// --- Dedup state: skip emit if segment data unchanged ---
|
|
147
179
|
let lastActiveKey: string | undefined;
|
|
148
180
|
let lastProgressKey: string | undefined;
|
|
181
|
+
let lastStepsKey: string | undefined;
|
|
149
182
|
|
|
150
183
|
interface PowerbarPayloadShape {
|
|
184
|
+
id?: string;
|
|
151
185
|
text?: string;
|
|
152
186
|
suffix?: string;
|
|
153
187
|
bar?: number;
|
|
@@ -160,6 +194,63 @@ function powerbarKey(payload: PowerbarPayloadShape): string {
|
|
|
160
194
|
return `${payload.text ?? ""}|${payload.suffix ?? ""}|${payload.bar ?? ""}|${payload.color ?? ""}|${payload.icon ?? ""}|${payload.barSegments ?? ""}`;
|
|
161
195
|
}
|
|
162
196
|
|
|
197
|
+
interface ActiveItem {
|
|
198
|
+
run: TeamRunManifest;
|
|
199
|
+
agents: ReturnType<typeof readCrewAgents>;
|
|
200
|
+
tasks: TeamTaskState[];
|
|
201
|
+
snapshot?: RunUiSnapshot;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Build the workflow steps segment showing: ✓explore › →plan › ○execute › ○verify
|
|
206
|
+
* with the current/active step highlighted using → arrow.
|
|
207
|
+
*/
|
|
208
|
+
function buildStepsPayload(active: ActiveItem[], allTasks: TeamTaskState[]): PowerbarPayloadShape {
|
|
209
|
+
if (!active.length) {
|
|
210
|
+
return { id: "pi-crew-steps" };
|
|
211
|
+
}
|
|
212
|
+
const run = active[0]!.run;
|
|
213
|
+
const workflowName = run.workflow ?? "default";
|
|
214
|
+
// Load workflow steps
|
|
215
|
+
const workflows = allWorkflows(discoverWorkflows(run.cwd));
|
|
216
|
+
const workflow = workflows.find((w) => w.name === workflowName);
|
|
217
|
+
if (!workflow || workflow.steps.length === 0) {
|
|
218
|
+
return { id: "pi-crew-steps", text: workflowName };
|
|
219
|
+
}
|
|
220
|
+
// Build step status map from tasks
|
|
221
|
+
const stepStatus = new Map<string, "completed" | "running" | "pending">();
|
|
222
|
+
for (const task of allTasks) {
|
|
223
|
+
if (!task.stepId) continue;
|
|
224
|
+
if (!stepStatus.has(task.stepId)) {
|
|
225
|
+
if (task.status === "completed") {
|
|
226
|
+
stepStatus.set(task.stepId, "completed");
|
|
227
|
+
} else if (task.status === "running" || task.status === "queued" || task.status === "waiting") {
|
|
228
|
+
stepStatus.set(task.stepId, "running");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Format: "✓explore › →plan › ○execute › ○verify"
|
|
233
|
+
// ✓ = completed, → = running (current), ○ = pending
|
|
234
|
+
const stepParts: string[] = [];
|
|
235
|
+
for (const step of workflow.steps) {
|
|
236
|
+
const status = stepStatus.get(step.id) ?? "pending";
|
|
237
|
+
const icon = status === "completed" ? "✓" : status === "running" ? "→" : "○";
|
|
238
|
+
// Shorten long step names
|
|
239
|
+
const stepName = step.id.length > 10 ? step.id.slice(0, 9) + "…" : step.id;
|
|
240
|
+
stepParts.push(`${icon}${stepName}`);
|
|
241
|
+
}
|
|
242
|
+
const stepsText = stepParts.join(" › ");
|
|
243
|
+
// Color: accent if running step exists, success if all complete, dim otherwise
|
|
244
|
+
const hasRunningStep = [...stepStatus.values()].includes("running");
|
|
245
|
+
const allComplete = stepStatus.size === workflow.steps.length && ![...stepStatus.values()].includes("running");
|
|
246
|
+
const color = allComplete ? "success" : hasRunningStep ? "accent" : "dim";
|
|
247
|
+
return {
|
|
248
|
+
id: "pi-crew-steps",
|
|
249
|
+
text: stepsText,
|
|
250
|
+
color,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
163
254
|
// --- Coalesced powerbar update ---
|
|
164
255
|
|
|
165
256
|
interface PowerbarUpdateArgs {
|
|
@@ -203,19 +294,22 @@ export function requestPowerbarUpdate(
|
|
|
203
294
|
|
|
204
295
|
/** Dispose the powerbar coalescer. Call during extension cleanup. */
|
|
205
296
|
export function disposePowerbarCoalescer(): void {
|
|
297
|
+
powerbarCoalescer.flush();
|
|
206
298
|
powerbarCoalescer.dispose();
|
|
207
299
|
}
|
|
208
300
|
|
|
209
|
-
export function clearPiCrewPowerbar(events: EventBus
|
|
301
|
+
export function clearPiCrewPowerbar(events: EventBus): void {
|
|
210
302
|
lastActiveKey = undefined;
|
|
211
303
|
lastProgressKey = undefined;
|
|
304
|
+
lastStepsKey = undefined;
|
|
212
305
|
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
213
306
|
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
214
|
-
|
|
307
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
|
|
215
308
|
}
|
|
216
309
|
|
|
217
310
|
/** Reset dedup state on session lifecycle events. */
|
|
218
311
|
export function resetPowerbarDedupState(): void {
|
|
219
312
|
lastActiveKey = undefined;
|
|
220
313
|
lastProgressKey = undefined;
|
|
314
|
+
lastStepsKey = undefined;
|
|
221
315
|
}
|