pi-crew 0.10.5 → 0.11.0
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 +292 -0
- package/agents/analyst.md +37 -2
- package/agents/cold-verifier.md +10 -1
- package/agents/councillor-critic.md +39 -0
- package/agents/councillor-pragmatist.md +39 -0
- package/agents/councillor-skeptic.md +41 -0
- package/agents/critic.md +40 -2
- package/agents/designer.md +58 -0
- package/agents/executor.md +39 -2
- package/agents/explorer.md +38 -2
- package/agents/librarian.md +49 -0
- package/agents/oracle.md +54 -0
- package/agents/orchestrator.md +48 -0
- package/agents/planner.md +41 -2
- package/agents/reviewer.md +39 -2
- package/agents/security-reviewer.md +43 -2
- package/agents/test-engineer.md +48 -2
- package/agents/verifier.md +14 -1
- package/agents/writer.md +32 -2
- package/dist/index.mjs +35196 -34789
- package/package.json +170 -169
- package/scripts/watch-agent-transcript.mjs +120 -0
- package/skills/async-worker-recovery/SKILL.md +4 -1
- package/skills/child-pi-spawning/SKILL.md +4 -1
- package/skills/context-artifact-hygiene/SKILL.md +4 -1
- package/skills/council/SKILL.md +24 -45
- package/skills/delegation-patterns/SKILL.md +18 -1
- package/skills/distill-persona/SKILL.md +4 -1
- package/skills/distill-software/SKILL.md +4 -1
- package/skills/event-log-tracing/SKILL.md +4 -1
- package/skills/git-master/SKILL.md +4 -1
- package/skills/iterative-audit/SKILL.md +4 -1
- package/skills/live-agent-lifecycle/SKILL.md +4 -1
- package/skills/mailbox-interactive/SKILL.md +4 -1
- package/skills/model-routing-context/SKILL.md +10 -1
- package/skills/multi-perspective-review/SKILL.md +18 -1
- package/skills/observability-reliability/SKILL.md +4 -1
- package/skills/orchestration/SKILL.md +18 -1
- package/skills/ownership-session-security/SKILL.md +4 -1
- package/skills/pi-extension-lifecycle/SKILL.md +4 -1
- package/skills/post-mortem/SKILL.md +4 -1
- package/skills/read-only-explorer/SKILL.md +4 -1
- package/skills/real-test-pi-crew/SKILL.md +165 -12
- package/skills/requirements-to-task-packet/SKILL.md +10 -1
- package/skills/research/SKILL.md +4 -1
- package/skills/resource-discovery-config/SKILL.md +10 -1
- package/skills/runtime-state-reader/SKILL.md +4 -1
- package/skills/safe-bash/SKILL.md +4 -1
- package/skills/scrutinize/SKILL.md +24 -1
- package/skills/secure-agent-orchestration-review/SKILL.md +4 -1
- package/skills/state-mutation-locking/SKILL.md +4 -1
- package/skills/systematic-debugging/SKILL.md +4 -1
- package/skills/verification-before-done/SKILL.md +18 -1
- package/skills/widget-rendering/SKILL.md +4 -1
- package/skills/workspace-isolation/SKILL.md +4 -1
- package/skills/worktree-isolation/SKILL.md +4 -1
- package/src/config/config-validation.ts +1 -0
- package/src/config/types.ts +8 -0
- package/src/errors.ts +1 -1
- package/src/extension/context-status-injection.ts +2 -2
- package/src/extension/crew-input-router.ts +2 -0
- package/src/extension/crew-vibes/config.ts +1 -56
- package/src/extension/crew-vibes/figures.ts +5 -84
- package/src/extension/crew-vibes/font-detect.ts +1 -1
- package/src/extension/crew-vibes/index.ts +36 -328
- package/src/extension/crew-vibes/render.ts +2 -58
- package/src/extension/help.ts +1 -0
- package/src/extension/knowledge-injection.ts +19 -7
- package/src/extension/post-init-skill-check.ts +32 -0
- package/src/extension/register.ts +9 -1
- package/src/extension/registration/command-registration.ts +5 -1
- package/src/extension/registration/commands/schedules.ts +212 -0
- package/src/extension/registration/commands/shared.ts +56 -1
- package/src/extension/registration/hook-registration.ts +20 -3
- package/src/extension/registration/lifecycle-handlers.ts +16 -2
- package/src/extension/registration/schedule-toast-bridge.ts +109 -0
- package/src/extension/registration/tool-loop-guard.ts +243 -0
- package/src/extension/registration/viewers.ts +66 -1
- package/src/extension/team-tool/handle-schedule.ts +120 -1
- package/src/extension/team-tool/handle-settings.ts +10 -0
- package/src/extension/team-tool/run.ts +42 -1
- package/src/extension/team-tool-types.ts +6 -0
- package/src/prompt/prompt-runtime.ts +25 -6
- package/src/runtime/async-runner.ts +75 -11
- package/src/runtime/background-runner.ts +41 -0
- package/src/runtime/broker/crew-broker-client.ts +45 -2
- package/src/runtime/broker/crew-broker.ts +22 -27
- package/src/runtime/broker/protocol/request-parsers.ts +10 -2
- package/src/runtime/broker/stdin-handshake.ts +87 -0
- package/src/runtime/broker/wait-push.ts +45 -0
- package/src/runtime/detached-run-results.ts +25 -1
- package/src/runtime/foreground-watchdog.ts +24 -5
- package/src/runtime/live-session/live-session-runtime.ts +1 -1
- package/src/runtime/model/model-scope.ts +2 -2
- package/src/runtime/run-tracker.ts +65 -11
- package/src/runtime/scheduling/scheduler.ts +120 -6
- package/src/runtime/settings-store.ts +28 -2
- package/src/runtime/skill-instructions.ts +20 -4
- package/src/runtime/task-runner/child-executor.ts +1 -1
- package/src/runtime/task-runner/prompt-builder.ts +22 -9
- package/src/schema/config-schema.ts +1 -0
- package/src/skills/discover-skills.ts +2 -2
- package/src/state/coordination/locks.ts +17 -2
- package/src/ui/agents-jobs-browser.ts +785 -0
- package/src/ui/dashboard-panes/agents-pane.ts +7 -4
- package/src/ui/dashboard-panes/schedules-pane.ts +154 -0
- package/src/ui/inline-panel/crew-editor.ts +19 -6
- package/src/ui/inline-panel/index.ts +6 -0
- package/src/ui/key-utils.ts +3 -0
- package/src/ui/keybinding-map.ts +56 -2
- package/src/ui/mascot.ts +2 -1
- package/src/ui/overlays/agent-picker-overlay.ts +3 -3
- package/src/ui/overlays/confirm-overlay.ts +3 -2
- package/src/ui/overlays/mailbox-compose-overlay.ts +4 -3
- package/src/ui/overlays/mailbox-detail-overlay.ts +3 -3
- package/src/ui/run-dashboard.ts +257 -6
- package/src/ui/settings-overlay.ts +43 -3
- package/src/ui/transcript-viewer.ts +3 -2
- package/src/ui/widget/index.ts +61 -88
- package/src/ui/widget/widget-formatters.ts +7 -4
- package/src/ui/widget/widget-renderer.ts +134 -174
- package/src/ui/widget/widget-types.ts +0 -7
- package/src/utils/frontmatter.ts +7 -1
- package/src/utils/ndjson.ts +9 -1
- package/src/utils/relative-time.ts +33 -0
- package/src/extension/crew-vibes/footer.ts +0 -313
- package/src/extension/crew-vibes/speed.ts +0 -286
- package/src/ui/dock-footer.ts +0 -49
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { requestRender, setFooter } from "../../ui/pi-ui-compat.ts";
|
|
3
2
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Wrap a pi UI call in try/catch. If the call throws (e.g.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Crew-vibes must NEVER break the user's session. A broken spinner is not
|
|
12
|
-
* worth a crashed pi.
|
|
5
|
+
* Wrap a pi UI call in try/catch. If the call throws (e.g. theme helper
|
|
6
|
+
* returns undefined or pi's API signature changed), log it and silently fall
|
|
7
|
+
* back to pi's default behavior. Crew-vibes must NEVER break the user's
|
|
8
|
+
* session.
|
|
13
9
|
*/
|
|
14
10
|
function safeUiCall(scope: string, fn: () => void): void {
|
|
15
11
|
try {
|
|
@@ -19,155 +15,39 @@ function safeUiCall(scope: string, fn: () => void): void {
|
|
|
19
15
|
}
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
import { setFooterDockSinkActive } from "../../ui/dock-footer.ts";
|
|
23
18
|
import { type CrewVibesConfig, loadConfig, saveConfig } from "./config.ts";
|
|
24
|
-
import { intervalForSpeed } from "./figures.ts";
|
|
25
|
-
import { type CrewVibesFooterSource, createCrewVibesFooter } from "./footer.ts";
|
|
26
19
|
import { clearProviderUsageCache, fetchProviderUsage, type ProviderUsage } from "./provider-usage.ts";
|
|
27
|
-
import {
|
|
28
|
-
asCrewTheme,
|
|
29
|
-
clearVibesStatus,
|
|
30
|
-
crewIndicatorFrames,
|
|
31
|
-
formatSpeed,
|
|
32
|
-
getCapacityUsage,
|
|
33
|
-
renderSpeedFooter,
|
|
34
|
-
renderWorkingMessage,
|
|
35
|
-
setSpeedStatus,
|
|
36
|
-
} from "./render.ts";
|
|
37
|
-
import { SpeedAnimator, SpeedTracker } from "./speed.ts";
|
|
20
|
+
import { asCrewTheme, clearVibesStatus, renderProviderUsage, setProviderStatus } from "./render.ts";
|
|
38
21
|
|
|
39
22
|
export const CREW_VIBES_STATUS_KEY = "pi-crew-vibes";
|
|
40
23
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function assistantStopReason(message: unknown): string | undefined {
|
|
52
|
-
const reason = (message as { stopReason?: unknown }).stopReason;
|
|
53
|
-
return typeof reason === "string" ? reason : undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function assistantEventType(event: unknown): string | undefined {
|
|
57
|
-
return typeof (event as { type?: string }).type === "string" ? (event as { type: string }).type : undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
24
|
+
/**
|
|
25
|
+
* crew-vibes — provider rate-limit quota publisher.
|
|
26
|
+
*
|
|
27
|
+
* Maintainer decisions 2026-09-13/14 (UI review follow-ups) stripped this
|
|
28
|
+
* module down to its one remaining job: fetching the current provider's
|
|
29
|
+
* rate-limit usage and publishing it as a STATUS entry on pi's NATIVE
|
|
30
|
+
* footer. Removed entirely: the custom footer replacement (it had drifted
|
|
31
|
+
* from native) and the tok/s speed UI (custom spinner + "Working N tok/s")
|
|
32
|
+
* — pi's built-ins are always used.
|
|
33
|
+
*/
|
|
60
34
|
export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
61
35
|
let config: CrewVibesConfig = loadConfig();
|
|
62
|
-
let lastRenderedAt = 0;
|
|
63
|
-
let currentIntervalMs = 0;
|
|
64
|
-
const speedTracker = new SpeedTracker(config.speed);
|
|
65
|
-
const footerAnimator = new SpeedAnimator(config.speed.renderIntervalMs);
|
|
66
|
-
let liveTimer: ReturnType<typeof setInterval> | undefined;
|
|
67
|
-
let footerTimer: ReturnType<typeof setInterval> | undefined;
|
|
68
|
-
let capacityTimer: ReturnType<typeof setInterval> | undefined;
|
|
69
36
|
let providerTimer: ReturnType<typeof setInterval> | undefined;
|
|
70
37
|
let lastProviderUsage: ProviderUsage | null = null;
|
|
71
38
|
let currentProvider: string | undefined;
|
|
72
|
-
let currentThinkingLevel: string | undefined;
|
|
73
39
|
|
|
74
40
|
function themeOf(ctx: ExtensionContext) {
|
|
75
41
|
return asCrewTheme(ctx.hasUI ? ctx.ui.theme : undefined);
|
|
76
42
|
}
|
|
77
43
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/** Whether the custom footer has crew-vibes meters to add on top of pi's stats. */
|
|
85
|
-
function metersActive(): boolean {
|
|
86
|
-
return config.enabled && (config.capacity.enabled || config.capacity.providerUsage);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Install our custom footer, or restore pi's built-in footer when idle.
|
|
90
|
-
* Unlike setStatus (joined + right-truncated), the footer's render(width)
|
|
91
|
-
* gets the real width and owns line layout, so the quota is never chopped. */
|
|
92
|
-
function installFooter(ctx: ExtensionContext): void {
|
|
93
|
-
if (!ctx?.hasUI) return;
|
|
94
|
-
if (!metersActive()) {
|
|
95
|
-
safeUiCall("clear-footer", () => {
|
|
96
|
-
// The crew dock can no longer render at the very bottom — the
|
|
97
|
-
// widget falls back to pi's belowEditor slot automatically.
|
|
98
|
-
setFooterDockSinkActive(false);
|
|
99
|
-
setFooter(ctx, undefined);
|
|
100
|
-
});
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
safeUiCall("install-footer", () => {
|
|
104
|
-
setFooter(ctx, (tui, theme, footerData) => createCrewVibesFooter({ tui, theme, footerData, ctx, source: footerSource }));
|
|
105
|
-
// The footer can now host the crew dock below the quota lines.
|
|
106
|
-
setFooterDockSinkActive(true);
|
|
107
|
-
requestRender(ctx);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Trigger a footer repaint; the footer recomputes capacity/quota on render.
|
|
112
|
-
*
|
|
113
|
-
* Wrapped wholly in safeUiCall because fetchProviderAndRefresh is async: after
|
|
114
|
-
* its `await` the session may have shut down (session_shutdown clears the
|
|
115
|
-
* timers, but an in-flight fetchProviderAndRefresh still resumes), making ctx
|
|
116
|
-
* stale. Accessing the `hasUI` getter on a stale ctx throws — catch it so
|
|
117
|
-
* crew-vibes never crashes pi. Matches the file's core philosophy: "must
|
|
118
|
-
* NEVER break the user's session". */
|
|
119
|
-
function refreshFooter(ctx: ExtensionContext): void {
|
|
120
|
-
safeUiCall("refresh-footer", () => {
|
|
121
|
-
if (ctx?.hasUI) requestRender(ctx);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function publishSpeedFooter(ctx: ExtensionContext, speed = footerAnimator.value()): void {
|
|
126
|
-
if (!config.enabled || !config.speed.enabled || !config.speed.footer) {
|
|
127
|
-
safeUiCall("clear-speed-status", () => setSpeedStatus(ctx, config, undefined));
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
safeUiCall("publish-speed-status", () => setSpeedStatus(ctx, config, renderSpeedFooter(themeOf(ctx), config.speed, speed)));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function applyIndicator(ctx: ExtensionContext, speed: number | null, force = false): void {
|
|
134
|
-
if (!ctx.hasUI || !ctx.ui.setWorkingIndicator) return;
|
|
135
|
-
if (!config.enabled || !config.speed.enabled || !config.speed.indicator) {
|
|
136
|
-
safeUiCall("reset-indicator", () => ctx.ui.setWorkingIndicator?.());
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const next = intervalForSpeed(config.speed, speed);
|
|
140
|
-
if (!force && Math.abs(next - currentIntervalMs) < 10) return;
|
|
141
|
-
safeUiCall("set-indicator", () =>
|
|
142
|
-
ctx.ui.setWorkingIndicator?.({
|
|
143
|
-
frames: crewIndicatorFrames(themeOf(ctx)),
|
|
144
|
-
intervalMs: next,
|
|
145
|
-
}),
|
|
44
|
+
/** Publish the provider rate-limit quota as a STATUS entry (joined into
|
|
45
|
+
* pi's NATIVE footer status line; right-truncates on very narrow
|
|
46
|
+
* terminals — pi's documented behavior, accepted). */
|
|
47
|
+
function publishQuotaStatus(ctx: ExtensionContext): void {
|
|
48
|
+
safeUiCall("publish-quota-status", () =>
|
|
49
|
+
setProviderStatus(ctx, config, lastProviderUsage ? renderProviderUsage(themeOf(ctx), lastProviderUsage) : undefined),
|
|
146
50
|
);
|
|
147
|
-
currentIntervalMs = next;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function renderWorking(ctx: ExtensionContext, speed: number | null): void {
|
|
151
|
-
if (!config.enabled || !config.speed.enabled || !ctx.hasUI) return;
|
|
152
|
-
safeUiCall("set-working-message", () => ctx.ui.setWorkingMessage?.(renderWorkingMessage(themeOf(ctx), config.speed, speed)));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function stopLiveTimer(): void {
|
|
156
|
-
if (!liveTimer) return;
|
|
157
|
-
clearInterval(liveTimer);
|
|
158
|
-
liveTimer = undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function stopFooterTimer(): void {
|
|
162
|
-
if (!footerTimer) return;
|
|
163
|
-
clearInterval(footerTimer);
|
|
164
|
-
footerTimer = undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function stopCapacityTimer(): void {
|
|
168
|
-
if (!capacityTimer) return;
|
|
169
|
-
clearInterval(capacityTimer);
|
|
170
|
-
capacityTimer = undefined;
|
|
171
51
|
}
|
|
172
52
|
|
|
173
53
|
function stopProviderTimer(): void {
|
|
@@ -176,48 +56,14 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
176
56
|
providerTimer = undefined;
|
|
177
57
|
}
|
|
178
58
|
|
|
179
|
-
|
|
180
|
-
if (liveTimer || !ctx.hasUI) return;
|
|
181
|
-
liveTimer = setInterval(() => {
|
|
182
|
-
if (!config.enabled || !config.speed.enabled || !speedTracker.isStreaming) {
|
|
183
|
-
stopLiveTimer();
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const speed = speedTracker.liveTokS();
|
|
187
|
-
applyIndicator(ctx, speed);
|
|
188
|
-
renderWorking(ctx, speed);
|
|
189
|
-
}, config.speed.renderIntervalMs);
|
|
190
|
-
liveTimer.unref?.();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function startFooterTimer(ctx: ExtensionContext): void {
|
|
194
|
-
if (footerTimer || !ctx.hasUI) return;
|
|
195
|
-
footerTimer = setInterval(() => {
|
|
196
|
-
if (!config.enabled || !config.speed.enabled) {
|
|
197
|
-
stopFooterTimer();
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
publishSpeedFooter(ctx);
|
|
201
|
-
if (!footerAnimator.isAnimating()) stopFooterTimer();
|
|
202
|
-
}, config.speed.renderIntervalMs);
|
|
203
|
-
footerTimer.unref?.();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function startCapacityTimer(ctx: ExtensionContext): void {
|
|
207
|
-
if (capacityTimer) return;
|
|
208
|
-
const interval = Math.max(250, config.capacity.refreshIntervalMs);
|
|
209
|
-
capacityTimer = setInterval(() => refreshFooter(ctx), interval);
|
|
210
|
-
capacityTimer.unref?.();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Fetch provider usage for currentProvider and refresh the footer.
|
|
59
|
+
/** Fetch provider usage for currentProvider and publish the quota status.
|
|
214
60
|
* Called on start, on each timer tick, and immediately when the
|
|
215
61
|
* provider changes (model_select) so the quota reflects the new
|
|
216
|
-
* provider without waiting for the next
|
|
62
|
+
* provider without waiting for the next tick. */
|
|
217
63
|
async function fetchProviderAndRefresh(ctx: ExtensionContext): Promise<void> {
|
|
218
64
|
if (!config.enabled || !config.capacity.providerUsage) {
|
|
219
65
|
lastProviderUsage = null;
|
|
220
|
-
|
|
66
|
+
publishQuotaStatus(ctx);
|
|
221
67
|
return;
|
|
222
68
|
}
|
|
223
69
|
try {
|
|
@@ -226,7 +72,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
226
72
|
// Never crash on provider fetch failure
|
|
227
73
|
lastProviderUsage = null;
|
|
228
74
|
}
|
|
229
|
-
|
|
75
|
+
publishQuotaStatus(ctx);
|
|
230
76
|
}
|
|
231
77
|
|
|
232
78
|
function startProviderTimer(ctx: ExtensionContext): void {
|
|
@@ -239,204 +85,66 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
239
85
|
providerTimer.unref?.();
|
|
240
86
|
}
|
|
241
87
|
|
|
242
|
-
function resetWorking(ctx: ExtensionContext): void {
|
|
243
|
-
applyIndicator(ctx, null, true);
|
|
244
|
-
renderWorking(ctx, speedTracker.lastTokS);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
88
|
function applyConfig(ctx: ExtensionContext): void {
|
|
248
89
|
saveConfig(config);
|
|
249
|
-
speedTracker.updateConfig(config.speed);
|
|
250
|
-
footerAnimator.updateDuration(config.speed.renderIntervalMs);
|
|
251
90
|
if (!config.enabled) {
|
|
252
|
-
stopLiveTimer();
|
|
253
|
-
stopFooterTimer();
|
|
254
|
-
stopCapacityTimer();
|
|
255
91
|
stopProviderTimer();
|
|
256
|
-
setFooterDockSinkActive(false);
|
|
257
|
-
setFooter(ctx, undefined);
|
|
258
92
|
clearVibesStatus(ctx);
|
|
259
93
|
return;
|
|
260
94
|
}
|
|
261
|
-
installFooter(ctx);
|
|
262
|
-
publishSpeedFooter(ctx);
|
|
263
|
-
startCapacityTimer(ctx);
|
|
264
95
|
if (config.capacity.providerUsage) startProviderTimer(ctx);
|
|
96
|
+
else publishQuotaStatus(ctx); // clears the stale status when disabled
|
|
265
97
|
}
|
|
266
98
|
|
|
267
99
|
pi.on("session_start", (_event, ctx) => {
|
|
268
|
-
stopLiveTimer();
|
|
269
|
-
stopFooterTimer();
|
|
270
|
-
stopCapacityTimer();
|
|
271
100
|
stopProviderTimer();
|
|
272
101
|
config = loadConfig();
|
|
273
|
-
speedTracker.updateConfig(config.speed);
|
|
274
|
-
footerAnimator.updateDuration(config.speed.renderIntervalMs);
|
|
275
|
-
speedTracker.resetSession();
|
|
276
|
-
footerAnimator.reset(null);
|
|
277
102
|
clearProviderUsageCache();
|
|
278
103
|
// Initialize provider from current model — model_select only fires on manual switch
|
|
279
104
|
currentProvider = (ctx.model as { provider?: string } | undefined)?.provider;
|
|
280
|
-
currentThinkingLevel = undefined;
|
|
281
105
|
if (!config.enabled) {
|
|
282
|
-
setFooter(ctx, undefined);
|
|
283
106
|
clearVibesStatus(ctx);
|
|
284
107
|
return;
|
|
285
108
|
}
|
|
286
|
-
installFooter(ctx);
|
|
287
|
-
publishSpeedFooter(ctx);
|
|
288
|
-
startCapacityTimer(ctx);
|
|
289
109
|
startProviderTimer(ctx);
|
|
290
|
-
// Set the working indicator early (matches pi's official working-indicator
|
|
291
|
-
// example) so pi has the custom frames configured before streaming begins.
|
|
292
|
-
// Calls before the loading animation exists are ignored, so we re-apply on
|
|
293
|
-
// agent_start/message_update as well.
|
|
294
|
-
applyIndicator(ctx, null, true);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
298
|
-
if (!config.enabled) return;
|
|
299
|
-
resetWorking(ctx);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
pi.on("turn_start", (_event, ctx) => {
|
|
303
|
-
if (!config.enabled) return;
|
|
304
|
-
resetWorking(ctx);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
pi.on("message_start", (event, ctx) => {
|
|
308
|
-
if (!config.enabled || !config.speed.enabled || !isAssistantMessage(event.message)) return;
|
|
309
|
-
speedTracker.startMessage();
|
|
310
|
-
footerAnimator.reset(speedTracker.lastTokS);
|
|
311
|
-
startLiveTimer(ctx);
|
|
312
|
-
lastRenderedAt = 0;
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
pi.on("message_update", (event, ctx) => {
|
|
316
|
-
if (!config.enabled || !config.speed.enabled || !isAssistantMessage(event.message) || !speedTracker.isStreaming) return;
|
|
317
|
-
|
|
318
|
-
const ev = event.assistantMessageEvent;
|
|
319
|
-
const type = assistantEventType(ev);
|
|
320
|
-
if (type === "text_delta" || type === "thinking_delta") {
|
|
321
|
-
const delta = (ev as { delta?: string }).delta ?? "";
|
|
322
|
-
speedTracker.recordDelta(delta, assistantUsageOutput(event.message));
|
|
323
|
-
}
|
|
324
|
-
if (type === "start") resetWorking(ctx);
|
|
325
|
-
|
|
326
|
-
const now = Date.now();
|
|
327
|
-
if (now - lastRenderedAt < config.speed.renderIntervalMs && type !== "done") return;
|
|
328
|
-
lastRenderedAt = now;
|
|
329
|
-
|
|
330
|
-
const speed = speedTracker.liveTokS();
|
|
331
|
-
applyIndicator(ctx, speed);
|
|
332
|
-
renderWorking(ctx, speed);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
pi.on("message_end", (event, ctx) => {
|
|
336
|
-
if (!isAssistantMessage(event.message)) return;
|
|
337
|
-
refreshFooter(ctx);
|
|
338
|
-
if (!config.enabled || !config.speed.enabled || !speedTracker.isStreaming) return;
|
|
339
|
-
|
|
340
|
-
const completed = speedTracker.finishMessage(assistantUsageOutput(event.message) ?? 0, assistantStopReason(event.message));
|
|
341
|
-
if (!completed) return;
|
|
342
|
-
|
|
343
|
-
footerAnimator.setTarget(speedTracker.sessionAvgTokS());
|
|
344
|
-
publishSpeedFooter(ctx);
|
|
345
|
-
startFooterTimer(ctx);
|
|
346
|
-
applyIndicator(ctx, speedTracker.lastTokS);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
pi.on("turn_end", () => {
|
|
350
|
-
speedTracker.stopMessage();
|
|
351
|
-
stopLiveTimer();
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
pi.on("agent_end", (_event, ctx) => {
|
|
355
|
-
speedTracker.stopMessage();
|
|
356
|
-
stopLiveTimer();
|
|
357
|
-
if (ctx && config.enabled && ctx.hasUI) {
|
|
358
|
-
applyIndicator(ctx, speedTracker.lastTokS);
|
|
359
|
-
safeUiCall("clear-working-message", () => ctx.ui.setWorkingMessage?.());
|
|
360
|
-
}
|
|
361
110
|
});
|
|
362
111
|
|
|
363
112
|
pi.on("model_select", (event, ctx) => {
|
|
364
113
|
currentProvider = (event as { model?: { provider?: string } }).model?.provider;
|
|
365
114
|
clearProviderUsageCache();
|
|
366
115
|
// Fetch immediately so the quota reflects the new provider without
|
|
367
|
-
// waiting for the next
|
|
116
|
+
// waiting for the next timer tick.
|
|
368
117
|
fetchProviderAndRefresh(ctx);
|
|
369
118
|
});
|
|
370
|
-
pi.on("thinking_level_select", (event, ctx) => {
|
|
371
|
-
currentThinkingLevel = (event as { level?: unknown }).level as string | undefined;
|
|
372
|
-
refreshFooter(ctx);
|
|
373
|
-
});
|
|
374
|
-
pi.on("session_compact", (_event, ctx) => refreshFooter(ctx));
|
|
375
|
-
pi.on("session_tree", (_event, ctx) => refreshFooter(ctx));
|
|
376
119
|
|
|
377
120
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
378
|
-
stopLiveTimer();
|
|
379
|
-
stopFooterTimer();
|
|
380
|
-
stopCapacityTimer();
|
|
381
121
|
stopProviderTimer();
|
|
382
|
-
setFooterDockSinkActive(false);
|
|
383
|
-
setFooter(ctx, undefined);
|
|
384
122
|
clearVibesStatus(ctx);
|
|
385
123
|
});
|
|
386
124
|
|
|
387
125
|
async function handleCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
388
|
-
const
|
|
389
|
-
const [first, second] = tokens;
|
|
390
|
-
|
|
391
|
-
const mutate = (next: CrewVibesConfig): void => {
|
|
392
|
-
config = next;
|
|
393
|
-
applyConfig(ctx);
|
|
394
|
-
};
|
|
126
|
+
const [first] = args.trim().split(/\s+/).filter(Boolean);
|
|
395
127
|
|
|
396
128
|
if (!first) {
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
Math.max(
|
|
402
|
-
0,
|
|
403
|
-
Math.min(
|
|
404
|
-
config.capacity.labels.length - 1,
|
|
405
|
-
Math.floor(((usage.percent ?? 0) / 100) * config.capacity.labels.length),
|
|
406
|
-
),
|
|
407
|
-
)
|
|
408
|
-
]
|
|
409
|
-
: "?";
|
|
410
|
-
ctx.ui.notify(
|
|
411
|
-
`crew-vibes: ${config.enabled ? "on" : "off"} · speed ${config.speed.enabled ? "on" : "off"} (${formatSpeed(config.speed, speed)}) · capacity ${config.capacity.enabled ? "on" : "off"} (${stage})`,
|
|
412
|
-
"info",
|
|
413
|
-
);
|
|
129
|
+
const quota = lastProviderUsage
|
|
130
|
+
? `${lastProviderUsage.providerName} 5h ${Math.round(lastProviderUsage.fiveHourPercent)}% · Wk ${Math.round(lastProviderUsage.weeklyPercent)}%`
|
|
131
|
+
: "no data yet";
|
|
132
|
+
ctx.ui.notify(`crew-vibes: ${config.enabled ? "on" : "off"} · quota ${quota}`, "info");
|
|
414
133
|
return;
|
|
415
134
|
}
|
|
416
135
|
|
|
417
136
|
if (first === "on" || first === "off") {
|
|
418
|
-
|
|
137
|
+
config = { ...config, enabled: first === "on" };
|
|
138
|
+
applyConfig(ctx);
|
|
419
139
|
ctx.ui.notify(`crew-vibes ${first === "on" ? "enabled" : "disabled"}`, "info");
|
|
420
140
|
return;
|
|
421
141
|
}
|
|
422
142
|
|
|
423
|
-
|
|
424
|
-
mutate({ ...config, speed: { ...config.speed, enabled: second === "on" } });
|
|
425
|
-
ctx.ui.notify(`crew-vibes speed ${second === "on" ? "enabled" : "disabled"}`, "info");
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (first === "capacity" && (second === "on" || second === "off")) {
|
|
430
|
-
mutate({ ...config, capacity: { ...config.capacity, enabled: second === "on" } });
|
|
431
|
-
ctx.ui.notify(`crew-vibes capacity ${second === "on" ? "enabled" : "disabled"}`, "info");
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
ctx.ui.notify("Usage: /team-vibes [on|off|speed on|off|capacity on|off]", "error");
|
|
143
|
+
ctx.ui.notify("Usage: /team-vibes [on|off]", "error");
|
|
436
144
|
}
|
|
437
145
|
|
|
438
146
|
pi.registerCommand("team-vibes", {
|
|
439
|
-
description: "Toggle
|
|
147
|
+
description: "Toggle the provider-quota status (on/off)",
|
|
440
148
|
handler: handleCommand,
|
|
441
149
|
});
|
|
442
150
|
}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { CrewTheme } from "../../ui/theme-adapter.ts";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
type CapacityConfig,
|
|
6
|
-
type CrewVibesConfig,
|
|
7
|
-
capacityIcons,
|
|
8
|
-
PROVIDER_STATUS_ID,
|
|
9
|
-
SPEED_STATUS_ID,
|
|
10
|
-
type SpeedConfig,
|
|
11
|
-
type TokenDisplay,
|
|
12
|
-
} from "./config.ts";
|
|
13
|
-
import { capacityIndex, crewFrames, isDangerStage } from "./figures.ts";
|
|
3
|
+
import { type CapacityConfig, type CrewVibesConfig, capacityIcons, PROVIDER_STATUS_ID, type TokenDisplay } from "./config.ts";
|
|
4
|
+
import { capacityIndex, isDangerStage } from "./figures.ts";
|
|
14
5
|
|
|
15
6
|
export type CapacityUsage = {
|
|
16
7
|
tokens: number | null;
|
|
@@ -41,31 +32,6 @@ export function getCapacityUsage(ctx: ExtensionContext): CapacityUsage {
|
|
|
41
32
|
};
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
export function formatSpeed(config: SpeedConfig, speed: number | null): string {
|
|
45
|
-
return speed === null ? `-- ${config.label}` : `${speed.toFixed(1)} ${config.label}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function renderSpeedFooter(theme: CrewTheme | undefined, config: SpeedConfig, speed: number | null): string {
|
|
49
|
-
const value = speed === null ? "--" : speed.toFixed(1);
|
|
50
|
-
const valueTone = speed === null ? "dim" : "accent";
|
|
51
|
-
const styled = theme ? `${theme.fg(valueTone, value)} ${theme.fg("dim", config.label)}` : `${value} ${config.label}`;
|
|
52
|
-
return styled;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function renderWorkingMessage(theme: CrewTheme | undefined, config: SpeedConfig, speed: number | null): string {
|
|
56
|
-
const left = "Working";
|
|
57
|
-
const speedText = theme
|
|
58
|
-
? `${theme.fg(speed === null ? "dim" : "accent", speed === null ? "--" : speed.toFixed(1))} ${theme.fg("dim", config.label)}`
|
|
59
|
-
: `${speed === null ? "--" : speed.toFixed(1)} ${config.label}`;
|
|
60
|
-
return theme ? `${theme.fg("muted", left)} ${speedText}` : `${left} ${speedText}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function crewIndicatorFrames(theme: CrewTheme | undefined): string[] {
|
|
64
|
-
const frames = crewFrames();
|
|
65
|
-
if (!theme) return [...frames];
|
|
66
|
-
return frames.map((frame) => theme.fg("accent", frame));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
35
|
function formatCapacityPrefix(config: CapacityConfig, usage: CapacityUsage): string {
|
|
70
36
|
const display: TokenDisplay = config.tokenDisplay;
|
|
71
37
|
if (display === "off") return "";
|
|
@@ -92,31 +58,9 @@ export function renderCapacity(theme: CrewTheme | undefined, config: CapacityCon
|
|
|
92
58
|
return `${prefix}${coloredIcon}${afterIcon}`;
|
|
93
59
|
}
|
|
94
60
|
|
|
95
|
-
export function setSpeedStatus(ctx: ExtensionContext, config: CrewVibesConfig, text: string | undefined): void {
|
|
96
|
-
if (!ctx?.hasUI) return;
|
|
97
|
-
if (!config.enabled || !config.speed.enabled || !config.speed.footer) {
|
|
98
|
-
ctx.ui.setStatus(SPEED_STATUS_ID, undefined);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
ctx.ui.setStatus(SPEED_STATUS_ID, text);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function setCapacityStatus(ctx: ExtensionContext, config: CrewVibesConfig, text: string | undefined): void {
|
|
105
|
-
if (!ctx?.hasUI) return;
|
|
106
|
-
if (!config.enabled || !config.capacity.enabled) {
|
|
107
|
-
ctx.ui.setStatus(CAPACITY_STATUS_ID, undefined);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
ctx.ui.setStatus(CAPACITY_STATUS_ID, text);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
61
|
export function clearVibesStatus(ctx: ExtensionContext): void {
|
|
114
62
|
if (!ctx?.hasUI) return;
|
|
115
|
-
ctx.ui.setStatus(SPEED_STATUS_ID, undefined);
|
|
116
|
-
ctx.ui.setStatus(CAPACITY_STATUS_ID, undefined);
|
|
117
63
|
ctx.ui.setStatus(PROVIDER_STATUS_ID, undefined);
|
|
118
|
-
if (ctx.ui.setWorkingIndicator) ctx.ui.setWorkingIndicator();
|
|
119
|
-
if (ctx.ui.setWorkingMessage) ctx.ui.setWorkingMessage();
|
|
120
64
|
}
|
|
121
65
|
|
|
122
66
|
// Provider rate-limit usage snapshot (mirrors provider-usage.ts interface).
|
package/src/extension/help.ts
CHANGED
|
@@ -19,6 +19,7 @@ export function piTeamsHelp(): string {
|
|
|
19
19
|
"- /team-worktrees <runId>",
|
|
20
20
|
"- /team-api <runId> <operation> [taskId=<taskId>] [body=<message>]",
|
|
21
21
|
"- /team-dashboard",
|
|
22
|
+
"- /schedules [log <jobId-or-name>] — list scheduled jobs / tail latest run output",
|
|
22
23
|
"- /team-mascot",
|
|
23
24
|
"- /team-transcript <runId> [taskId]",
|
|
24
25
|
"- /team-result <runId> [taskId]",
|
|
@@ -437,13 +437,21 @@ export function buildKnowledgeFragment(cwd: string, query?: KnowledgeQuery): str
|
|
|
437
437
|
|
|
438
438
|
/**
|
|
439
439
|
* Register the knowledge-injection hook. Appends project knowledge to the
|
|
440
|
-
* MAIN session's system prompt on `before_agent_start`.
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
440
|
+
* MAIN session's system prompt on `before_agent_start`.
|
|
441
|
+
*
|
|
442
|
+
* Worker knowledge path (ARCH-2 corrected): children are NOT spawned with
|
|
443
|
+
* `--no-extensions` anymore — `pi-args.ts` runs extension discovery like
|
|
444
|
+
* the main session, but a child only loads an extension when the agent's
|
|
445
|
+
* frontmatter declares it (`extensions:`; builtin pi-crew agents declare
|
|
446
|
+
* none, so in practice workers never load pi-crew). Workers instead receive
|
|
447
|
+
* knowledge via `buildKnowledgeFragment(task.cwd)` injected into their
|
|
448
|
+
* prompt stablePrefix by `prompt-builder.ts`.
|
|
449
|
+
*
|
|
450
|
+
* Defense-in-depth: if an agent DOES declare the pi-crew extension, this
|
|
451
|
+
* hook would fire in the worker process and double-inject knowledge (once
|
|
452
|
+
* here, once via prompt-builder). The `PI_CREW_KIND=subagent` early-return
|
|
453
|
+
* below keeps main-session hooks main-session-only regardless of how the
|
|
454
|
+
* child was spawned. Do NOT remove it.
|
|
447
455
|
*
|
|
448
456
|
* The hook calls buildKnowledgeFragment(cwd) with NO query — so the main
|
|
449
457
|
* session gets conventions-only (no session-log noise), which is the right
|
|
@@ -452,6 +460,10 @@ export function buildKnowledgeFragment(cwd: string, query?: KnowledgeQuery): str
|
|
|
452
460
|
*/
|
|
453
461
|
export function registerKnowledgeInjection(pi: ExtensionAPI): void {
|
|
454
462
|
pi.on("before_agent_start", (event: BeforeAgentStartEvent) => {
|
|
463
|
+
// ARCH-2: never fire in child worker processes — knowledge reaches
|
|
464
|
+
// workers via prompt-builder's stablePrefix fragment; firing here too
|
|
465
|
+
// would double-inject.
|
|
466
|
+
if (process.env.PI_CREW_KIND === "subagent") return;
|
|
455
467
|
const options =
|
|
456
468
|
(
|
|
457
469
|
event as BeforeAgentStartEvent & {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { renderSkillInstructions } from "../runtime/skill-instructions.ts";
|
|
2
|
+
|
|
3
|
+
export type SkillCheckResult = {
|
|
4
|
+
total: number;
|
|
5
|
+
resolved: number;
|
|
6
|
+
missing: string[];
|
|
7
|
+
severity: "ok" | "warn" | "error";
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function runPostInitSkillCheck(cwd: string): Promise<SkillCheckResult> {
|
|
12
|
+
const result = renderSkillInstructions({ cwd, role: "executor" });
|
|
13
|
+
const total = result.names.length;
|
|
14
|
+
const missingMatches = result.block.match(/Skill '([^']+)' was selected but no SKILL\.md file was found/g);
|
|
15
|
+
const missing = missingMatches ? missingMatches.map((m) => m.match(/'([^']+)'/)![1]) : [];
|
|
16
|
+
const resolved = total - missing.length;
|
|
17
|
+
|
|
18
|
+
let severity: "ok" | "warn" | "error";
|
|
19
|
+
let message: string;
|
|
20
|
+
if (resolved === total) {
|
|
21
|
+
severity = "ok";
|
|
22
|
+
message = `All ${total} default skills resolved`;
|
|
23
|
+
} else if (resolved === 0) {
|
|
24
|
+
severity = "error";
|
|
25
|
+
message = `0/${total} default skills resolved — likely bundle stale. Run \`npm run build:bundle\`.`;
|
|
26
|
+
} else {
|
|
27
|
+
severity = "warn";
|
|
28
|
+
message = `${resolved}/${total} default skills resolved — degraded: ${missing.join(", ")}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { total, resolved, missing, severity, message };
|
|
32
|
+
}
|
|
@@ -32,6 +32,7 @@ import { registerCrewShortcuts } from "./crew-shortcuts.ts";
|
|
|
32
32
|
import { registerCrewVibes } from "./crew-vibes/index.ts";
|
|
33
33
|
import { registerKnowledgeInjection } from "./knowledge-injection.ts";
|
|
34
34
|
import { registerCrewMessageRenderers } from "./message-renderers.ts";
|
|
35
|
+
import { runPostInitSkillCheck } from "./post-init-skill-check.ts";
|
|
35
36
|
import { registerPiCommands } from "./registration/command-registration.ts";
|
|
36
37
|
import { buildRegistrationContext } from "./registration/context-builder.ts";
|
|
37
38
|
import { importCrashRecovery, purgeStaleActiveRunIndexSyncIfLoaded } from "./registration/crash-recovery-cache.ts";
|
|
@@ -50,7 +51,7 @@ export { __test__subagentSpawnParams };
|
|
|
50
51
|
/**
|
|
51
52
|
* Pi extension entry point. See module-level docstring for the full pipeline.
|
|
52
53
|
*/
|
|
53
|
-
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
54
|
+
export async function registerPiTeams(pi: ExtensionAPI): Promise<void> {
|
|
54
55
|
resetTimings();
|
|
55
56
|
time("register:start");
|
|
56
57
|
|
|
@@ -127,6 +128,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
127
128
|
} catch (err) {
|
|
128
129
|
console.warn("[pi-crew] crew-vibes initialization failed:", err instanceof Error ? err.message : err);
|
|
129
130
|
}
|
|
131
|
+
|
|
132
|
+
const skillCheck = await runPostInitSkillCheck(process.cwd());
|
|
133
|
+
if (skillCheck.severity === "error") {
|
|
134
|
+
console.error(`[pi-crew] ${skillCheck.message}`);
|
|
135
|
+
} else if (skillCheck.severity === "warn") {
|
|
136
|
+
console.warn(`[pi-crew] ${skillCheck.message}`);
|
|
137
|
+
}
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
// Bottom-of-file import: keeps compaction-guard out of the top-level
|