pi-crew 0.9.25 → 0.9.27

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 (59) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/NOTICE.md +14 -0
  3. package/assets/crew-vibes.ttf +0 -0
  4. package/assets/runner-spritesheet.png +0 -0
  5. package/dist/build-meta.json +255 -40
  6. package/dist/index.mjs +1564 -241
  7. package/dist/index.mjs.map +4 -4
  8. package/docs/perf/optimization-plan-2026-07-verified.md +396 -0
  9. package/package.json +10 -3
  10. package/scripts/bench-check.mjs +96 -0
  11. package/scripts/bench-cold-start.mjs +216 -0
  12. package/scripts/build-bundle.mjs +84 -0
  13. package/scripts/build-crew-vibes-font.py +328 -0
  14. package/scripts/check-all-skills.ts +294 -0
  15. package/scripts/check-bundle-staleness.mjs +97 -0
  16. package/scripts/check-conflict-markers.mjs +91 -0
  17. package/scripts/check-lazy-imports.mjs +28 -0
  18. package/scripts/install-crew-vibes-font.mjs +91 -0
  19. package/scripts/postinstall.mjs +47 -0
  20. package/scripts/profile-startup.mjs +125 -0
  21. package/scripts/release-smoke.mjs +74 -0
  22. package/scripts/run-bench.mjs +47 -0
  23. package/scripts/run-real-chain.ts +41 -0
  24. package/scripts/test-issue-29-crash.ts +111 -0
  25. package/scripts/test-issue-29-e2e.ts +183 -0
  26. package/scripts/test-issue-29-real-runtime.ts +330 -0
  27. package/scripts/test-issue-29-real-tasks.ts +387 -0
  28. package/scripts/test-issue-29-team-tool.ts +105 -0
  29. package/scripts/test-runner.mjs +74 -0
  30. package/scripts/verify-flicker-fix.ts +109 -0
  31. package/scripts/verify-skill.ts +550 -0
  32. package/scripts/watch-bundle.mjs +259 -0
  33. package/scripts/watchdog-harness.ts +119 -0
  34. package/src/agents/discover-agents.ts +6 -1
  35. package/src/config/defaults.ts +6 -0
  36. package/src/extension/crew-vibes/cat-frames.ts +18 -0
  37. package/src/extension/crew-vibes/config.ts +195 -0
  38. package/src/extension/crew-vibes/figures.ts +94 -0
  39. package/src/extension/crew-vibes/font-detect.ts +58 -0
  40. package/src/extension/crew-vibes/index.ts +396 -0
  41. package/src/extension/crew-vibes/provider-usage.ts +330 -0
  42. package/src/extension/crew-vibes/render.ts +206 -0
  43. package/src/extension/crew-vibes/speed.ts +286 -0
  44. package/src/extension/knowledge-injection.ts +12 -3
  45. package/src/extension/management.ts +23 -3
  46. package/src/extension/register.ts +7 -0
  47. package/src/runtime/child-pi.ts +117 -10
  48. package/src/runtime/crew-agent-records.ts +10 -3
  49. package/src/runtime/retry-executor.ts +4 -1
  50. package/src/runtime/task-runner/state-helpers.ts +9 -30
  51. package/src/runtime/team-runner.ts +5 -3
  52. package/src/state/atomic-write.ts +153 -49
  53. package/src/state/event-log.ts +28 -19
  54. package/src/state/locks.ts +7 -8
  55. package/src/state/mailbox.ts +15 -4
  56. package/src/state/state-store.ts +39 -10
  57. package/src/teams/discover-teams.ts +56 -1
  58. package/src/utils/safe-paths.ts +2 -1
  59. package/src/workflows/discover-workflows.ts +72 -1
@@ -114,12 +114,67 @@ function readTeamDir(dir: string, source: ResourceSource): TeamConfig[] {
114
114
  .sort((a, b) => a.name.localeCompare(b.name));
115
115
  }
116
116
 
117
+ // ─── Team Discovery Cache (F15) ──────────────────────────────────────────
118
+ // Mirrors the Workflow Discovery Cache (see workflows/discover-workflows.ts).
119
+ // `discoverTeams(cwd)` is called from recommend/scheduler paths and previously
120
+ // re-scanned 3 roots on every call. 5 s TTL + dir-stamp invalidation keeps
121
+ // changes (e.g. user edits a `.team.md`) visible within a few seconds without
122
+ // paying a full re-scan per call.
123
+ const TEAM_DISCOVERY_TTL_MS = 5000;
124
+ const TEAM_DISCOVERY_MAX_ENTRIES = 32;
125
+ interface CachedTeamEntry {
126
+ result: TeamDiscoveryResult;
127
+ expiresAt: number;
128
+ dirStamp: string;
129
+ }
130
+ const teamCache = new Map<string, CachedTeamEntry>();
131
+
132
+ function teamDirStamp(cwd: string): string {
133
+ const dirs = [path.join(packageRoot(), "teams"), path.join(userPiRoot(), "teams"), path.join(projectCrewRoot(cwd), "teams")];
134
+ let out = "";
135
+ for (const d of dirs) {
136
+ try {
137
+ out += `${fs.statSync(d).mtimeMs}|`;
138
+ } catch {
139
+ out += "0|";
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+
145
+ /** Drop one or all cached entries. Called from management actions and tests. */
146
+ export function invalidateTeamDiscoveryCache(cwd?: string): void {
147
+ if (cwd) {
148
+ teamCache.delete(cwd);
149
+ } else {
150
+ teamCache.clear();
151
+ }
152
+ }
153
+
117
154
  export function discoverTeams(cwd: string): TeamDiscoveryResult {
118
- return {
155
+ // F15: serve from cache when both TTL is fresh AND dir-stamp is unchanged.
156
+ const now = Date.now();
157
+ const stamp = teamDirStamp(cwd);
158
+ const cached = teamCache.get(cwd);
159
+ if (cached && cached.expiresAt > now && cached.dirStamp === stamp) {
160
+ return cached.result;
161
+ }
162
+ const result: TeamDiscoveryResult = {
119
163
  builtin: readTeamDir(path.join(packageRoot(), "teams"), "builtin"),
120
164
  user: readTeamDir(path.join(userPiRoot(), "teams"), "user"),
121
165
  project: readTeamDir(path.join(projectCrewRoot(cwd), "teams"), "project"),
122
166
  };
167
+ teamCache.set(cwd, {
168
+ result,
169
+ expiresAt: now + TEAM_DISCOVERY_TTL_MS,
170
+ dirStamp: stamp,
171
+ });
172
+ while (teamCache.size > TEAM_DISCOVERY_MAX_ENTRIES) {
173
+ const oldest = teamCache.keys().next().value;
174
+ if (oldest === undefined) break;
175
+ teamCache.delete(oldest);
176
+ }
177
+ return result;
123
178
  }
124
179
 
125
180
  export function allTeams(discovery: TeamDiscoveryResult | undefined): TeamConfig[] {
@@ -255,7 +255,8 @@ export function resolveRealContainedPath(baseDir: string, targetPath: string): s
255
255
  if (process.platform === "darwin") {
256
256
  const resolvedSymlink = resolvedAccumulated;
257
257
  const knownDarwinSymlinks = ["/var", "/tmp", "/etc", "/private/var", "/private/tmp", "/private/etc"];
258
- if (knownDarwinSymlinks.includes(resolvedSymlink)) continue;
258
+ // Use prefix match to handle subdirectories like /var/folders/...
259
+ if (knownDarwinSymlinks.some((s) => resolvedSymlink === s || resolvedSymlink.startsWith(s + "/"))) continue;
259
260
  }
260
261
  throw new Error("Refusing to resolve: target path ancestor is a symlink: " + resolvedAccumulated);
261
262
  }
@@ -198,15 +198,86 @@ function parseDynamicWorkflowFile(filePath: string, source: ResourceSource): Wor
198
198
  }
199
199
  }
200
200
 
201
+ // ─── Workflow Discovery Cache (F17) ──────────────────────────────────────
202
+ // Mirrors the Agent Discovery Cache pattern (see discover-agents.ts:493-556).
203
+ // `discoverWorkflows(cwd)` is called on the powerbar hot path (≤200 ms coalesce =
204
+ // up to ~5 Hz when a run is active + emitting events). Without caching, each call
205
+ // walks 3 roots, each requiring readdirSync × 2 + readFileSync + regex-parse per
206
+ // `.workflow.md`. We use TTL + dir-stamp invalidation so the cache survives
207
+ // steady-state rendering yet still picks up file changes within a few seconds.
208
+ //
209
+ // SECURITY: ResourceIdentity preserved — same precedence + same parsers as before.
210
+ // Reviewers: there is no path-component concern; this only memoises a pure function
211
+ // of the filesystem state into a process-local Map.
212
+ const WORKFLOW_DISCOVERY_TTL_MS = 5000;
213
+ const WORKFLOW_DISCOVERY_MAX_ENTRIES = 32;
214
+ interface CachedWorkflowEntry {
215
+ result: WorkflowDiscoveryResult;
216
+ expiresAt: number;
217
+ /** mtime tuple of the 3 workflow source dirs; change ⇒ invalidate early. */
218
+ dirStamp: string;
219
+ }
220
+ const workflowCache = new Map<string, CachedWorkflowEntry>();
221
+
222
+ /** Compact mtime signature for the 3 workflow source dirs. Cheap (3 statSync). */
223
+ function workflowDirStamp(cwd: string): string {
224
+ const dirs = [
225
+ path.join(packageRoot(), "workflows"),
226
+ path.join(userPiRoot(), "workflows"),
227
+ path.join(projectCrewRoot(cwd), "workflows"),
228
+ ];
229
+ let out = "";
230
+ for (const d of dirs) {
231
+ try {
232
+ const st = fs.statSync(d);
233
+ out += `${st.mtimeMs}|`;
234
+ } catch {
235
+ out += "0|";
236
+ }
237
+ }
238
+ return out;
239
+ }
240
+
241
+ /** Drop one or all cached entries. Called from management actions and tests. */
242
+ export function invalidateWorkflowDiscoveryCache(cwd?: string): void {
243
+ if (cwd) {
244
+ workflowCache.delete(cwd);
245
+ } else {
246
+ workflowCache.clear();
247
+ }
248
+ }
249
+
201
250
  export function discoverWorkflows(cwd: string): WorkflowDiscoveryResult {
202
251
  if (!cwd || typeof cwd !== "string") {
203
252
  return { builtin: [], user: [], project: [] };
204
253
  }
205
- return {
254
+ // F17: serve from cache when both TTL is fresh AND dir-stamp is unchanged.
255
+ // `dirStamp` is a 3-stat pulse we run on every cached call — it is much
256
+ // cheaper than the disk scan it protects against (3 statSync vs 2 readdirSync
257
+ // + N readFileSync + regex parse per `.workflow.md`).
258
+ const now = Date.now();
259
+ const stamp = workflowDirStamp(cwd);
260
+ const cached = workflowCache.get(cwd);
261
+ if (cached && cached.expiresAt > now && cached.dirStamp === stamp) {
262
+ return cached.result;
263
+ }
264
+ const result: WorkflowDiscoveryResult = {
206
265
  builtin: readWorkflowDir(path.join(packageRoot(), "workflows"), "builtin"),
207
266
  user: readWorkflowDir(path.join(userPiRoot(), "workflows"), "user"),
208
267
  project: readWorkflowDir(path.join(projectCrewRoot(cwd), "workflows"), "project"),
209
268
  };
269
+ workflowCache.set(cwd, {
270
+ result,
271
+ expiresAt: now + WORKFLOW_DISCOVERY_TTL_MS,
272
+ dirStamp: stamp,
273
+ });
274
+ // Bounded LRU-ish eviction by insertion order (Map iteration order).
275
+ while (workflowCache.size > WORKFLOW_DISCOVERY_MAX_ENTRIES) {
276
+ const oldest = workflowCache.keys().next().value;
277
+ if (oldest === undefined) break;
278
+ workflowCache.delete(oldest);
279
+ }
280
+ return result;
210
281
  }
211
282
 
212
283
  export function allWorkflows(discovery: WorkflowDiscoveryResult | undefined): WorkflowConfig[] {