pi-crew 0.9.22 → 0.9.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.22",
3
+ "version": "0.9.23",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  import type { AgentConfig } from "../agents/agent-config.ts";
2
2
  import { errors } from "../errors.ts";
3
- import { appendEventAsync } from "../state/event-log.ts";
3
+ import { appendEventAsync, flushEventLogBuffer } from "../state/event-log.ts";
4
4
  import type { TeamTaskState } from "../state/types.ts";
5
5
  import type { TeamConfig } from "../teams/team-config.ts";
6
6
  import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
@@ -107,142 +107,151 @@ export class PipelineRunner {
107
107
  let previousResults: unknown[] = [];
108
108
  const startTime = Date.now();
109
109
 
110
- await appendEventAsync(eventsPath, {
111
- type: "pipeline:started",
112
- runId,
113
- message: `Pipeline '${workflow.name}' started`,
114
- data: { stages: workflow.stages.map((s) => s.name) },
115
- });
116
-
117
- for (let i = 0; i < workflow.stages.length; i++) {
118
- const stage = workflow.stages[i];
119
- const stageStartTime = Date.now();
120
-
121
- // Determine stop behavior for this stage
122
- const effectiveStopOnError = stage.stopOnError ?? workflow.stopOnError ?? this.stopOnError;
123
-
110
+ try {
124
111
  await appendEventAsync(eventsPath, {
125
- type: "pipeline:stage_started",
112
+ type: "pipeline:started",
126
113
  runId,
127
- message: `Stage '${stage.name}' started`,
128
- data: { stageIndex: i, stageName: stage.name },
114
+ message: `Pipeline '${workflow.name}' started`,
115
+ data: { stages: workflow.stages.map((s) => s.name) },
129
116
  });
130
117
 
131
- try {
132
- // Build stage context
133
- const stageContext: PipelineContext = {
134
- stageIndex: i,
135
- stageName: stage.name,
136
- previousResults,
137
- totalStages: workflow.stages.length,
138
- runId,
139
- };
140
-
141
- // Resolve inputs
142
- const inputs = this.resolveInputs(stage.inputs, previousResults, context);
143
-
144
- // Execute stage (handle fan-out if enabled)
145
- const results = await this.executeStageInternal(stage, inputs, stageContext, executeStage);
118
+ for (let i = 0; i < workflow.stages.length; i++) {
119
+ const stage = workflow.stages[i];
120
+ const stageStartTime = Date.now();
146
121
 
147
- const duration = Date.now() - stageStartTime;
148
- stages.push({
149
- name: stage.name,
150
- status: "completed",
151
- results,
152
- duration,
153
- fanOutItems: Array.isArray(inputs) ? inputs.length : undefined,
154
- });
155
-
156
- previousResults = results;
122
+ // Determine stop behavior for this stage
123
+ const effectiveStopOnError = stage.stopOnError ?? workflow.stopOnError ?? this.stopOnError;
157
124
 
158
125
  await appendEventAsync(eventsPath, {
159
- type: "pipeline:stage_completed",
126
+ type: "pipeline:stage_started",
160
127
  runId,
161
- message: `Stage '${stage.name}' completed`,
162
- data: {
163
- stageIndex: i,
164
- stageName: stage.name,
165
- duration,
166
- resultCount: results.length,
167
- },
128
+ message: `Stage '${stage.name}' started`,
129
+ data: { stageIndex: i, stageName: stage.name },
168
130
  });
169
- } catch (error) {
170
- const duration = Date.now() - stageStartTime;
171
- const errorMessage = error instanceof Error ? error.message : String(error);
172
131
 
173
- if (effectiveStopOnError) {
174
- stages.push({
175
- name: stage.name,
176
- status: "failed",
177
- results: [],
178
- error: errorMessage,
179
- duration,
180
- });
181
-
182
- await appendEventAsync(eventsPath, {
183
- type: "pipeline:stage_failed",
132
+ try {
133
+ // Build stage context
134
+ const stageContext: PipelineContext = {
135
+ stageIndex: i,
136
+ stageName: stage.name,
137
+ previousResults,
138
+ totalStages: workflow.stages.length,
184
139
  runId,
185
- message: `Stage '${stage.name}' failed: ${errorMessage}`,
186
- data: {
187
- stageIndex: i,
188
- stageName: stage.name,
189
- duration,
190
- error: errorMessage,
191
- },
192
- });
140
+ };
193
141
 
194
- await appendEventAsync(eventsPath, {
195
- type: "pipeline:failed",
196
- runId,
197
- message: `Pipeline '${workflow.name}' failed at stage '${stage.name}'`,
198
- data: { failedStage: stage.name, error: errorMessage },
199
- });
142
+ // Resolve inputs
143
+ const inputs = this.resolveInputs(stage.inputs, previousResults, context);
200
144
 
201
- return {
202
- stages,
203
- totalDuration: Date.now() - startTime,
204
- finalResults: previousResults,
205
- status: "failed",
206
- };
207
- } else {
145
+ // Execute stage (handle fan-out if enabled)
146
+ const results = await this.executeStageInternal(stage, inputs, stageContext, executeStage);
147
+
148
+ const duration = Date.now() - stageStartTime;
208
149
  stages.push({
209
150
  name: stage.name,
210
- status: "failed",
211
- results: [],
212
- error: errorMessage,
151
+ status: "completed",
152
+ results,
213
153
  duration,
154
+ fanOutItems: Array.isArray(inputs) ? inputs.length : undefined,
214
155
  });
215
156
 
157
+ previousResults = results;
158
+
216
159
  await appendEventAsync(eventsPath, {
217
- type: "pipeline:stage_skipped",
160
+ type: "pipeline:stage_completed",
218
161
  runId,
219
- message: `Stage '${stage.name}' skipped due to error`,
162
+ message: `Stage '${stage.name}' completed`,
220
163
  data: {
221
164
  stageIndex: i,
222
165
  stageName: stage.name,
223
166
  duration,
224
- error: errorMessage,
167
+ resultCount: results.length,
225
168
  },
226
169
  });
170
+ } catch (error) {
171
+ const duration = Date.now() - stageStartTime;
172
+ const errorMessage = error instanceof Error ? error.message : String(error);
173
+
174
+ if (effectiveStopOnError) {
175
+ stages.push({
176
+ name: stage.name,
177
+ status: "failed",
178
+ results: [],
179
+ error: errorMessage,
180
+ duration,
181
+ });
182
+
183
+ await appendEventAsync(eventsPath, {
184
+ type: "pipeline:stage_failed",
185
+ runId,
186
+ message: `Stage '${stage.name}' failed: ${errorMessage}`,
187
+ data: {
188
+ stageIndex: i,
189
+ stageName: stage.name,
190
+ duration,
191
+ error: errorMessage,
192
+ },
193
+ });
194
+
195
+ await appendEventAsync(eventsPath, {
196
+ type: "pipeline:failed",
197
+ runId,
198
+ message: `Pipeline '${workflow.name}' failed at stage '${stage.name}'`,
199
+ data: { failedStage: stage.name, error: errorMessage },
200
+ });
201
+
202
+ return {
203
+ stages,
204
+ totalDuration: Date.now() - startTime,
205
+ finalResults: previousResults,
206
+ status: "failed",
207
+ };
208
+ } else {
209
+ stages.push({
210
+ name: stage.name,
211
+ status: "failed",
212
+ results: [],
213
+ error: errorMessage,
214
+ duration,
215
+ });
216
+
217
+ await appendEventAsync(eventsPath, {
218
+ type: "pipeline:stage_skipped",
219
+ runId,
220
+ message: `Stage '${stage.name}' skipped due to error`,
221
+ data: {
222
+ stageIndex: i,
223
+ stageName: stage.name,
224
+ duration,
225
+ error: errorMessage,
226
+ },
227
+ });
228
+ }
227
229
  }
228
230
  }
229
- }
230
231
 
231
- await appendEventAsync(eventsPath, {
232
- type: "pipeline:completed",
233
- runId,
234
- message: `Pipeline '${workflow.name}' completed`,
235
- data: {
236
- stages: stages.map((s) => ({ name: s.name, status: s.status })),
237
- },
238
- });
232
+ await appendEventAsync(eventsPath, {
233
+ type: "pipeline:completed",
234
+ runId,
235
+ message: `Pipeline '${workflow.name}' completed`,
236
+ data: {
237
+ stages: stages.map((s) => ({ name: s.name, status: s.status })),
238
+ },
239
+ });
239
240
 
240
- return {
241
- stages,
242
- totalDuration: Date.now() - startTime,
243
- finalResults: previousResults,
244
- status: stages.some((s) => s.status === "failed") ? "partial" : "completed",
245
- };
241
+ return {
242
+ stages,
243
+ totalDuration: Date.now() - startTime,
244
+ finalResults: previousResults,
245
+ status: stages.some((s) => s.status === "failed") ? "partial" : "completed",
246
+ };
247
+ } finally {
248
+ // FIX (P0 follow-up): Drain the event-log buffer before returning so
249
+ // --test-force-exit does not detect "Promise resolution is still
250
+ // pending". The buffered flush uses a 20ms ref'd timer that fires
251
+ // asynchronously; without this explicit drain, the test runner sees
252
+ // pending appendEventAsync promises and cancels the test file.
253
+ await flushEventLogBuffer(eventsPath);
254
+ }
246
255
  }
247
256
 
248
257
  /**
@@ -1013,6 +1013,30 @@ process.on("exit", () => {
1013
1013
  }
1014
1014
  asyncQueues.clear();
1015
1015
  });
1016
+
1017
+ // FIX (P0 follow-up): Drain buffered events on `beforeExit` (async-aware).
1018
+ // The `exit` handler above is sync-only and cannot await the flush, leaving
1019
+ // pending promises that the test runner detects under --test-force-exit as
1020
+ // "Promise resolution is still pending but the event loop has already
1021
+ // resolved". `beforeExit` fires when the event loop drains naturally and
1022
+ // supports async handlers, so we can await the flush here and let the loop
1023
+ // drain cleanly before process.exit() is called.
1024
+ process.on("beforeExit", async () => {
1025
+ if (bufferedQueues.size > 0) {
1026
+ try {
1027
+ await flushEventLogBuffer();
1028
+ } catch {
1029
+ /* best-effort */
1030
+ }
1031
+ }
1032
+ if (asyncQueues.size > 0) {
1033
+ try {
1034
+ await drainAsyncQueues();
1035
+ } catch {
1036
+ /* best-effort */
1037
+ }
1038
+ }
1039
+ });
1016
1040
  process.on("SIGTERM", () => setImmediate(() => flushEventLogBuffer()));
1017
1041
  process.on("SIGINT", () => setImmediate(() => flushEventLogBuffer()));
1018
1042
  // FIX (Issue 1): Handle uncaught exceptions to flush buffered events before