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.
- package/CHANGELOG.md +324 -0
- package/dist/build-meta.json +84 -57
- package/dist/index.mjs +224 -96
- package/dist/index.mjs.map +4 -4
- package/docs/decisions/2026-07-26-c6-mascot-visibility-not-wired.md +84 -0
- package/package.json +1 -2
- package/skills/distill-persona/SKILL.md +83 -145
- package/skills/distill-persona/references/cross-skill-differentiation.md +12 -0
- package/skills/distill-persona/references/description-discipline.md +6 -0
- package/skills/distill-persona/references/diagnostic-path.md +25 -0
- package/skills/distill-persona/references/fidelity-rubric.md +19 -0
- package/skills/distill-persona/references/field-models.md +20 -0
- package/skills/distill-persona/references/optional-body-sections.md +9 -0
- package/skills/distill-persona/references/registry-routing.md +11 -0
- package/skills/distill-persona/references/self-upgrade-directive.md +20 -0
- package/skills/distill-persona/references/taste-principles.md +8 -0
- package/skills/distill-persona/references/topic-variant.md +13 -0
- package/skills/distill-persona/references/update-mode.md +7 -0
- package/skills/distill-persona/scripts/validate-run.mjs +297 -0
- package/skills/distill-software/SKILL.md +174 -90
- package/skills/research/SKILL.md +1 -1
- package/src/extension/crew-cleanup.ts +18 -1
- package/src/extension/crew-vibes/index.ts +11 -2
- package/src/extension/register.ts +1 -1
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands.ts +7 -3
- package/src/extension/registration/lifecycle-handlers.ts +1 -3
- package/src/extension/registration/ui.ts +4 -0
- package/src/extension/registration/viewers.ts +3 -0
- package/src/extension/team-tool/run.ts +7 -6
- package/src/runtime/chain-runner.ts +3 -2
- package/src/runtime/pipeline-runner.ts +8 -7
- package/src/ui/live-run-sidebar.ts +7 -13
- package/src/ui/loaders.ts +6 -176
- package/src/ui/mascot.ts +25 -10
- package/src/ui/render-coalescer.ts +9 -0
- package/src/ui/render-scheduler.ts +60 -6
- package/src/ui/run-dashboard.ts +12 -21
- package/src/ui/run-snapshot-cache.ts +10 -11
- package/src/ui/shared-overlay-scheduler.ts +96 -0
- package/src/ui/terminal-status.ts +5 -0
- package/src/ui/widget/index.ts +48 -16
- package/src/ui/widget/widget-types.ts +0 -1
- package/assets/runner-spritesheet.png +0 -0
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -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 {
|
|
24
|
-
import {
|
|
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
|
|
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.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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.
|
|
502
|
-
this.
|
|
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.
|
|
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
|
-
|
|
1009
|
-
runId
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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;
|
package/src/ui/widget/index.ts
CHANGED
|
@@ -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 {
|
|
16
|
-
import {
|
|
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
|
|
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.
|
|
134
|
-
runEventBusAsRenderScheduler(["run:state", "worker:lifecycle", "ui:invalidate"]),
|
|
145
|
+
this.schedulerHandle = registerOverlayScheduler(
|
|
135
146
|
() => this.invalidate(),
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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);
|
|
Binary file
|