pi-crew 0.9.49 → 0.9.50

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/build-meta.json +50 -32
  3. package/dist/index.mjs +75 -42
  4. package/dist/index.mjs.map +4 -4
  5. package/package.json +1 -2
  6. package/skills/distill-persona/SKILL.md +83 -145
  7. package/skills/distill-persona/references/cross-skill-differentiation.md +12 -0
  8. package/skills/distill-persona/references/description-discipline.md +6 -0
  9. package/skills/distill-persona/references/diagnostic-path.md +25 -0
  10. package/skills/distill-persona/references/fidelity-rubric.md +19 -0
  11. package/skills/distill-persona/references/field-models.md +20 -0
  12. package/skills/distill-persona/references/optional-body-sections.md +9 -0
  13. package/skills/distill-persona/references/registry-routing.md +11 -0
  14. package/skills/distill-persona/references/self-upgrade-directive.md +20 -0
  15. package/skills/distill-persona/references/taste-principles.md +8 -0
  16. package/skills/distill-persona/references/topic-variant.md +13 -0
  17. package/skills/distill-persona/references/update-mode.md +7 -0
  18. package/skills/distill-persona/scripts/validate-run.mjs +297 -0
  19. package/skills/distill-software/SKILL.md +151 -90
  20. package/skills/research/SKILL.md +1 -1
  21. package/src/extension/crew-cleanup.ts +18 -1
  22. package/src/extension/crew-vibes/index.ts +11 -2
  23. package/src/extension/register.ts +1 -1
  24. package/src/extension/registration/command-registration.ts +1 -0
  25. package/src/extension/registration/commands.ts +7 -3
  26. package/src/extension/registration/lifecycle-handlers.ts +1 -3
  27. package/src/extension/registration/ui.ts +4 -0
  28. package/src/extension/registration/viewers.ts +3 -0
  29. package/src/extension/team-tool/run.ts +7 -6
  30. package/src/runtime/chain-runner.ts +3 -2
  31. package/src/runtime/pipeline-runner.ts +8 -7
  32. package/src/ui/live-run-sidebar.ts +2 -0
  33. package/src/ui/mascot.ts +11 -9
  34. package/src/ui/render-coalescer.ts +9 -0
  35. package/src/ui/run-snapshot-cache.ts +10 -11
  36. package/src/ui/terminal-status.ts +5 -0
  37. package/src/ui/widget/index.ts +3 -5
  38. package/src/ui/widget/widget-types.ts +0 -1
  39. package/assets/runner-spritesheet.png +0 -0
@@ -15,6 +15,7 @@ import { assertCleanLeaderAsync, findGitRootAsync } from "../../worktree/worktre
15
15
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- type-only import for TS inference
16
16
  const _typeCheck: typeof ExecuteTeamRunFn = null as never as typeof ExecuteTeamRunFn;
17
17
 
18
+ import { errorMessage } from "../../utils/guards.ts";
18
19
  import { logInternalError } from "../../utils/internal-error.ts";
19
20
  import { resolveContainedPath, resolveRealContainedPath } from "../../utils/safe-paths.ts";
20
21
 
@@ -432,7 +433,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
432
433
  try {
433
434
  await assertCleanLeaderAsync(gitRoot);
434
435
  } catch (err) {
435
- const msg = err instanceof Error ? err.message : String(err);
436
+ const msg = errorMessage(err);
436
437
  return result(
437
438
  `${msg}\nCommit or stash changes before using worktree mode, or use workspaceMode: 'single'.`,
438
439
  { action: "run", status: "error" },
@@ -655,7 +656,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
655
656
  // Round-11 runtime fix: persist manifest with status=failed when runner throws
656
657
  // (e.g., script timeout, script syntax error, async failure). Previously the
657
658
  // manifest stayed at 'queued' indefinitely, leaving an orphan state file.
658
- const failureReason = runnerError instanceof Error ? runnerError.message : String(runnerError);
659
+ const failureReason = errorMessage(runnerError);
659
660
  const failedManifest = {
660
661
  ...dwfManifest,
661
662
  status: "failed" as const,
@@ -856,13 +857,13 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
856
857
  workspaceId: ctx.sessionId ?? ctx.cwd,
857
858
  });
858
859
  } catch (waitError: unknown) {
859
- const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
860
+ const waitErrMsg = errorMessage(waitError);
860
861
  return result(
861
862
  [
862
863
  `pi-crew run timed out or failed: ${updatedManifest.runId}`,
863
864
  `Team: ${team.name}`,
864
865
  `Workflow: ${workflow.name}`,
865
- `Error: ${errorMessage}`,
866
+ `Error: ${waitErrMsg}`,
866
867
  "",
867
868
  `Check status with: team status runId=${updatedManifest.runId}`,
868
869
  `State: ${updatedManifest.stateRoot}`,
@@ -970,13 +971,13 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
970
971
  workspaceId: ctx.sessionId ?? ctx.cwd,
971
972
  });
972
973
  } catch (waitError: unknown) {
973
- const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
974
+ const waitErrMsg = errorMessage(waitError);
974
975
  return result(
975
976
  [
976
977
  `pi-crew run timed out or failed: ${updatedManifest.runId}`,
977
978
  `Team: ${team.name}`,
978
979
  `Workflow: ${workflow.name}`,
979
- `Error: ${errorMessage}`,
980
+ `Error: ${waitErrMsg}`,
980
981
  "",
981
982
  `Check status with: team status runId=${updatedManifest.runId}`,
982
983
  `State: ${updatedManifest.stateRoot}`,
@@ -10,6 +10,7 @@
10
10
  * @see docs/pi-boomerang-integration-plan.md
11
11
  */
12
12
 
13
+ import { errorMessage } from "../utils/guards.ts";
13
14
  import type { ChainStep as DSLChainStep } from "./chain-parser.ts";
14
15
  import { parseChainDSL } from "./chain-parser.ts";
15
16
  import type { HandoffManager, HandoffSummary, TaskPacket, TaskResult } from "./handoff-manager.ts";
@@ -250,14 +251,14 @@ export class ChainRunner {
250
251
  });
251
252
  }
252
253
  } catch (error) {
253
- const errorMessage = error instanceof Error ? error.message : String(error);
254
+ const errMsg = errorMessage(error);
254
255
 
255
256
  stepResults.push({
256
257
  step: i + 1,
257
258
  name: step.name,
258
259
  outcome: "failure",
259
260
  duration: Date.now() - stepStart,
260
- error: errorMessage,
261
+ error: errMsg,
261
262
  });
262
263
 
263
264
  // Stop chain on failure unless configured to continue
@@ -1,5 +1,6 @@
1
1
  import { errors } from "../errors.ts";
2
2
  import { appendEventAsync, flushEventLogBuffer } from "../state/event-log.ts";
3
+ import { errorMessage } from "../utils/guards.ts";
3
4
  import type { WorkflowConfig } from "../workflows/workflow-config.ts";
4
5
  import { mapConcurrent } from "./parallel-utils.ts";
5
6
 
@@ -166,26 +167,26 @@ export class PipelineRunner {
166
167
  });
167
168
  } catch (error) {
168
169
  const duration = Date.now() - stageStartTime;
169
- const errorMessage = error instanceof Error ? error.message : String(error);
170
+ const errMsg = errorMessage(error);
170
171
 
171
172
  if (effectiveStopOnError) {
172
173
  stages.push({
173
174
  name: stage.name,
174
175
  status: "failed",
175
176
  results: [],
176
- error: errorMessage,
177
+ error: errMsg,
177
178
  duration,
178
179
  });
179
180
 
180
181
  await appendEventAsync(eventsPath, {
181
182
  type: "pipeline:stage_failed",
182
183
  runId,
183
- message: `Stage '${stage.name}' failed: ${errorMessage}`,
184
+ message: `Stage '${stage.name}' failed: ${errMsg}`,
184
185
  data: {
185
186
  stageIndex: i,
186
187
  stageName: stage.name,
187
188
  duration,
188
- error: errorMessage,
189
+ error: errMsg,
189
190
  },
190
191
  });
191
192
 
@@ -193,7 +194,7 @@ export class PipelineRunner {
193
194
  type: "pipeline:failed",
194
195
  runId,
195
196
  message: `Pipeline '${workflow.name}' failed at stage '${stage.name}'`,
196
- data: { failedStage: stage.name, error: errorMessage },
197
+ data: { failedStage: stage.name, error: errMsg },
197
198
  });
198
199
 
199
200
  return {
@@ -207,7 +208,7 @@ export class PipelineRunner {
207
208
  name: stage.name,
208
209
  status: "failed",
209
210
  results: [],
210
- error: errorMessage,
211
+ error: errMsg,
211
212
  duration,
212
213
  });
213
214
 
@@ -219,7 +220,7 @@ export class PipelineRunner {
219
220
  stageIndex: i,
220
221
  stageName: stage.name,
221
222
  duration,
222
- error: errorMessage,
223
+ error: errMsg,
223
224
  },
224
225
  });
225
226
  }
@@ -270,10 +270,12 @@ export class LiveRunSidebar {
270
270
  if (isTerminal && !hasActiveAgents && !this.hasAutoClosed) {
271
271
  const autoCloseMs = this.config?.autoCloseDashboardMs ?? 3000;
272
272
  if (autoCloseMs > 0) {
273
+ if (this.autoCloseTimeout) clearTimeout(this.autoCloseTimeout);
273
274
  this.autoCloseTimeout = setTimeout(() => {
274
275
  this.hasAutoClosed = true;
275
276
  this.done(undefined);
276
277
  }, autoCloseMs);
278
+ this.autoCloseTimeout?.unref();
277
279
  lines.push(line(`auto-close in ${Math.round(autoCloseMs / 1000)}s…`, w));
278
280
  }
279
281
  }
package/src/ui/mascot.ts CHANGED
@@ -226,7 +226,7 @@ export class AnimatedMascot {
226
226
  private tickTypewriter(): boolean {
227
227
  const state = this.effectState;
228
228
  if (state.pos === undefined) return true;
229
- for (let i = 0; i < 6; i++) {
229
+ for (let i = 0; i < 18; i++) {
230
230
  const row = Math.floor(state.pos / ARMIN_WIDTH);
231
231
  const x = state.pos % ARMIN_WIDTH;
232
232
  if (row >= ARMIN_DISPLAY_HEIGHT) return true;
@@ -239,9 +239,11 @@ export class AnimatedMascot {
239
239
  private tickScanline(): boolean {
240
240
  const state = this.effectState;
241
241
  if (state.row === undefined) return true;
242
- if (state.row >= ARMIN_DISPLAY_HEIGHT) return true;
243
- for (let x = 0; x < ARMIN_WIDTH; x++) this.currentArminGrid[state.row][x] = this.finalArminGrid[state.row][x];
244
- state.row++;
242
+ for (let step = 0; step < 3; step++) {
243
+ if (state.row >= ARMIN_DISPLAY_HEIGHT) return true;
244
+ for (let x = 0; x < ARMIN_WIDTH; x++) this.currentArminGrid[state.row][x] = this.finalArminGrid[state.row][x];
245
+ state.row++;
246
+ }
245
247
  return false;
246
248
  }
247
249
 
@@ -264,7 +266,7 @@ export class AnimatedMascot {
264
266
  break;
265
267
  }
266
268
  }
267
- drop.y++;
269
+ drop.y += 3;
268
270
  if (drop.y >= 0 && drop.y < ARMIN_DISPLAY_HEIGHT) {
269
271
  if (targetRow >= 0 && drop.y >= targetRow) {
270
272
  drop.settled = ARMIN_DISPLAY_HEIGHT - targetRow;
@@ -280,7 +282,7 @@ export class AnimatedMascot {
280
282
  private tickFade(): boolean {
281
283
  const state = this.effectState;
282
284
  if (!state.positions || state.idx === undefined) return true;
283
- for (let i = 0; i < 18; i++) {
285
+ for (let i = 0; i < 54; i++) {
284
286
  if (state.idx >= state.positions.length) return true;
285
287
  const [row, x] = state.positions[state.idx];
286
288
  this.currentArminGrid[row][x] = this.finalArminGrid[row][x];
@@ -299,7 +301,7 @@ export class AnimatedMascot {
299
301
  for (let row = Math.max(0, top); row <= Math.min(ARMIN_DISPLAY_HEIGHT - 1, bottom); row++) {
300
302
  for (let x = 0; x < ARMIN_WIDTH; x++) this.currentArminGrid[row][x] = this.finalArminGrid[row][x];
301
303
  }
302
- state.expansion++;
304
+ state.expansion += 3;
303
305
  return state.expansion > ARMIN_DISPLAY_HEIGHT;
304
306
  }
305
307
 
@@ -321,7 +323,7 @@ export class AnimatedMascot {
321
323
  }
322
324
  }
323
325
  }
324
- state.phase++;
326
+ state.phase += 3;
325
327
  return false;
326
328
  }
327
329
  // Restore final grid in-place
@@ -336,7 +338,7 @@ export class AnimatedMascot {
336
338
  private tickDissolve(): boolean {
337
339
  const state = this.effectState;
338
340
  if (!state.positions || state.idx === undefined) return true;
339
- for (let i = 0; i < 22; i++) {
341
+ for (let i = 0; i < 66; i++) {
340
342
  if (state.idx >= state.positions.length) return true;
341
343
  const [row, x] = state.positions[state.idx];
342
344
  this.currentArminGrid[row][x] = this.finalArminGrid[row][x];
@@ -73,6 +73,15 @@ export class RenderCoalescer {
73
73
  this.#timerId = null;
74
74
  }
75
75
  this.#pending = false;
76
+ const dropped = this.#dropped;
77
+ this.#dropped = 0;
78
+ if (dropped > 0) {
79
+ try {
80
+ this.#onDrop(dropped);
81
+ } catch {
82
+ /* drop callback errors are non-fatal */
83
+ }
84
+ }
76
85
  this.#callback();
77
86
  }
78
87
 
@@ -1005,17 +1005,16 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
1005
1005
  const scheduleRefresh = (runId: string): void => {
1006
1006
  const existing = pendingRefreshes.get(runId);
1007
1007
  if (existing) clearTimeout(existing);
1008
- pendingRefreshes.set(
1009
- runId,
1010
- setTimeout(() => {
1011
- pendingRefreshes.delete(runId);
1012
- try {
1013
- localRefreshIfStale(runId);
1014
- } catch {
1015
- /* best-effort; widget falls back gracefully */
1016
- }
1017
- }, INVAL_COALESCE_MS),
1018
- );
1008
+ const timer = setTimeout(() => {
1009
+ pendingRefreshes.delete(runId);
1010
+ try {
1011
+ localRefreshIfStale(runId);
1012
+ } catch {
1013
+ /* best-effort; widget falls back gracefully */
1014
+ }
1015
+ }, INVAL_COALESCE_MS);
1016
+ timer.unref();
1017
+ pendingRefreshes.set(runId, timer);
1019
1018
  };
1020
1019
  const unsubState = runEventBus.onChannel("run:state", (event) => {
1021
1020
  if (entries.has(event.runId)) scheduleRefresh(event.runId);
@@ -213,6 +213,9 @@ export function createTerminalStatusController(ctx: TerminalStatusUi): TerminalS
213
213
  setTerminalTitle(ctx, buildIdleTitle());
214
214
  scheduleIdleReassert(Math.min(delay * 2, IDLE_REASSERT_MAX_MS));
215
215
  }, delay);
216
+ // Unref so this recursive backoff timer never keeps the event loop alive
217
+ // (critical on SIGTERM where dispose() is not reached → process hang).
218
+ state.idleTimer.unref?.();
216
219
  };
217
220
 
218
221
  return {
@@ -240,6 +243,8 @@ export function createTerminalStatusController(ctx: TerminalStatusUi): TerminalS
240
243
  ghosttyClear();
241
244
  }
242
245
  }, COMPLETE_FLASH_MS);
246
+ // Unref so the one-shot flash clear never blocks process exit.
247
+ state.flashTimer.unref?.();
243
248
  },
244
249
  onIdle(): void {
245
250
  if (state.destroyed) return;
@@ -154,7 +154,7 @@ class CrewWidgetComponent implements WidgetComponent {
154
154
  [...listLiveAgents()].some((h) => h.status === "running");
155
155
  const animation = hasRunning ? `:spin=${spinnerBucket()}` : "";
156
156
 
157
- return (
157
+ const sig =
158
158
  runs
159
159
  .map(
160
160
  (entry) =>
@@ -175,8 +175,8 @@ class CrewWidgetComponent implements WidgetComponent {
175
175
  })
176
176
  .join(","),
177
177
  )
178
- .join("|") + `|live:${liveSig}${animation}`
179
- );
178
+ .join("|") + `|live:${liveSig}${animation}`;
179
+ return sig;
180
180
  }
181
181
 
182
182
  private colorize(lines: string[], width: number): string[] {
@@ -342,8 +342,6 @@ export function stopCrewWidget(
342
342
  state: CrewWidgetState,
343
343
  config?: CrewUiConfig,
344
344
  ): void {
345
- if (state.interval) clearInterval(state.interval);
346
- state.interval = undefined;
347
345
  if (ctx?.hasUI) {
348
346
  const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
349
347
  ctx.ui.setStatus(STATUS_KEY, undefined);
@@ -25,7 +25,6 @@ export interface CrewWidgetModel {
25
25
 
26
26
  export interface CrewWidgetState {
27
27
  frame: number;
28
- interval?: ReturnType<typeof setInterval>;
29
28
  lastPlacement?: string;
30
29
  lastVisibility?: "hidden" | "visible";
31
30
  lastKey?: string;
Binary file