pi-crew 0.1.51 → 0.2.1

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 (240) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +176 -781
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +70 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/actions-reference.md +595 -0
  14. package/docs/commands-reference.md +347 -0
  15. package/docs/runtime-flow.md +148 -148
  16. package/index.ts +6 -6
  17. package/package.json +99 -99
  18. package/skills/async-worker-recovery/SKILL.md +42 -42
  19. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  20. package/skills/delegation-patterns/SKILL.md +54 -54
  21. package/skills/mailbox-interactive/SKILL.md +40 -40
  22. package/skills/model-routing-context/SKILL.md +39 -39
  23. package/skills/multi-perspective-review/SKILL.md +58 -58
  24. package/skills/observability-reliability/SKILL.md +41 -41
  25. package/skills/orchestration/SKILL.md +157 -157
  26. package/skills/ownership-session-security/SKILL.md +41 -41
  27. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  28. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  29. package/skills/resource-discovery-config/SKILL.md +41 -41
  30. package/skills/runtime-state-reader/SKILL.md +44 -44
  31. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  32. package/skills/state-mutation-locking/SKILL.md +42 -42
  33. package/skills/systematic-debugging/SKILL.md +67 -67
  34. package/skills/ui-render-performance/SKILL.md +39 -39
  35. package/skills/verification-before-done/SKILL.md +57 -57
  36. package/skills/worktree-isolation/SKILL.md +39 -39
  37. package/src/adapters/claude-adapter.ts +25 -0
  38. package/src/adapters/codex-adapter.ts +21 -0
  39. package/src/adapters/cursor-adapter.ts +17 -0
  40. package/src/adapters/export-util.ts +137 -0
  41. package/src/adapters/index.ts +15 -0
  42. package/src/adapters/registry.ts +18 -0
  43. package/src/adapters/types.ts +23 -0
  44. package/src/agents/agent-config.ts +2 -0
  45. package/src/agents/agent-search.ts +98 -98
  46. package/src/agents/discover-agents.ts +2 -1
  47. package/src/config/config.ts +13 -1
  48. package/src/config/drift-detector.ts +211 -0
  49. package/src/config/markers.ts +327 -0
  50. package/src/config/resilient-parser.ts +108 -0
  51. package/src/config/suggestions.ts +74 -0
  52. package/src/extension/cross-extension-rpc.ts +103 -94
  53. package/src/extension/project-init.ts +21 -1
  54. package/src/extension/register.ts +45 -14
  55. package/src/extension/registration/commands.ts +77 -8
  56. package/src/extension/registration/subagent-tools.ts +10 -1
  57. package/src/extension/registration/team-tool.ts +10 -1
  58. package/src/extension/registration/viewers.ts +48 -34
  59. package/src/extension/run-bundle-schema.ts +89 -89
  60. package/src/extension/run-import.ts +25 -1
  61. package/src/extension/run-index.ts +5 -1
  62. package/src/extension/run-maintenance.ts +142 -68
  63. package/src/extension/team-manager-command.ts +10 -1
  64. package/src/extension/team-tool/api.ts +441 -441
  65. package/src/extension/team-tool/doctor.ts +28 -3
  66. package/src/extension/team-tool/handle-settings.ts +195 -188
  67. package/src/extension/team-tool/inspect.ts +41 -41
  68. package/src/extension/team-tool/intent-policy.ts +42 -42
  69. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  70. package/src/extension/team-tool/plan.ts +19 -19
  71. package/src/extension/team-tool/run.ts +12 -1
  72. package/src/extension/team-tool.ts +332 -322
  73. package/src/i18n.ts +184 -184
  74. package/src/observability/exporters/otlp-exporter.ts +92 -77
  75. package/src/prompt/prompt-runtime.ts +72 -72
  76. package/src/runtime/agent-memory.ts +72 -72
  77. package/src/runtime/agent-observability.ts +114 -114
  78. package/src/runtime/async-marker.ts +26 -26
  79. package/src/runtime/attention-events.ts +28 -28
  80. package/src/runtime/auto-resume.ts +100 -0
  81. package/src/runtime/background-runner.ts +11 -1
  82. package/src/runtime/cancellation-token.ts +89 -89
  83. package/src/runtime/cancellation.ts +61 -61
  84. package/src/runtime/capability-inventory.ts +116 -116
  85. package/src/runtime/child-pi.ts +7 -2
  86. package/src/runtime/compaction-summary.ts +271 -0
  87. package/src/runtime/completion-guard.ts +190 -190
  88. package/src/runtime/crash-recovery.ts +33 -1
  89. package/src/runtime/delta-conflict.ts +360 -0
  90. package/src/runtime/direct-run.ts +35 -35
  91. package/src/runtime/foreground-control.ts +82 -82
  92. package/src/runtime/green-contract.ts +46 -46
  93. package/src/runtime/group-join.ts +106 -106
  94. package/src/runtime/heartbeat-gradient.ts +28 -28
  95. package/src/runtime/heartbeat-watcher.ts +124 -124
  96. package/src/runtime/iteration-hooks.ts +264 -0
  97. package/src/runtime/live-agent-control.ts +88 -88
  98. package/src/runtime/live-control-realtime.ts +36 -36
  99. package/src/runtime/live-extension-bridge.ts +150 -150
  100. package/src/runtime/live-irc.ts +92 -92
  101. package/src/runtime/live-session-health.ts +100 -100
  102. package/src/runtime/loop-gates.ts +129 -0
  103. package/src/runtime/metric-parser.ts +40 -0
  104. package/src/runtime/notebook-helpers.ts +90 -90
  105. package/src/runtime/orphan-sentinel.ts +7 -7
  106. package/src/runtime/parallel-research.ts +44 -44
  107. package/src/runtime/phase-progress.ts +217 -0
  108. package/src/runtime/pi-args.ts +38 -11
  109. package/src/runtime/pi-json-output.ts +111 -111
  110. package/src/runtime/pi-spawn.ts +57 -7
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/post-checks.ts +122 -0
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/prose-compressor.ts +164 -164
  115. package/src/runtime/recovery-recipes.ts +74 -74
  116. package/src/runtime/result-extractor.ts +121 -121
  117. package/src/runtime/role-permission.ts +39 -39
  118. package/src/runtime/sensitive-paths.ts +2 -2
  119. package/src/runtime/session-resources.ts +25 -25
  120. package/src/runtime/session-snapshot.ts +59 -59
  121. package/src/runtime/session-usage.ts +79 -79
  122. package/src/runtime/sidechain-output.ts +29 -29
  123. package/src/runtime/stream-preview.ts +177 -177
  124. package/src/runtime/supervisor-contact.ts +59 -59
  125. package/src/runtime/task-display.ts +38 -38
  126. package/src/runtime/task-graph.ts +207 -0
  127. package/src/runtime/task-quality.ts +207 -0
  128. package/src/runtime/task-runner/capabilities.ts +78 -78
  129. package/src/runtime/task-runner/live-executor.ts +7 -1
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  132. package/src/runtime/task-runner/result-utils.ts +14 -14
  133. package/src/runtime/task-runner/run-projection.ts +103 -103
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/team-runner.ts +117 -7
  136. package/src/runtime/worker-heartbeat.ts +21 -21
  137. package/src/runtime/worker-startup.ts +57 -57
  138. package/src/runtime/workflow-state.ts +187 -0
  139. package/src/runtime/workspace-tree.ts +298 -298
  140. package/src/schema/config-schema.ts +11 -0
  141. package/src/schema/validation-types.ts +148 -0
  142. package/src/skills/skill-templates.ts +374 -0
  143. package/src/state/active-run-registry.ts +35 -11
  144. package/src/state/atomic-write.ts +33 -26
  145. package/src/state/contracts.ts +1 -0
  146. package/src/state/event-reconstructor.ts +217 -0
  147. package/src/state/locks.ts +2 -13
  148. package/src/state/mailbox.ts +4 -3
  149. package/src/state/state-store.ts +16 -6
  150. package/src/state/task-claims.ts +44 -44
  151. package/src/state/types.ts +9 -0
  152. package/src/state/usage.ts +29 -29
  153. package/src/subagents/async-entry.ts +1 -1
  154. package/src/subagents/index.ts +3 -3
  155. package/src/subagents/live/control.ts +1 -1
  156. package/src/subagents/live/manager.ts +1 -1
  157. package/src/subagents/live/realtime.ts +1 -1
  158. package/src/subagents/live/session-runtime.ts +1 -1
  159. package/src/subagents/manager.ts +1 -1
  160. package/src/subagents/spawn.ts +1 -1
  161. package/src/teams/team-serializer.ts +38 -38
  162. package/src/types/diff.d.ts +18 -18
  163. package/src/ui/crew-footer.ts +101 -101
  164. package/src/ui/crew-select-list.ts +111 -111
  165. package/src/ui/crew-widget.ts +5 -2
  166. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  167. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  168. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  169. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  170. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  171. package/src/ui/dynamic-border.ts +25 -25
  172. package/src/ui/layout-primitives.ts +106 -106
  173. package/src/ui/loaders.ts +158 -158
  174. package/src/ui/render-coalescer.ts +51 -51
  175. package/src/ui/render-diff.ts +119 -119
  176. package/src/ui/render-scheduler.ts +143 -143
  177. package/src/ui/run-action-dispatcher.ts +10 -1
  178. package/src/ui/spinner.ts +17 -17
  179. package/src/ui/status-colors.ts +58 -58
  180. package/src/ui/syntax-highlight.ts +116 -116
  181. package/src/ui/transcript-entries.ts +258 -258
  182. package/src/utils/completion-dedupe.ts +63 -63
  183. package/src/utils/frontmatter.ts +68 -68
  184. package/src/utils/git.ts +262 -262
  185. package/src/utils/ids.ts +17 -17
  186. package/src/utils/incremental-reader.ts +104 -104
  187. package/src/utils/names.ts +27 -27
  188. package/src/utils/redaction.ts +44 -44
  189. package/src/utils/safe-paths.ts +47 -47
  190. package/src/utils/scan-cache.ts +136 -136
  191. package/src/utils/sleep.ts +40 -26
  192. package/src/utils/task-name-generator.ts +337 -337
  193. package/src/workflows/validate-workflow.ts +40 -40
  194. package/src/worktree/branch-freshness.ts +45 -45
  195. package/teams/default.team.md +12 -12
  196. package/teams/fast-fix.team.md +11 -11
  197. package/teams/implementation.team.md +18 -18
  198. package/teams/parallel-research.team.md +14 -14
  199. package/teams/research.team.md +11 -11
  200. package/teams/review.team.md +12 -12
  201. package/workflows/default.workflow.md +30 -29
  202. package/workflows/fast-fix.workflow.md +23 -22
  203. package/workflows/implementation.workflow.md +43 -43
  204. package/workflows/parallel-research.workflow.md +46 -46
  205. package/workflows/research.workflow.md +22 -22
  206. package/workflows/review.workflow.md +30 -30
  207. package/docs/refactor-tasks-phase3.md +0 -394
  208. package/docs/refactor-tasks-phase4.md +0 -564
  209. package/docs/refactor-tasks-phase5.md +0 -402
  210. package/docs/refactor-tasks-phase6.md +0 -662
  211. package/docs/refactor-tasks.md +0 -1484
  212. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  213. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  214. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  215. package/docs/research/AUDIT_PI_CREW.md +0 -457
  216. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  217. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  218. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  219. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  220. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  221. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  222. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  223. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  224. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  225. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  226. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  227. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  228. package/docs/research-extension-examples.md +0 -297
  229. package/docs/research-extension-system.md +0 -324
  230. package/docs/research-oh-my-pi-distillation.md +0 -369
  231. package/docs/research-optimization-plan.md +0 -548
  232. package/docs/research-phase10-distillation.md +0 -199
  233. package/docs/research-phase11-distillation.md +0 -201
  234. package/docs/research-phase8-operator-experience-plan.md +0 -819
  235. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  236. package/docs/research-pi-coding-agent.md +0 -357
  237. package/docs/research-source-pi-crew-reference.md +0 -174
  238. package/docs/research-ui-optimization-plan.md +0 -480
  239. package/docs/source-runtime-refactor-map.md +0 -107
  240. package/src/utils/atomic-write.ts +0 -33
@@ -1,137 +1,137 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
-
4
- export interface ScanEntry {
5
- /** Unique key for this entry (e.g., runId, artifactPath) */
6
- key: string;
7
- /** Filesystem path */
8
- path: string;
9
- /** Raw content (parsed JSON or text) */
10
- raw: unknown;
11
- /** File modification time */
12
- mtimeMs: number;
13
- /** File size in bytes */
14
- sizeBytes: number;
15
- /** When this entry was loaded */
16
- loadedAtMs: number;
17
- }
18
-
19
- export interface ScanCacheOptions {
20
- /** TTL in ms for cached entries. Default 1000. */
21
- ttlMs?: number;
22
- /** Maximum number of entries. Default 100. */
23
- maxEntries?: number;
24
- }
25
-
26
- interface CacheBucket {
27
- entries: Map<string, ScanEntry>;
28
- expireAtMs: number;
29
- }
30
-
31
- /**
32
- * Shared raw scan-entry cache for runs, artifacts, mailbox, transcripts.
33
- * Provides deterministic sort order and invalidation on mutation.
34
- */
35
- export class SharedScanCache {
36
- #buckets = new Map<string, CacheBucket>();
37
- #ttlMs: number;
38
- #maxEntries: number;
39
- #now: () => number;
40
-
41
- constructor(options: ScanCacheOptions = {}) {
42
- this.#ttlMs = options.ttlMs ?? 1000;
43
- this.#maxEntries = options.maxEntries ?? 100;
44
- this.#now = () => Date.now();
45
- }
46
-
47
- /** Get a cached entry by bucket and key. Returns undefined if not cached or expired. */
48
- get(bucket: string, key: string): ScanEntry | undefined {
49
- const b = this.#buckets.get(bucket);
50
- if (!b) return undefined;
51
- if (this.#now() > b.expireAtMs) {
52
- this.#buckets.delete(bucket);
53
- return undefined;
54
- }
55
- return b.entries.get(key);
56
- }
57
-
58
- /** Get all entries in a bucket. Returns empty array if expired or missing. */
59
- list(bucket: string): ScanEntry[] {
60
- const b = this.#buckets.get(bucket);
61
- if (!b) return [];
62
- if (this.#now() > b.expireAtMs) {
63
- this.#buckets.delete(bucket);
64
- return [];
65
- }
66
- return [...b.entries.values()].sort((a, b) => a.key.localeCompare(b.key));
67
- }
68
-
69
- /** Set an entry in a bucket. */
70
- set(bucket: string, entry: ScanEntry): void {
71
- let b = this.#buckets.get(bucket);
72
- if (!b || this.#now() > b.expireAtMs) {
73
- b = { entries: new Map(), expireAtMs: this.#now() + this.#ttlMs };
74
- this.#buckets.set(bucket, b);
75
- }
76
- if (b.entries.size >= this.#maxEntries) {
77
- // Evict oldest entry
78
- const firstKey = b.entries.keys().next().value;
79
- if (firstKey !== undefined) b.entries.delete(firstKey);
80
- }
81
- b.entries.set(entry.key, entry);
82
- }
83
-
84
- /** Invalidate a specific key in a bucket. */
85
- invalidate(bucket: string, key: string): void {
86
- const b = this.#buckets.get(bucket);
87
- if (b) b.entries.delete(key);
88
- }
89
-
90
- /** Invalidate an entire bucket. */
91
- invalidateBucket(bucket: string): void {
92
- this.#buckets.delete(bucket);
93
- }
94
-
95
- /** Invalidate all buckets. */
96
- clear(): void {
97
- this.#buckets.clear();
98
- }
99
-
100
- /** Read a file, parse if JSON, and cache the result. */
101
- readAndCache(bucket: string, key: string, filePath: string, parseJson = true): ScanEntry | undefined {
102
- try {
103
- if (!fs.existsSync(filePath)) return undefined;
104
- const stat = fs.statSync(filePath);
105
- const cached = this.get(bucket, key);
106
- if (cached && cached.mtimeMs >= stat.mtimeMs && cached.sizeBytes === stat.size) return cached;
107
- const content = fs.readFileSync(filePath, "utf-8");
108
- const raw = parseJson ? JSON.parse(content) : content;
109
- const entry: ScanEntry = { key, path: filePath, raw, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, loadedAtMs: this.#now() };
110
- this.set(bucket, entry);
111
- return entry;
112
- } catch {
113
- return undefined;
114
- }
115
- }
116
-
117
- /** Read a directory and cache entries for each file. */
118
- scanAndCache(bucket: string, dirPath: string, parseJson = true): ScanEntry[] {
119
- try {
120
- if (!fs.existsSync(dirPath)) return [];
121
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
122
- const results: ScanEntry[] = [];
123
- for (const entry of entries) {
124
- if (!entry.isFile()) continue;
125
- const filePath = path.join(dirPath, entry.name);
126
- const cached = this.readAndCache(bucket, entry.name, filePath, parseJson);
127
- if (cached) results.push(cached);
128
- }
129
- return results.sort((a, b) => a.key.localeCompare(b.key));
130
- } catch {
131
- return [];
132
- }
133
- }
134
- }
135
-
136
- /** Global shared scan cache instance. */
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface ScanEntry {
5
+ /** Unique key for this entry (e.g., runId, artifactPath) */
6
+ key: string;
7
+ /** Filesystem path */
8
+ path: string;
9
+ /** Raw content (parsed JSON or text) */
10
+ raw: unknown;
11
+ /** File modification time */
12
+ mtimeMs: number;
13
+ /** File size in bytes */
14
+ sizeBytes: number;
15
+ /** When this entry was loaded */
16
+ loadedAtMs: number;
17
+ }
18
+
19
+ export interface ScanCacheOptions {
20
+ /** TTL in ms for cached entries. Default 1000. */
21
+ ttlMs?: number;
22
+ /** Maximum number of entries. Default 100. */
23
+ maxEntries?: number;
24
+ }
25
+
26
+ interface CacheBucket {
27
+ entries: Map<string, ScanEntry>;
28
+ expireAtMs: number;
29
+ }
30
+
31
+ /**
32
+ * Shared raw scan-entry cache for runs, artifacts, mailbox, transcripts.
33
+ * Provides deterministic sort order and invalidation on mutation.
34
+ */
35
+ export class SharedScanCache {
36
+ #buckets = new Map<string, CacheBucket>();
37
+ #ttlMs: number;
38
+ #maxEntries: number;
39
+ #now: () => number;
40
+
41
+ constructor(options: ScanCacheOptions = {}) {
42
+ this.#ttlMs = options.ttlMs ?? 1000;
43
+ this.#maxEntries = options.maxEntries ?? 100;
44
+ this.#now = () => Date.now();
45
+ }
46
+
47
+ /** Get a cached entry by bucket and key. Returns undefined if not cached or expired. */
48
+ get(bucket: string, key: string): ScanEntry | undefined {
49
+ const b = this.#buckets.get(bucket);
50
+ if (!b) return undefined;
51
+ if (this.#now() > b.expireAtMs) {
52
+ this.#buckets.delete(bucket);
53
+ return undefined;
54
+ }
55
+ return b.entries.get(key);
56
+ }
57
+
58
+ /** Get all entries in a bucket. Returns empty array if expired or missing. */
59
+ list(bucket: string): ScanEntry[] {
60
+ const b = this.#buckets.get(bucket);
61
+ if (!b) return [];
62
+ if (this.#now() > b.expireAtMs) {
63
+ this.#buckets.delete(bucket);
64
+ return [];
65
+ }
66
+ return [...b.entries.values()].sort((a, b) => a.key.localeCompare(b.key));
67
+ }
68
+
69
+ /** Set an entry in a bucket. */
70
+ set(bucket: string, entry: ScanEntry): void {
71
+ let b = this.#buckets.get(bucket);
72
+ if (!b || this.#now() > b.expireAtMs) {
73
+ b = { entries: new Map(), expireAtMs: this.#now() + this.#ttlMs };
74
+ this.#buckets.set(bucket, b);
75
+ }
76
+ if (b.entries.size >= this.#maxEntries) {
77
+ // Evict oldest entry
78
+ const firstKey = b.entries.keys().next().value;
79
+ if (firstKey !== undefined) b.entries.delete(firstKey);
80
+ }
81
+ b.entries.set(entry.key, entry);
82
+ }
83
+
84
+ /** Invalidate a specific key in a bucket. */
85
+ invalidate(bucket: string, key: string): void {
86
+ const b = this.#buckets.get(bucket);
87
+ if (b) b.entries.delete(key);
88
+ }
89
+
90
+ /** Invalidate an entire bucket. */
91
+ invalidateBucket(bucket: string): void {
92
+ this.#buckets.delete(bucket);
93
+ }
94
+
95
+ /** Invalidate all buckets. */
96
+ clear(): void {
97
+ this.#buckets.clear();
98
+ }
99
+
100
+ /** Read a file, parse if JSON, and cache the result. */
101
+ readAndCache(bucket: string, key: string, filePath: string, parseJson = true): ScanEntry | undefined {
102
+ try {
103
+ if (!fs.existsSync(filePath)) return undefined;
104
+ const stat = fs.statSync(filePath);
105
+ const cached = this.get(bucket, key);
106
+ if (cached && cached.mtimeMs >= stat.mtimeMs && cached.sizeBytes === stat.size) return cached;
107
+ const content = fs.readFileSync(filePath, "utf-8");
108
+ const raw = parseJson ? JSON.parse(content) : content;
109
+ const entry: ScanEntry = { key, path: filePath, raw, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, loadedAtMs: this.#now() };
110
+ this.set(bucket, entry);
111
+ return entry;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ }
116
+
117
+ /** Read a directory and cache entries for each file. */
118
+ scanAndCache(bucket: string, dirPath: string, parseJson = true): ScanEntry[] {
119
+ try {
120
+ if (!fs.existsSync(dirPath)) return [];
121
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
122
+ const results: ScanEntry[] = [];
123
+ for (const entry of entries) {
124
+ if (!entry.isFile()) continue;
125
+ const filePath = path.join(dirPath, entry.name);
126
+ const cached = this.readAndCache(bucket, entry.name, filePath, parseJson);
127
+ if (cached) results.push(cached);
128
+ }
129
+ return results.sort((a, b) => a.key.localeCompare(b.key));
130
+ } catch {
131
+ return [];
132
+ }
133
+ }
134
+ }
135
+
136
+ /** Global shared scan cache instance. */
137
137
  export const sharedScanCache = new SharedScanCache();
@@ -1,32 +1,46 @@
1
1
  /**
2
- * Sleep helper that respects abort signal.
2
+ * Synchronous sleep using Atomics.wait (non-busy) with low-CPU fallback.
3
+ *
4
+ * WARNING: This blocks the Node.js main thread. Only use in sync I/O paths
5
+ * where blocking is acceptable (lock acquisition, rename retry).
6
+ * NOT safe to call from Pi extension async code paths.
3
7
  */
4
- export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
5
- return new Promise((resolve, reject) => {
6
- if (signal?.aborted) {
7
- reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
8
- return;
8
+ export function sleepSync(ms: number): void {
9
+ try {
10
+ const buffer = new SharedArrayBuffer(4);
11
+ Atomics.wait(new Int32Array(buffer), 0, 0, ms);
12
+ } catch {
13
+ // Fallback for environments without Atomics.wait (e.g. Windows).
14
+ // On Unix, try spawning the `sleep` command to avoid CPU busy-wait.
15
+ if (process.platform !== "win32" && ms >= 10) {
16
+ try {
17
+ const { execFileSync } = require("node:child_process") as typeof import("node:child_process");
18
+ execFileSync("sleep", [(ms / 1000).toFixed(3)], { timeout: ms + 1000, stdio: "pipe" });
19
+ return;
20
+ } catch {
21
+ // sleep command unavailable — fall through to busy-wait
22
+ }
9
23
  }
24
+ // Last resort: busy-wait. This burns CPU but is the only guaranteed
25
+ // synchronous delay on Windows without SharedArrayBuffer.
26
+ const deadline = Date.now() + ms;
27
+ while (Date.now() < deadline) {
28
+ // Busy-wait fallback for environments without Atomics.wait.
29
+ }
30
+ }
31
+ }
10
32
 
11
- let settled = false;
12
- const cleanup = (): void => {
13
- if (signal) signal.removeEventListener("abort", onAbort);
14
- };
15
- const timeout = setTimeout(() => {
16
- if (settled) return;
17
- settled = true;
18
- cleanup();
19
- resolve();
20
- }, ms);
21
-
22
- const onAbort = (): void => {
23
- if (settled) return;
24
- settled = true;
25
- clearTimeout(timeout);
26
- cleanup();
27
- reject(signal?.reason instanceof Error ? signal.reason : new Error("Aborted"));
28
- };
29
-
30
- signal?.addEventListener("abort", onAbort);
33
+ /**
34
+ * Async sleep with optional AbortSignal support.
35
+ * Rejects immediately if the signal is already aborted, or when aborted during wait.
36
+ */
37
+ export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
38
+ if (signal?.aborted) return Promise.reject(new Error("aborted"));
39
+ return new Promise<void>((resolve, reject) => {
40
+ const timer = setTimeout(resolve, ms);
41
+ signal?.addEventListener("abort", () => {
42
+ clearTimeout(timer);
43
+ reject(new Error("aborted"));
44
+ }, { once: true });
31
45
  });
32
46
  }