pi-crew 0.9.13 → 0.9.15
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 +114 -0
- package/README.md +66 -0
- package/docs/bugs/bug-021-notification-badge-counter-misleading.md +293 -0
- package/docs/bugs/bug-023-chain-windows-path-resolution.md +106 -0
- package/package.json +1 -1
- package/src/config/defaults.ts +3 -1
- package/src/config/types.ts +5 -0
- package/src/extension/pi-api.ts +1 -1
- package/src/extension/registration/team-tool.ts +145 -35
- package/src/extension/team-tool/run.ts +646 -150
- package/src/runtime/child-pi.ts +87 -1
- package/src/runtime/goal-achievement.ts +131 -0
- package/src/runtime/recovery-recipes.ts +35 -0
- package/src/runtime/task-runner.ts +6 -0
- package/src/runtime/team-runner.ts +1482 -310
- package/src/state/types.ts +3 -0
- package/src/ui/widget/index.ts +1 -1
- package/src/ui/widget/widget-formatters.ts +14 -1
- package/src/ui/widget/widget-renderer.ts +13 -2
- package/src/utils/redaction.ts +20 -0
- package/src/workflows/discover-workflows.ts +144 -29
- package/src/workflows/preflight-validator.ts +195 -0
- package/src/workflows/topology-analyzer.ts +219 -0
- package/src/workflows/workflow-config.ts +11 -0
- package/workflows/chain.workflow.md +6 -0
- package/workflows/default.workflow.md +1 -0
- package/workflows/fast-fix.workflow.md +1 -0
- package/workflows/implementation.workflow.md +1 -0
- package/workflows/parallel-research.workflow.md +1 -0
- package/workflows/pipeline.workflow.md +1 -0
- package/workflows/research.workflow.md +1 -0
- package/workflows/review.workflow.md +1 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// src/workflows/topology-analyzer.ts
|
|
2
|
+
//
|
|
3
|
+
// Workflow topology classifier. Given a WorkflowConfig, returns a TopologyAnalysis
|
|
4
|
+
// describing the structural shape (single / sequential / concurrent / complex-dag
|
|
5
|
+
// / dynamic). The shape is used by the preflight validator to enforce the
|
|
6
|
+
// "don't use pi-crew for sequential independent agents" rule from .crew/knowledge.md.
|
|
7
|
+
//
|
|
8
|
+
// Algorithm (high level):
|
|
9
|
+
// 1. runtime === "dynamic" → DYNAMIC (chain/script workflows decide at runtime)
|
|
10
|
+
// 2. stepCount === 1 → SINGLE (one step: no concurrency, no DAG)
|
|
11
|
+
// 3. parallelGroupCount >= 1 AND max-fanout >= 3 → CONCURRENT
|
|
12
|
+
// 4. stepCount >= 4 AND has-multi-deps → COMPLEX_DAG
|
|
13
|
+
// 5. else → SEQUENTIAL
|
|
14
|
+
//
|
|
15
|
+
// "Has-multi-deps" = at least one step lists 2+ dependsOn. This is the
|
|
16
|
+
// unambiguous signal of a branching DAG (diamond patterns, fan-in joins).
|
|
17
|
+
// Plain linear chains with high depth are still SEQUENTIAL — Run #3 fast-fix
|
|
18
|
+
// (3 steps linear, depth 3) and the default workflow (4 steps linear, depth 4)
|
|
19
|
+
// are both sequential by this rule; raw Agent calls are faster for both.
|
|
20
|
+
|
|
21
|
+
import type { WorkflowConfig, WorkflowStep } from "./workflow-config.ts";
|
|
22
|
+
|
|
23
|
+
export type WorkflowTopology =
|
|
24
|
+
| "single"
|
|
25
|
+
| "sequential"
|
|
26
|
+
| "concurrent"
|
|
27
|
+
| "complex-dag"
|
|
28
|
+
| "dynamic";
|
|
29
|
+
|
|
30
|
+
export type TopologyRecommendation =
|
|
31
|
+
| "raw_agent"
|
|
32
|
+
| "fast_fix"
|
|
33
|
+
| "parallel_research"
|
|
34
|
+
| "implementation_adaptive"
|
|
35
|
+
| "any";
|
|
36
|
+
|
|
37
|
+
export interface TopologyAnalysis {
|
|
38
|
+
topology: WorkflowTopology;
|
|
39
|
+
stepCount: number;
|
|
40
|
+
parallelGroupCount: number;
|
|
41
|
+
fanOutDegree: number;
|
|
42
|
+
dagDepth: number;
|
|
43
|
+
recommendation: TopologyRecommendation;
|
|
44
|
+
reason: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Distinct parallelGroup values across all steps. Empty if no step sets a group. */
|
|
48
|
+
export function parallelGroupsFromSteps(steps: WorkflowStep[]): Set<string> {
|
|
49
|
+
const out = new Set<string>();
|
|
50
|
+
for (const step of steps) {
|
|
51
|
+
if (step.parallelGroup !== undefined) out.add(step.parallelGroup);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Max group size across all parallelGroup values. 0 if no step sets a group. */
|
|
57
|
+
export function fanOutDegreeFromSteps(steps: WorkflowStep[]): number {
|
|
58
|
+
const counts = new Map<string, number>();
|
|
59
|
+
for (const step of steps) {
|
|
60
|
+
if (step.parallelGroup === undefined) continue;
|
|
61
|
+
counts.set(
|
|
62
|
+
step.parallelGroup,
|
|
63
|
+
(counts.get(step.parallelGroup) ?? 0) + 1,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
let max = 0;
|
|
67
|
+
for (const count of counts.values()) if (count > max) max = count;
|
|
68
|
+
return max;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Longest-path DAG depth from roots. A root is a step with no dependsOn.
|
|
73
|
+
* Returns 0 for an empty list, 1 for a list of roots only with no inter-step deps.
|
|
74
|
+
*
|
|
75
|
+
* Defensive: a step depending on an unknown id is treated as a root
|
|
76
|
+
* (validate-workflow.ts catches real cycles before this runs; this is
|
|
77
|
+
* belt-and-suspenders against malformed inputs).
|
|
78
|
+
*/
|
|
79
|
+
export function dagDepthFromSteps(steps: WorkflowStep[]): number {
|
|
80
|
+
if (steps.length === 0) return 0;
|
|
81
|
+
const ids = new Set(steps.map((s) => s.id));
|
|
82
|
+
// Build adjacency and indegree. Unknown deps are dropped to avoid phantom edges.
|
|
83
|
+
const indeg = new Map<string, number>();
|
|
84
|
+
const adj = new Map<string, string[]>();
|
|
85
|
+
for (const step of steps) {
|
|
86
|
+
const deps = (step.dependsOn ?? []).filter((d) => ids.has(d));
|
|
87
|
+
indeg.set(step.id, deps.length);
|
|
88
|
+
// step.id's parents are deps; deps's children include step.id.
|
|
89
|
+
adj.set(step.id, adj.get(step.id) ?? []);
|
|
90
|
+
for (const dep of deps) {
|
|
91
|
+
const children = adj.get(dep) ?? [];
|
|
92
|
+
children.push(step.id);
|
|
93
|
+
adj.set(dep, children);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Kahn-style BFS. depth[v] = max(depth[parent]) + 1, roots = 1.
|
|
97
|
+
const depth = new Map<string, number>();
|
|
98
|
+
const queue: string[] = [];
|
|
99
|
+
for (const [id, n] of indeg) {
|
|
100
|
+
if (n === 0) {
|
|
101
|
+
depth.set(id, 1);
|
|
102
|
+
queue.push(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let maxDepth = 0;
|
|
106
|
+
while (queue.length > 0) {
|
|
107
|
+
const v = queue.shift()!;
|
|
108
|
+
const dv = depth.get(v) ?? 0;
|
|
109
|
+
if (dv > maxDepth) maxDepth = dv;
|
|
110
|
+
for (const child of adj.get(v) ?? []) {
|
|
111
|
+
const cand = dv + 1;
|
|
112
|
+
if (cand > (depth.get(child) ?? 0)) depth.set(child, cand);
|
|
113
|
+
indeg.set(child, (indeg.get(child) ?? 0) - 1);
|
|
114
|
+
if ((indeg.get(child) ?? 0) === 0) queue.push(child);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return maxDepth;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** True if at least one step lists 2+ dependsOn entries. */
|
|
121
|
+
function hasMultiDeps(steps: WorkflowStep[]): boolean {
|
|
122
|
+
for (const step of steps) {
|
|
123
|
+
if ((step.dependsOn?.length ?? 0) >= 2) return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Classify a workflow's topology. See file header for the rule order.
|
|
130
|
+
* Pure function — no I/O, no side effects, no exceptions.
|
|
131
|
+
*/
|
|
132
|
+
export function analyzeWorkflowTopology(
|
|
133
|
+
workflow: WorkflowConfig,
|
|
134
|
+
): TopologyAnalysis {
|
|
135
|
+
// Chain/dynamic workflows: runtime decides the topology at execution time.
|
|
136
|
+
// Don't try to classify the empty `steps: []` (DynamicWorkflowConfig forces
|
|
137
|
+
// it to be empty).
|
|
138
|
+
if (workflow.runtime === "dynamic") {
|
|
139
|
+
return {
|
|
140
|
+
topology: "dynamic",
|
|
141
|
+
stepCount: 0,
|
|
142
|
+
parallelGroupCount: 0,
|
|
143
|
+
fanOutDegree: 0,
|
|
144
|
+
dagDepth: 0,
|
|
145
|
+
recommendation: "any",
|
|
146
|
+
reason: "Chain/dynamic workflow — runtime decides topology",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const steps = workflow.steps;
|
|
151
|
+
const stepCount = steps.length;
|
|
152
|
+
const parallelGroupCount = parallelGroupsFromSteps(steps).size;
|
|
153
|
+
const fanOutDegree = fanOutDegreeFromSteps(steps);
|
|
154
|
+
const dagDepth = dagDepthFromSteps(steps);
|
|
155
|
+
|
|
156
|
+
// Explicit topology override from frontmatter `topology:` field.
|
|
157
|
+
// When set, the author has already classified the workflow — respect that.
|
|
158
|
+
// We still compute structural metrics so telemetry can show the delta.
|
|
159
|
+
if (workflow.topology !== undefined && workflow.topology !== "dynamic") {
|
|
160
|
+
const recommendation: TopologyRecommendation =
|
|
161
|
+
workflow.topology === "single" || workflow.topology === "sequential"
|
|
162
|
+
? "raw_agent"
|
|
163
|
+
: workflow.topology === "concurrent"
|
|
164
|
+
? "parallel_research"
|
|
165
|
+
: workflow.topology === "complex-dag"
|
|
166
|
+
? "implementation_adaptive"
|
|
167
|
+
: "any";
|
|
168
|
+
return {
|
|
169
|
+
topology: workflow.topology,
|
|
170
|
+
stepCount,
|
|
171
|
+
parallelGroupCount,
|
|
172
|
+
fanOutDegree,
|
|
173
|
+
dagDepth,
|
|
174
|
+
recommendation,
|
|
175
|
+
reason: `Explicit topology from frontmatter: '${workflow.topology}'`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let topology: WorkflowTopology;
|
|
180
|
+
let recommendation: TopologyRecommendation;
|
|
181
|
+
let reason: string;
|
|
182
|
+
|
|
183
|
+
// Note: explicit `workflow.topology` override is handled BEFORE the variable
|
|
184
|
+
// declarations below (in the block above). If we reach here, auto-classify.
|
|
185
|
+
|
|
186
|
+
if (stepCount === 1) {
|
|
187
|
+
topology = "single";
|
|
188
|
+
recommendation = "raw_agent";
|
|
189
|
+
reason =
|
|
190
|
+
"Single-task workflow: no concurrency or DAG structure to justify pi-crew overhead.";
|
|
191
|
+
} else if (parallelGroupCount >= 1 && fanOutDegree >= 3) {
|
|
192
|
+
topology = "concurrent";
|
|
193
|
+
recommendation = "parallel_research";
|
|
194
|
+
reason = `Concurrent fan-out: ${fanOutDegree} steps in parallel group(s) of size ≥3.`;
|
|
195
|
+
} else if (stepCount >= 4 && hasMultiDeps(steps)) {
|
|
196
|
+
topology = "complex-dag";
|
|
197
|
+
recommendation = "implementation_adaptive";
|
|
198
|
+
reason = `Complex DAG: ${stepCount} steps with branching (≥2 deps on ≥1 node), depth ${dagDepth}.`;
|
|
199
|
+
} else {
|
|
200
|
+
topology = "sequential";
|
|
201
|
+
if (stepCount <= 3) {
|
|
202
|
+
recommendation = "raw_agent";
|
|
203
|
+
reason = `Sequential chain of ${stepCount} steps — raw Agent calls are faster.`;
|
|
204
|
+
} else {
|
|
205
|
+
recommendation = "fast_fix";
|
|
206
|
+
reason = `Sequential chain of ${stepCount} steps — audit-trail-justified, but raw calls remain faster.`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
topology,
|
|
212
|
+
stepCount,
|
|
213
|
+
parallelGroupCount,
|
|
214
|
+
fanOutDegree,
|
|
215
|
+
dagDepth,
|
|
216
|
+
recommendation,
|
|
217
|
+
reason,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -48,6 +48,17 @@ export interface WorkflowConfig {
|
|
|
48
48
|
/** For runtime:"dynamic" — per-workflow token budget. When set, ctx.agent() auto-rejects with
|
|
49
49
|
* ok:false once exhausted. Accumulated from each agent run's reported usage. */
|
|
50
50
|
maxTokenBudget?: number;
|
|
51
|
+
/** Explicit topology classification from frontmatter `topology:` field.
|
|
52
|
+
* When set, overrides the auto-classified topology in analyzeWorkflowTopology().
|
|
53
|
+
* Used by preflight-validator to enforce "don't use pi-crew for sequential chains".
|
|
54
|
+
* Valid values: 'single' | 'sequential' | 'concurrent' | 'complex-dag' | 'dynamic'.
|
|
55
|
+
* Absent = auto-classify from step structure (default). */
|
|
56
|
+
topology?:
|
|
57
|
+
| "single"
|
|
58
|
+
| "sequential"
|
|
59
|
+
| "concurrent"
|
|
60
|
+
| "complex-dag"
|
|
61
|
+
| "dynamic";
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
/** A dynamic workflow (runtime === "dynamic"). steps is empty — the script is the source of truth. */
|