pi-crew 0.9.49 → 0.9.51

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 (44) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/dist/build-meta.json +84 -57
  3. package/dist/index.mjs +224 -96
  4. package/dist/index.mjs.map +4 -4
  5. package/docs/decisions/2026-07-26-c6-mascot-visibility-not-wired.md +84 -0
  6. package/package.json +1 -2
  7. package/skills/distill-persona/SKILL.md +83 -145
  8. package/skills/distill-persona/references/cross-skill-differentiation.md +12 -0
  9. package/skills/distill-persona/references/description-discipline.md +6 -0
  10. package/skills/distill-persona/references/diagnostic-path.md +25 -0
  11. package/skills/distill-persona/references/fidelity-rubric.md +19 -0
  12. package/skills/distill-persona/references/field-models.md +20 -0
  13. package/skills/distill-persona/references/optional-body-sections.md +9 -0
  14. package/skills/distill-persona/references/registry-routing.md +11 -0
  15. package/skills/distill-persona/references/self-upgrade-directive.md +20 -0
  16. package/skills/distill-persona/references/taste-principles.md +8 -0
  17. package/skills/distill-persona/references/topic-variant.md +13 -0
  18. package/skills/distill-persona/references/update-mode.md +7 -0
  19. package/skills/distill-persona/scripts/validate-run.mjs +297 -0
  20. package/skills/distill-software/SKILL.md +174 -90
  21. package/skills/research/SKILL.md +1 -1
  22. package/src/extension/crew-cleanup.ts +18 -1
  23. package/src/extension/crew-vibes/index.ts +11 -2
  24. package/src/extension/register.ts +1 -1
  25. package/src/extension/registration/command-registration.ts +1 -0
  26. package/src/extension/registration/commands.ts +7 -3
  27. package/src/extension/registration/lifecycle-handlers.ts +1 -3
  28. package/src/extension/registration/ui.ts +4 -0
  29. package/src/extension/registration/viewers.ts +3 -0
  30. package/src/extension/team-tool/run.ts +7 -6
  31. package/src/runtime/chain-runner.ts +3 -2
  32. package/src/runtime/pipeline-runner.ts +8 -7
  33. package/src/ui/live-run-sidebar.ts +7 -13
  34. package/src/ui/loaders.ts +6 -176
  35. package/src/ui/mascot.ts +25 -10
  36. package/src/ui/render-coalescer.ts +9 -0
  37. package/src/ui/render-scheduler.ts +60 -6
  38. package/src/ui/run-dashboard.ts +12 -21
  39. package/src/ui/run-snapshot-cache.ts +10 -11
  40. package/src/ui/shared-overlay-scheduler.ts +96 -0
  41. package/src/ui/terminal-status.ts +5 -0
  42. package/src/ui/widget/index.ts +48 -16
  43. package/src/ui/widget/widget-types.ts +0 -1
  44. package/assets/runner-spritesheet.png +0 -0
@@ -20,8 +20,8 @@ import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
20
20
  import { DynamicCrewBorder } from "./dynamic-border.ts";
21
21
  import { dashboardActionForKey } from "./keybinding-map.ts";
22
22
  import { HelpOverlay } from "./overlays/help-overlay.ts";
23
- import { RenderScheduler } from "./render-scheduler.ts";
24
- import { runEventBusAsRenderScheduler } from "./run-event-bus.ts";
23
+ import type { OverlaySchedulerHandle } from "./shared-overlay-scheduler.ts";
24
+ import { registerOverlayScheduler } from "./shared-overlay-scheduler.ts";
25
25
  import type { RunSnapshotCache, RunUiSnapshot } from "./snapshot-types.ts";
26
26
  import { spinnerBucket, spinnerFrame } from "./spinner.ts";
27
27
  import { applyStatusColor, colorizeStatusGlyphs, iconForStatus, type RunStatus } from "./status-colors.ts";
@@ -423,7 +423,7 @@ export class RunDashboard implements DashboardComponent {
423
423
  private cachedSignatureAt = 0;
424
424
  private cachedSignature = "";
425
425
  private readonly unsubscribeTheme: () => void;
426
- private readonly renderScheduler: RenderScheduler | undefined;
426
+ private readonly schedulerHandle: OverlaySchedulerHandle | undefined;
427
427
 
428
428
  constructor(
429
429
  runs: TeamRunManifest[],
@@ -466,21 +466,12 @@ export class RunDashboard implements DashboardComponent {
466
466
  const renderTick = (): void => {
467
467
  this.invalidateAndRender();
468
468
  };
469
- this.renderScheduler = new RenderScheduler(
470
- runEventBusAsRenderScheduler(["run:state", "worker:lifecycle", "ui:invalidate"]),
471
- renderTick,
472
- {
473
- debounceMs: 75,
474
- fallbackMs: 750,
475
- events: ["run:state", "worker:lifecycle", "ui:invalidate"],
476
- onInvalidate: () => {
477
- // Drop any cached signature — data underneath may have
478
- // changed so the next buildSignature() needs to recompute.
479
- this.cachedSignature = "";
480
- this.cachedSignatureAt = 0;
481
- },
482
- },
483
- );
469
+ this.schedulerHandle = registerOverlayScheduler(renderTick, () => {
470
+ // Drop any cached signature — data underneath may have
471
+ // changed so the next buildSignature() needs to recompute.
472
+ this.cachedSignature = "";
473
+ this.cachedSignatureAt = 0;
474
+ });
484
475
  // FIND-07: route theme changes through the scheduler so they coalesce
485
476
  // with run:state / worker:lifecycle / ui:invalidate bursts instead of
486
477
  // each firing their own immediate invalidate+requestRender.
@@ -498,8 +489,8 @@ export class RunDashboard implements DashboardComponent {
498
489
  * (defensive — currently always created in the constructor).
499
490
  */
500
491
  private scheduleRender(): void {
501
- if (this.renderScheduler) {
502
- this.renderScheduler.schedule();
492
+ if (this.schedulerHandle) {
493
+ this.schedulerHandle.schedule();
503
494
  return;
504
495
  }
505
496
  this.invalidateAndRender();
@@ -619,7 +610,7 @@ export class RunDashboard implements DashboardComponent {
619
610
 
620
611
  dispose(): void {
621
612
  this.unsubscribeTheme();
622
- this.renderScheduler?.dispose();
613
+ this.schedulerHandle?.dispose();
623
614
  }
624
615
 
625
616
  private selectedRunId(): string | undefined {
@@ -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);
@@ -0,0 +1,96 @@
1
+ import { logInternalError } from "../utils/internal-error.ts";
2
+ import { RenderScheduler } from "./render-scheduler.ts";
3
+ import { runEventBusAsRenderScheduler } from "./run-event-bus.ts";
4
+
5
+ export interface OverlaySchedulerHandle {
6
+ /** Force a debounced re-render fan-out (e.g. theme change, dashboard interaction). */
7
+ schedule(): void;
8
+ /** Unregister this overlay. Disposes the shared scheduler when the last overlay leaves (ref-count). */
9
+ dispose(): void;
10
+ }
11
+
12
+ /**
13
+ * C3 (Option G): the 3 overlays (widget + sidebar + dashboard) each used to
14
+ * instantiate an identical RenderScheduler (same run-event-bus channels,
15
+ * debounce 75ms, fallback 750ms) → 9 subscriptions + 9 live timers + 3×
16
+ * per-event schedule() CPU during active runs. This module owns ONE shared
17
+ * RenderScheduler (module singleton) whose `render` and `onInvalidate`
18
+ * fan out to all registered overlays, with per-callback try/catch isolation
19
+ * + ref-counted disposal (the underlying scheduler is disposed when the last
20
+ * overlay leaves, and a future register recreates it).
21
+ */
22
+
23
+ const CHANNELS = ["run:state", "worker:lifecycle", "ui:invalidate"] as const;
24
+
25
+ interface SharedOverlayState {
26
+ readonly scheduler: RenderScheduler;
27
+ readonly renderers: Set<() => void>;
28
+ readonly invalidators: Set<(payload: unknown) => void>;
29
+ }
30
+
31
+ let shared: SharedOverlayState | undefined;
32
+
33
+ function ensureShared(): SharedOverlayState {
34
+ if (shared) return shared;
35
+ const renderers = new Set<() => void>();
36
+ const invalidators = new Set<(payload: unknown) => void>();
37
+ const scheduler = new RenderScheduler(
38
+ runEventBusAsRenderScheduler([...CHANNELS]),
39
+ () => {
40
+ // Fan-out: each registered overlay's render runs in its own
41
+ // try/catch so one failing component cannot break the others.
42
+ for (const render of renderers) {
43
+ try {
44
+ render();
45
+ } catch (error) {
46
+ logInternalError("shared-overlay-scheduler.render", error);
47
+ }
48
+ }
49
+ },
50
+ {
51
+ debounceMs: 75,
52
+ fallbackMs: 750,
53
+ events: [...CHANNELS],
54
+ onInvalidate: (payload) => {
55
+ for (const invalidate of invalidators) {
56
+ try {
57
+ invalidate(payload);
58
+ } catch (error) {
59
+ logInternalError("shared-overlay-scheduler.invalidate", error);
60
+ }
61
+ }
62
+ },
63
+ },
64
+ );
65
+ shared = { scheduler, renderers, invalidators };
66
+ return shared;
67
+ }
68
+
69
+ /**
70
+ * Register an overlay's render (+ optional onInvalidate) with the shared
71
+ * scheduler. Returns a handle that lets the overlay force a debounced
72
+ * fan-out (schedule) and unregister itself (dispose). The underlying shared
73
+ * scheduler is created lazily on first register and disposed when the last
74
+ * overlay disposes.
75
+ */
76
+ export function registerOverlayScheduler(render: () => void, onInvalidate?: (payload: unknown) => void): OverlaySchedulerHandle {
77
+ const state = ensureShared();
78
+ state.renderers.add(render);
79
+ if (onInvalidate) state.invalidators.add(onInvalidate);
80
+ return {
81
+ schedule: () => {
82
+ state.scheduler.schedule();
83
+ },
84
+ dispose: () => {
85
+ state.renderers.delete(render);
86
+ if (onInvalidate) state.invalidators.delete(onInvalidate);
87
+ // Ref-count: when the last overlay leaves, dispose the underlying
88
+ // scheduler (clears subscriptions + timers) and reset the module
89
+ // variable so a future register recreates a fresh instance.
90
+ if (state.renderers.size === 0) {
91
+ state.scheduler.dispose();
92
+ if (shared === state) shared = undefined;
93
+ }
94
+ },
95
+ };
96
+ }
@@ -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;
@@ -12,8 +12,8 @@ import type { ManifestCache } from "../../runtime/manifest-cache.ts";
12
12
  import type { TeamRunManifest } from "../../state/types.ts";
13
13
  import { truncate } from "../../utils/visual.ts";
14
14
  import { requestRender, requestRenderTarget, setExtensionWidget } from "../pi-ui-compat.ts";
15
- import { RenderScheduler } from "../render-scheduler.ts";
16
- import { runEventBusAsRenderScheduler } from "../run-event-bus.ts";
15
+ import type { OverlaySchedulerHandle } from "../shared-overlay-scheduler.ts";
16
+ import { registerOverlayScheduler } from "../shared-overlay-scheduler.ts";
17
17
  import type { RunSnapshotCache } from "../snapshot-types.ts";
18
18
  import { spinnerBucket, spinnerFrame } from "../spinner.ts";
19
19
  import type { CrewTheme } from "../theme-adapter.ts";
@@ -64,6 +64,15 @@ const LEGACY_WIDGET_KEY = "pi-crew";
64
64
  const WIDGET_KEY = "pi-crew-active";
65
65
  const STATUS_KEY = "pi-crew";
66
66
 
67
+ /**
68
+ * C4 — short-TTL safety net for the buildSignature() result cache. Combined
69
+ * with invalidate-on-write (onInvalidate clears the cache on every real
70
+ * event), this prevents redundant O(runs×agents) signature recomputation
71
+ * during a burst of host-driven renders within one state snapshot while
72
+ * ensuring the signature is always recomputed after genuine data changes.
73
+ */
74
+ const SIGNATURE_CACHE_TTL_MS = 100;
75
+
67
76
  // ── Terminal resize handling (T-2) ────────────────────────────────────
68
77
  // On a terminal resize the widget's cached render width goes stale until the
69
78
  // next invalidate; a mid-run resize could briefly paint a frame at the old
@@ -104,13 +113,16 @@ class CrewWidgetComponent implements WidgetComponent {
104
113
  private readonly model: CrewWidgetModel;
105
114
  private theme: CrewTheme;
106
115
  private cacheSignature = "";
116
+ /** C4 — invalidate-on-write cache for the buildSignature() result. */
117
+ private cachedBuildSignature = "";
118
+ private cachedBuildSignatureAt = 0;
107
119
  private cachedWidth = 0;
108
120
  private cachedLines: string[] = [];
109
121
  private cachedBaseLines: string[] = [];
110
122
  private cachedTheme: CrewTheme;
111
123
  private readonly tui: unknown;
112
124
  private readonly unsubscribeTheme: () => void;
113
- private readonly renderScheduler: RenderScheduler;
125
+ private readonly schedulerHandle: OverlaySchedulerHandle;
114
126
 
115
127
  constructor(model: CrewWidgetModel, themeLike: unknown, tui?: unknown) {
116
128
  this.model = model;
@@ -130,13 +142,18 @@ class CrewWidgetComponent implements WidgetComponent {
130
142
  // subscribing independently a single event triggered up to 9 callbacks
131
143
  // and ~150 invalidates/sec under load. The scheduler collapses bursts
132
144
  // into one debounced invalidate.
133
- this.renderScheduler = new RenderScheduler(
134
- runEventBusAsRenderScheduler(["run:state", "worker:lifecycle", "ui:invalidate"]),
145
+ this.schedulerHandle = registerOverlayScheduler(
135
146
  () => this.invalidate(),
136
- {
137
- debounceMs: 75,
138
- fallbackMs: 750,
139
- events: ["run:state", "worker:lifecycle", "ui:invalidate"],
147
+ () => {
148
+ // C4 invalidate-on-write: drop the cached buildSignature()
149
+ // result immediately when a real event arrives (run:state /
150
+ // worker:lifecycle / ui:invalidate). The prior blind-TTL
151
+ // attempt (reverted in 619a0cd) held a stale signature
152
+ // within the window and skipped re-render on genuine state
153
+ // changes. Clearing here + in invalidate() guarantees the
154
+ // next render tick always recomputes from fresh data.
155
+ this.cachedBuildSignature = "";
156
+ this.cachedBuildSignatureAt = 0;
140
157
  },
141
158
  );
142
159
  }
@@ -154,7 +171,7 @@ class CrewWidgetComponent implements WidgetComponent {
154
171
  [...listLiveAgents()].some((h) => h.status === "running");
155
172
  const animation = hasRunning ? `:spin=${spinnerBucket()}` : "";
156
173
 
157
- return (
174
+ const sig =
158
175
  runs
159
176
  .map(
160
177
  (entry) =>
@@ -175,8 +192,8 @@ class CrewWidgetComponent implements WidgetComponent {
175
192
  })
176
193
  .join(","),
177
194
  )
178
- .join("|") + `|live:${liveSig}${animation}`
179
- );
195
+ .join("|") + `|live:${liveSig}${animation}`;
196
+ return sig;
180
197
  }
181
198
 
182
199
  private colorize(lines: string[], width: number): string[] {
@@ -190,6 +207,8 @@ class CrewWidgetComponent implements WidgetComponent {
190
207
  this.cacheSignature = "";
191
208
  this.cachedBaseLines = [];
192
209
  this.cachedLines = [];
210
+ this.cachedBuildSignature = "";
211
+ this.cachedBuildSignatureAt = 0;
193
212
  }
194
213
 
195
214
  /** Poke the host TUI to repaint immediately (defensive: no-op if unavailable). */
@@ -199,13 +218,28 @@ class CrewWidgetComponent implements WidgetComponent {
199
218
 
200
219
  dispose(): void {
201
220
  this.unsubscribeTheme();
202
- this.renderScheduler.dispose();
221
+ this.schedulerHandle.dispose();
203
222
  if (activeResizeTarget === this) activeResizeTarget = undefined;
204
223
  }
205
224
 
206
225
  render(width: number): string[] {
207
226
  const runs = activeWidgetRuns(this.model.cwd, this.model.manifestCache, this.model.snapshotCache, this.model.preloadManifests);
208
- const signature = `${this.buildSignature(runs)}:${this.model.notificationCount ?? 0}`;
227
+ // C4: invalidate-on-write signature cache. buildSignature() is
228
+ // O(runs×agents) string work called on every ~160ms host-driven render
229
+ // tick. Within one state snapshot (no event arrived → cache not cleared
230
+ // by onInvalidate) the result is identical, so we reuse the cached value
231
+ // to skip redundant recomputation. A short TTL acts as a safety net for
232
+ // the unlikely case where a real event is missed.
233
+ const now = Date.now();
234
+ let sigBase: string;
235
+ if (this.cachedBuildSignature !== "" && now - this.cachedBuildSignatureAt < SIGNATURE_CACHE_TTL_MS) {
236
+ sigBase = this.cachedBuildSignature;
237
+ } else {
238
+ sigBase = this.buildSignature(runs);
239
+ this.cachedBuildSignature = sigBase;
240
+ this.cachedBuildSignatureAt = now;
241
+ }
242
+ const signature = `${sigBase}:${this.model.notificationCount ?? 0}`;
209
243
  const runningGlyph = spinnerFrame("widget-header");
210
244
 
211
245
  if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
@@ -342,8 +376,6 @@ export function stopCrewWidget(
342
376
  state: CrewWidgetState,
343
377
  config?: CrewUiConfig,
344
378
  ): void {
345
- if (state.interval) clearInterval(state.interval);
346
- state.interval = undefined;
347
379
  if (ctx?.hasUI) {
348
380
  const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
349
381
  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