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.
@@ -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. */
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: chain
3
+ description: Sequential execution with context passing
4
+ topology: dynamic
5
+ ---
6
+
1
7
  # Chain Workflow - Sequential execution with context passing
2
8
 
3
9
  **Source:** `docs/pi-boomerang-integration-plan.md`
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: default
3
3
  description: Explore, plan, execute, and verify
4
+ topology: sequential
4
5
  ---
5
6
 
6
7
  ## explore
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: fast-fix
3
3
  description: Minimal workflow for small fixes
4
+ topology: sequential
4
5
  ---
5
6
 
6
7
  ## explore
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: implementation
3
3
  description: Adaptive implementation workflow where a planner agent decides the subagent fanout
4
+ topology: complex-dag
4
5
  ---
5
6
 
6
7
  ## assess
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: parallel-research
3
3
  description: Parallel research with shard exploration and synthesis
4
+ topology: concurrent
4
5
  ---
5
6
 
6
7
  ## discover
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: pipeline
3
3
  description: Multi-stage pipeline with automatic fan-out for array inputs
4
+ topology: sequential
4
5
  ---
5
6
 
6
7
  ## Stage 1: Research
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: research
3
3
  description: Research and write up findings
4
+ topology: sequential
4
5
  ---
5
6
 
6
7
  ## explore
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: review
3
3
  description: Review workflow for correctness and security
4
+ topology: concurrent
4
5
  ---
5
6
 
6
7
  ## explore