pi-crew 0.9.30 → 0.9.32
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 +28 -0
- package/dist/build-meta.json +187 -153
- package/dist/index.mjs +1346 -1276
- package/dist/index.mjs.map +4 -4
- package/package.json +3 -2
- package/src/extension/register.ts +3 -0
- package/src/runtime/team-runner.ts +36 -4
- package/src/state/state-store.ts +9 -3
- package/src/ui/run-dashboard.ts +25 -10
- package/src/workflows/cost-estimator.ts +34 -0
- package/src/workflows/validate-workflow.ts +10 -0
- package/tsconfig.json +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.32",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -117,9 +117,10 @@
|
|
|
117
117
|
"@earendil-works/pi-ai": "^0.77.0",
|
|
118
118
|
"@earendil-works/pi-coding-agent": "^0.77.0",
|
|
119
119
|
"@earendil-works/pi-tui": "^0.77.0",
|
|
120
|
+
"@types/node": "^25.9.5",
|
|
120
121
|
"esbuild": "^0.28.1",
|
|
121
122
|
"tsx": "^4.23.0",
|
|
122
|
-
"typescript": "^
|
|
123
|
+
"typescript": "^7.0.2"
|
|
123
124
|
},
|
|
124
125
|
"peerDependenciesMeta": {
|
|
125
126
|
"@earendil-works/pi-agent-core": {
|
|
@@ -139,6 +139,9 @@ export { __test__subagentSpawnParams };
|
|
|
139
139
|
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
140
140
|
const disposeI18n = initI18n(pi);
|
|
141
141
|
resetTimings();
|
|
142
|
+
// S06: Verbose/debug flags for pi-crew diagnostics
|
|
143
|
+
const verbose = process.env.PI_CREW_VERBOSE === "1" || process.argv.includes("--verbose");
|
|
144
|
+
const debug = process.env.PI_CREW_DEBUG === "1" || process.argv.includes("--debug");
|
|
142
145
|
time("register:start");
|
|
143
146
|
// Cold-start race fix (general): pre-warm the hot module graph NOW, during
|
|
144
147
|
// single-threaded registration, before any concurrent subagent can spawn.
|
|
@@ -2,13 +2,14 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
4
4
|
import type { CrewLimitsConfig, CrewReliabilityConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
5
|
+
import { CrewError, ErrorCode } from "../errors.ts";
|
|
5
6
|
import { appendHookEvent, executeHook } from "../hooks/registry.ts";
|
|
6
7
|
import { childCorrelation, withCorrelation } from "../observability/correlation.ts";
|
|
7
8
|
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
8
9
|
import { PluginRegistry } from "../plugins/plugin-registry.ts";
|
|
9
10
|
import { NextJsPlugin, VitePlugin, VitestPlugin } from "../plugins/plugins/index.ts";
|
|
10
11
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
11
|
-
import { appendEvent, appendEventAsync, appendEventBuffered, flushEventLogBuffer } from "../state/event-log.ts";
|
|
12
|
+
import { appendEvent, appendEventAsync, appendEventBuffered, appendEventFireAndForget, flushEventLogBuffer } from "../state/event-log.ts";
|
|
12
13
|
import { HealthStore } from "../state/health-store.ts";
|
|
13
14
|
import { withRunLock } from "../state/locks.ts";
|
|
14
15
|
import { loadRunManifestById, saveRunManifest, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/state-store.ts";
|
|
@@ -190,13 +191,19 @@ export function checkPerTaskBudget(
|
|
|
190
191
|
|
|
191
192
|
function findStep(workflow: WorkflowConfig, task: TeamTaskState): WorkflowStep {
|
|
192
193
|
const step = workflow.steps.find((candidate) => candidate.id === task.stepId);
|
|
193
|
-
if (!step)
|
|
194
|
+
if (!step)
|
|
195
|
+
throw new CrewError(ErrorCode.ResourceNotFound, `Workflow step '${task.stepId}' not found for task '${task.id}'.`).withContext(
|
|
196
|
+
`workflow step lookup (task=${task.id})`,
|
|
197
|
+
);
|
|
194
198
|
return step;
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
function findAgent(agents: AgentConfig[], task: TeamTaskState): AgentConfig {
|
|
198
202
|
const agent = agents.find((candidate) => candidate.name === task.agent);
|
|
199
|
-
if (!agent)
|
|
203
|
+
if (!agent)
|
|
204
|
+
throw new CrewError(ErrorCode.ResourceNotFound, `Agent '${task.agent}' not found for task '${task.id}'.`).withContext(
|
|
205
|
+
`agent lookup (task=${task.id})`,
|
|
206
|
+
);
|
|
200
207
|
return agent;
|
|
201
208
|
}
|
|
202
209
|
|
|
@@ -1301,7 +1308,9 @@ async function executeTeamRunCore(
|
|
|
1301
1308
|
};
|
|
1302
1309
|
if (failed) {
|
|
1303
1310
|
lastFailed = enriched;
|
|
1304
|
-
throw new
|
|
1311
|
+
throw new CrewError(ErrorCode.TaskNotFound, failed.error ?? `Task ${task.id} failed.`).withContext(
|
|
1312
|
+
`retry evaluation (run=${manifest.runId})`,
|
|
1313
|
+
);
|
|
1305
1314
|
}
|
|
1306
1315
|
input.metricRegistry?.histogram("crew.task.retry_count", "Retries per task", [0, 1, 2, 3, 5, 10]).observe(
|
|
1307
1316
|
{
|
|
@@ -1652,6 +1661,29 @@ async function executeTeamRunCore(
|
|
|
1652
1661
|
const waiting = tasks.find((task) => task.status === "waiting");
|
|
1653
1662
|
const running = tasks.find((task) => task.status === "running");
|
|
1654
1663
|
manifest = applyPolicy(manifest, tasks, input.limits);
|
|
1664
|
+
|
|
1665
|
+
// S02: Verify workflow-declared output files exist before marking completed
|
|
1666
|
+
if (input.workflow?.steps) {
|
|
1667
|
+
const missingOutputs: string[] = [];
|
|
1668
|
+
for (const step of input.workflow.steps) {
|
|
1669
|
+
if (step.output && typeof step.output === "string") {
|
|
1670
|
+
const outputPath = path.join(manifest.artifactsRoot, step.output);
|
|
1671
|
+
if (!fs.existsSync(outputPath)) {
|
|
1672
|
+
missingOutputs.push(step.output);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
if (missingOutputs.length > 0) {
|
|
1677
|
+
// Emit warning event — run still completes normally to avoid hanging
|
|
1678
|
+
appendEventFireAndForget(manifest.eventsPath, {
|
|
1679
|
+
type: "run.deliverable_warning",
|
|
1680
|
+
runId: manifest.runId,
|
|
1681
|
+
message: `Missing workflow output files: ${missingOutputs.join(", ")}`,
|
|
1682
|
+
data: { missingFiles: missingOutputs },
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1655
1687
|
const effectiveness = evaluateRunEffectiveness({
|
|
1656
1688
|
manifest,
|
|
1657
1689
|
tasks,
|
package/src/state/state-store.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
|
|
4
5
|
import { errors } from "../errors.ts";
|
|
@@ -83,7 +84,7 @@ function genOf(stateRoot: string): number {
|
|
|
83
84
|
return manifestCacheGeneration.get(stateRoot) ?? 0;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
const MANIFEST_CACHE_TTL_MS =
|
|
87
|
+
const MANIFEST_CACHE_TTL_MS = 60 * 1000; // 60 seconds (FIX: increased from 15s for read-heavy workloads; render tick 160ms caused frequent misses)
|
|
87
88
|
const LOAD_MANIFEST_RETRY_LIMIT = 5; // Configurable retry limit for mtime/size stability checks under contention
|
|
88
89
|
const manifestCache = new Map<string, ManifestCacheEntry>();
|
|
89
90
|
|
|
@@ -475,7 +476,7 @@ export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTa
|
|
|
475
476
|
// FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving.
|
|
476
477
|
invalidateRunCache(manifest.stateRoot);
|
|
477
478
|
try {
|
|
478
|
-
|
|
479
|
+
await fsp.access(manifest.stateRoot);
|
|
479
480
|
} catch {
|
|
480
481
|
return;
|
|
481
482
|
}
|
|
@@ -800,7 +801,12 @@ export async function loadRunManifestByIdAsync(
|
|
|
800
801
|
} else if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
|
|
801
802
|
manifestCache.delete(stateRoot);
|
|
802
803
|
return undefined;
|
|
803
|
-
} else if (
|
|
804
|
+
} else if (
|
|
805
|
+
!(await fsp.access(tasksPath).then(
|
|
806
|
+
() => true,
|
|
807
|
+
() => false,
|
|
808
|
+
))
|
|
809
|
+
) {
|
|
804
810
|
// Tasks file was deleted after cache was populated — do not serve stale cache.
|
|
805
811
|
manifestCache.delete(stateRoot);
|
|
806
812
|
return undefined;
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -28,6 +28,17 @@ import { applyStatusColor, colorizeStatusGlyphs, iconForStatus, type RunStatus }
|
|
|
28
28
|
import type { CrewTheme } from "./theme-adapter.ts";
|
|
29
29
|
import { asCrewTheme, subscribeThemeChange } from "./theme-adapter.ts";
|
|
30
30
|
|
|
31
|
+
/** S05 — wrap a pane render in try/catch so a single pane crash does not bring down the whole dashboard. */
|
|
32
|
+
function safeRenderPane(name: string, fn: () => string[]): string[] {
|
|
33
|
+
try {
|
|
34
|
+
return fn();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
logInternalError("run-dashboard", new Error(`Dashboard pane '${name}' render failed: ${message}`));
|
|
38
|
+
return [`<error: ${name}>`];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
interface DashboardComponent {
|
|
32
43
|
invalidate(): void;
|
|
33
44
|
render(width: number): string[];
|
|
@@ -615,20 +626,24 @@ export class RunDashboard implements DashboardComponent {
|
|
|
615
626
|
// Pane content (max 8 lines) — F-2: colorize embedded status glyphs.
|
|
616
627
|
const paneLines = snap
|
|
617
628
|
? this.activePane === "agents"
|
|
618
|
-
? renderAgentsPane(snap, this.options)
|
|
629
|
+
? safeRenderPane("agents", () => renderAgentsPane(snap, this.options))
|
|
619
630
|
: this.activePane === "progress"
|
|
620
|
-
? renderProgressPane(snap)
|
|
631
|
+
? safeRenderPane("progress", () => renderProgressPane(snap))
|
|
621
632
|
: this.activePane === "mailbox"
|
|
622
|
-
? renderMailboxPane(snap)
|
|
633
|
+
? safeRenderPane("mailbox", () => renderMailboxPane(snap))
|
|
623
634
|
: this.activePane === "health"
|
|
624
|
-
?
|
|
625
|
-
|
|
626
|
-
|
|
635
|
+
? safeRenderPane("health", () =>
|
|
636
|
+
renderHealthPane(snap, {
|
|
637
|
+
isForeground: !r.async,
|
|
638
|
+
}),
|
|
639
|
+
)
|
|
627
640
|
: this.activePane === "metrics"
|
|
628
|
-
?
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
641
|
+
? safeRenderPane("metrics", () =>
|
|
642
|
+
renderMetricsPane(snap, {
|
|
643
|
+
registry: this.options.registry,
|
|
644
|
+
}),
|
|
645
|
+
)
|
|
646
|
+
: safeRenderPane("transcript", () => renderTranscriptPane(snap))
|
|
632
647
|
: [...readAgentPreview(r, 4, this.options), ...readProgressPreview(r, 2)];
|
|
633
648
|
const filteredPane = paneLines.filter((l) => l && !l.includes("(none)") && l.trim() !== "");
|
|
634
649
|
if (filteredPane.length > 0) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { WorkflowConfig } from "./workflow-config.ts";
|
|
2
|
+
|
|
3
|
+
// Rough token estimates per role (input + output combined)
|
|
4
|
+
const ROLE_TOKEN_ESTIMATES: Record<string, number> = {
|
|
5
|
+
explorer: 15000,
|
|
6
|
+
planner: 20000,
|
|
7
|
+
executor: 25000,
|
|
8
|
+
reviewer: 12000,
|
|
9
|
+
"security-reviewer": 12000,
|
|
10
|
+
"test-engineer": 15000,
|
|
11
|
+
analyst: 18000,
|
|
12
|
+
writer: 15000,
|
|
13
|
+
verifier: 10000,
|
|
14
|
+
critic: 12000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Rough cost per 1K tokens (USD) — using a mid-range model price
|
|
18
|
+
const COST_PER_1K_TOKENS = 0.003;
|
|
19
|
+
|
|
20
|
+
export interface CostEstimate {
|
|
21
|
+
totalTokens: number;
|
|
22
|
+
estimatedCostUSD: number;
|
|
23
|
+
perStep: Array<{ stepId: string; role: string; tokens: number }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function estimateWorkflowCost(workflow: WorkflowConfig): CostEstimate {
|
|
27
|
+
const perStep = workflow.steps.map((step) => {
|
|
28
|
+
const tokens = ROLE_TOKEN_ESTIMATES[step.role] ?? 15000; // default 15K
|
|
29
|
+
return { stepId: step.id, role: step.role, tokens };
|
|
30
|
+
});
|
|
31
|
+
const totalTokens = perStep.reduce((sum, s) => sum + s.tokens, 0);
|
|
32
|
+
const estimatedCostUSD = (totalTokens / 1000) * COST_PER_1K_TOKENS;
|
|
33
|
+
return { totalTokens, estimatedCostUSD, perStep };
|
|
34
|
+
}
|
|
@@ -18,6 +18,16 @@ export function validateWorkflowForTeam(workflow: WorkflowConfig, team: TeamConf
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Validate output field types
|
|
22
|
+
for (const step of workflow.steps) {
|
|
23
|
+
if (step.output !== undefined && step.output !== false && typeof step.output !== "string") {
|
|
24
|
+
errors.push(`Step '${step.id}' has invalid 'output' field: expected string or false, got ${typeof step.output}.`);
|
|
25
|
+
}
|
|
26
|
+
if (typeof step.output === "string" && step.output.trim() === "") {
|
|
27
|
+
errors.push(`Step '${step.id}' has empty 'output' string.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
const visiting = new Set<string>();
|
|
22
32
|
const visited = new Set<string>();
|
|
23
33
|
const byId = new Map(workflow.steps.map((step) => [step.id, step]));
|
package/tsconfig.json
CHANGED