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.
Files changed (128) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/agents/analyst.md +37 -2
  3. package/agents/cold-verifier.md +10 -1
  4. package/agents/councillor-critic.md +39 -0
  5. package/agents/councillor-pragmatist.md +39 -0
  6. package/agents/councillor-skeptic.md +41 -0
  7. package/agents/critic.md +40 -2
  8. package/agents/designer.md +58 -0
  9. package/agents/executor.md +39 -2
  10. package/agents/explorer.md +38 -2
  11. package/agents/librarian.md +49 -0
  12. package/agents/oracle.md +54 -0
  13. package/agents/orchestrator.md +48 -0
  14. package/agents/planner.md +41 -2
  15. package/agents/reviewer.md +39 -2
  16. package/agents/security-reviewer.md +43 -2
  17. package/agents/test-engineer.md +48 -2
  18. package/agents/verifier.md +14 -1
  19. package/agents/writer.md +32 -2
  20. package/dist/index.mjs +35196 -34789
  21. package/package.json +170 -169
  22. package/scripts/watch-agent-transcript.mjs +120 -0
  23. package/skills/async-worker-recovery/SKILL.md +4 -1
  24. package/skills/child-pi-spawning/SKILL.md +4 -1
  25. package/skills/context-artifact-hygiene/SKILL.md +4 -1
  26. package/skills/council/SKILL.md +24 -45
  27. package/skills/delegation-patterns/SKILL.md +18 -1
  28. package/skills/distill-persona/SKILL.md +4 -1
  29. package/skills/distill-software/SKILL.md +4 -1
  30. package/skills/event-log-tracing/SKILL.md +4 -1
  31. package/skills/git-master/SKILL.md +4 -1
  32. package/skills/iterative-audit/SKILL.md +4 -1
  33. package/skills/live-agent-lifecycle/SKILL.md +4 -1
  34. package/skills/mailbox-interactive/SKILL.md +4 -1
  35. package/skills/model-routing-context/SKILL.md +10 -1
  36. package/skills/multi-perspective-review/SKILL.md +18 -1
  37. package/skills/observability-reliability/SKILL.md +4 -1
  38. package/skills/orchestration/SKILL.md +18 -1
  39. package/skills/ownership-session-security/SKILL.md +4 -1
  40. package/skills/pi-extension-lifecycle/SKILL.md +4 -1
  41. package/skills/post-mortem/SKILL.md +4 -1
  42. package/skills/read-only-explorer/SKILL.md +4 -1
  43. package/skills/real-test-pi-crew/SKILL.md +165 -12
  44. package/skills/requirements-to-task-packet/SKILL.md +10 -1
  45. package/skills/research/SKILL.md +4 -1
  46. package/skills/resource-discovery-config/SKILL.md +10 -1
  47. package/skills/runtime-state-reader/SKILL.md +4 -1
  48. package/skills/safe-bash/SKILL.md +4 -1
  49. package/skills/scrutinize/SKILL.md +24 -1
  50. package/skills/secure-agent-orchestration-review/SKILL.md +4 -1
  51. package/skills/state-mutation-locking/SKILL.md +4 -1
  52. package/skills/systematic-debugging/SKILL.md +4 -1
  53. package/skills/verification-before-done/SKILL.md +18 -1
  54. package/skills/widget-rendering/SKILL.md +4 -1
  55. package/skills/workspace-isolation/SKILL.md +4 -1
  56. package/skills/worktree-isolation/SKILL.md +4 -1
  57. package/src/config/config-validation.ts +1 -0
  58. package/src/config/types.ts +8 -0
  59. package/src/errors.ts +1 -1
  60. package/src/extension/context-status-injection.ts +2 -2
  61. package/src/extension/crew-input-router.ts +2 -0
  62. package/src/extension/crew-vibes/config.ts +1 -56
  63. package/src/extension/crew-vibes/figures.ts +5 -84
  64. package/src/extension/crew-vibes/font-detect.ts +1 -1
  65. package/src/extension/crew-vibes/index.ts +36 -328
  66. package/src/extension/crew-vibes/render.ts +2 -58
  67. package/src/extension/help.ts +1 -0
  68. package/src/extension/knowledge-injection.ts +19 -7
  69. package/src/extension/post-init-skill-check.ts +32 -0
  70. package/src/extension/register.ts +9 -1
  71. package/src/extension/registration/command-registration.ts +5 -1
  72. package/src/extension/registration/commands/schedules.ts +212 -0
  73. package/src/extension/registration/commands/shared.ts +56 -1
  74. package/src/extension/registration/hook-registration.ts +20 -3
  75. package/src/extension/registration/lifecycle-handlers.ts +16 -2
  76. package/src/extension/registration/schedule-toast-bridge.ts +109 -0
  77. package/src/extension/registration/tool-loop-guard.ts +243 -0
  78. package/src/extension/registration/viewers.ts +66 -1
  79. package/src/extension/team-tool/handle-schedule.ts +120 -1
  80. package/src/extension/team-tool/handle-settings.ts +10 -0
  81. package/src/extension/team-tool/run.ts +42 -1
  82. package/src/extension/team-tool-types.ts +6 -0
  83. package/src/prompt/prompt-runtime.ts +25 -6
  84. package/src/runtime/async-runner.ts +75 -11
  85. package/src/runtime/background-runner.ts +41 -0
  86. package/src/runtime/broker/crew-broker-client.ts +45 -2
  87. package/src/runtime/broker/crew-broker.ts +22 -27
  88. package/src/runtime/broker/protocol/request-parsers.ts +10 -2
  89. package/src/runtime/broker/stdin-handshake.ts +87 -0
  90. package/src/runtime/broker/wait-push.ts +45 -0
  91. package/src/runtime/detached-run-results.ts +25 -1
  92. package/src/runtime/foreground-watchdog.ts +24 -5
  93. package/src/runtime/live-session/live-session-runtime.ts +1 -1
  94. package/src/runtime/model/model-scope.ts +2 -2
  95. package/src/runtime/run-tracker.ts +65 -11
  96. package/src/runtime/scheduling/scheduler.ts +120 -6
  97. package/src/runtime/settings-store.ts +28 -2
  98. package/src/runtime/skill-instructions.ts +20 -4
  99. package/src/runtime/task-runner/child-executor.ts +1 -1
  100. package/src/runtime/task-runner/prompt-builder.ts +22 -9
  101. package/src/schema/config-schema.ts +1 -0
  102. package/src/skills/discover-skills.ts +2 -2
  103. package/src/state/coordination/locks.ts +17 -2
  104. package/src/ui/agents-jobs-browser.ts +785 -0
  105. package/src/ui/dashboard-panes/agents-pane.ts +7 -4
  106. package/src/ui/dashboard-panes/schedules-pane.ts +154 -0
  107. package/src/ui/inline-panel/crew-editor.ts +19 -6
  108. package/src/ui/inline-panel/index.ts +6 -0
  109. package/src/ui/key-utils.ts +3 -0
  110. package/src/ui/keybinding-map.ts +56 -2
  111. package/src/ui/mascot.ts +2 -1
  112. package/src/ui/overlays/agent-picker-overlay.ts +3 -3
  113. package/src/ui/overlays/confirm-overlay.ts +3 -2
  114. package/src/ui/overlays/mailbox-compose-overlay.ts +4 -3
  115. package/src/ui/overlays/mailbox-detail-overlay.ts +3 -3
  116. package/src/ui/run-dashboard.ts +257 -6
  117. package/src/ui/settings-overlay.ts +43 -3
  118. package/src/ui/transcript-viewer.ts +3 -2
  119. package/src/ui/widget/index.ts +61 -88
  120. package/src/ui/widget/widget-formatters.ts +7 -4
  121. package/src/ui/widget/widget-renderer.ts +134 -174
  122. package/src/ui/widget/widget-types.ts +0 -7
  123. package/src/utils/frontmatter.ts +7 -1
  124. package/src/utils/ndjson.ts +9 -1
  125. package/src/utils/relative-time.ts +33 -0
  126. package/src/extension/crew-vibes/footer.ts +0 -313
  127. package/src/extension/crew-vibes/speed.ts +0 -286
  128. 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. terminal doesn't
7
- * support PUA glyphs, theme helper returns undefined, or pi's API signature
8
- * changed), log it and silently fall back to pi's default behavior — which
9
- * is simply "do nothing", letting pi show its own built-in indicator.
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
- function isAssistantMessage(message: unknown): boolean {
42
- return typeof message === "object" && message !== null && (message as { role?: string }).role === "assistant";
43
- }
44
-
45
- function assistantUsageOutput(message: unknown): number | undefined {
46
- const usage = (message as { usage?: { output?: unknown } }).usage;
47
- const output = usage?.output;
48
- return typeof output === "number" && Number.isFinite(output) ? output : undefined;
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
- const footerSource: CrewVibesFooterSource = {
79
- getConfig: () => config,
80
- getQuotaUsage: () => lastProviderUsage,
81
- getThinkingLevel: () => currentThinkingLevel,
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
- function startLiveTimer(ctx: ExtensionContext): void {
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 5-minute tick. */
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
- refreshFooter(ctx);
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
- refreshFooter(ctx);
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 5-minute timer tick.
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 tokens = args.trim().split(/\s+/).filter(Boolean);
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 speed = speedTracker.liveTokS();
398
- const usage = getCapacityUsage(ctx);
399
- const stage = config.capacity.icons.length
400
- ? config.capacity.labels[
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
- mutate({ ...config, enabled: first === "on" });
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
- if (first === "speed" && (second === "on" || second === "off")) {
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 crew-vibes speed + context meters (on/off, speed, capacity)",
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
- CAPACITY_STATUS_ID,
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).
@@ -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`. This hook does NOT
441
- * fire for crew workers: they are spawned with `--no-extensions`, so the
442
- * extension layer (and this hook) never loads in their process. Workers
443
- * instead receive knowledge via `buildKnowledgeFragment(task.cwd)` injected
444
- * into their prompt stablePrefix by `prompt-builder.ts`. Do NOT "fix" this
445
- * perceived gap by making the hook reach workers — it would cause
446
- * double-injection. (Verified by research workflow 2026-06-28.)
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