pi-harness-runtime 1.1.95 → 1.1.97

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 (226) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/harness/loop-runtime.ts +3 -0
  3. package/index.ts +130 -29
  4. package/package.json +2 -1
  5. package/packages/a2a-adapter/package.json +1 -1
  6. package/packages/architecture-generator/package.json +1 -1
  7. package/packages/auth/package.json +1 -1
  8. package/packages/autonomous-refactor/package.json +1 -1
  9. package/packages/autonomous-runtime/package.json +1 -1
  10. package/packages/cache-strategy/package.json +1 -1
  11. package/packages/capability-registry/package.json +1 -1
  12. package/packages/checkpoint/package.json +1 -1
  13. package/packages/cli-plugin-sdk/package.json +1 -1
  14. package/packages/clipboard/package.json +1 -1
  15. package/packages/clipboard-plugin/package.json +1 -1
  16. package/packages/code-generation/package.json +1 -1
  17. package/packages/code-review/package.json +1 -1
  18. package/packages/codex-adapter/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/cookie-sanitizer/package.json +1 -1
  24. package/packages/cost-optimizer/package.json +1 -1
  25. package/packages/dependency-analyzer/package.json +1 -1
  26. package/packages/django-plugin/package.json +1 -1
  27. package/packages/doc-generator/package.json +1 -1
  28. package/packages/evaluation-engine/package.json +1 -1
  29. package/packages/evaluation-runner/package.json +1 -1
  30. package/packages/event-bus/package.json +1 -1
  31. package/packages/event-store/package.json +1 -1
  32. package/packages/experience-replay/package.json +1 -1
  33. package/packages/feedback-collector/package.json +1 -1
  34. package/packages/file-copy-helper/package.json +1 -1
  35. package/packages/framework-detector/package.json +1 -1
  36. package/packages/framework-plugin-sdk/package.json +1 -1
  37. package/packages/frappe-plugin/package.json +1 -1
  38. package/packages/generic-web-plugin/package.json +1 -1
  39. package/packages/health-monitor/package.json +1 -1
  40. package/packages/intent-analyzer/package.json +1 -1
  41. package/packages/jev-judge/package.json +1 -1
  42. package/packages/knowledge-graph/package.json +1 -1
  43. package/packages/knowledge-retrieval/package.json +1 -1
  44. package/packages/laravel-plugin/package.json +1 -1
  45. package/packages/learning-engine/package.json +1 -1
  46. package/packages/mcp-adapter/package.json +1 -1
  47. package/packages/memory-engine/package.json +1 -1
  48. package/packages/milestone-manager/package.json +1 -1
  49. package/packages/model-registry/package.json +1 -1
  50. package/packages/nextjs-plugin/package.json +1 -1
  51. package/packages/notification/package.json +1 -1
  52. package/packages/observability/package.json +1 -1
  53. package/packages/okf-indexer/package.json +1 -1
  54. package/packages/performance-optimizer/package.json +1 -1
  55. package/packages/privilege-broker/package.json +1 -1
  56. package/packages/project-analyzer/package.json +1 -1
  57. package/packages/project-bootstrap/package.json +1 -1
  58. package/packages/projection-engine/package.json +1 -1
  59. package/packages/prompt-compiler/package.json +1 -1
  60. package/packages/prompt-versioning/package.json +1 -1
  61. package/packages/provider-adapter-sdk/package.json +1 -1
  62. package/packages/provider-router/package.json +1 -1
  63. package/packages/provider-selector/package.json +1 -1
  64. package/packages/providers/package.json +1 -1
  65. package/packages/qdrant-skills/dist/client.d.ts +51 -0
  66. package/packages/qdrant-skills/dist/client.d.ts.map +1 -0
  67. package/packages/qdrant-skills/dist/client.js +105 -0
  68. package/packages/qdrant-skills/dist/client.js.map +1 -0
  69. package/packages/qdrant-skills/dist/embedder.d.ts +39 -0
  70. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -0
  71. package/packages/qdrant-skills/dist/embedder.js +101 -0
  72. package/packages/qdrant-skills/dist/embedder.js.map +1 -0
  73. package/packages/qdrant-skills/dist/index.d.ts +25 -0
  74. package/packages/qdrant-skills/dist/index.d.ts.map +1 -0
  75. package/packages/qdrant-skills/dist/index.js +25 -0
  76. package/packages/qdrant-skills/dist/index.js.map +1 -0
  77. package/packages/qdrant-skills/package.json +27 -0
  78. package/packages/qdrant-skills/src/client.ts +178 -0
  79. package/packages/qdrant-skills/src/embedder.ts +137 -0
  80. package/packages/qdrant-skills/src/index.ts +42 -0
  81. package/packages/quota-manager/package.json +1 -1
  82. package/packages/rate-limiter/package.json +1 -1
  83. package/packages/react-vite-plugin/package.json +1 -1
  84. package/packages/release-manager/package.json +1 -1
  85. package/packages/requirement-compiler/package.json +1 -1
  86. package/packages/runtime/package.json +1 -1
  87. package/packages/scheduler/package.json +1 -1
  88. package/packages/scheduler-adapter/package.json +1 -1
  89. package/packages/session/package.json +1 -1
  90. package/packages/session-api/package.json +1 -1
  91. package/packages/session-export/package.json +1 -1
  92. package/packages/shared-context/package.json +1 -1
  93. package/packages/skill-mcp-client/package.json +1 -1
  94. package/packages/skill-registry/package.json +1 -1
  95. package/packages/skills/dist/index.d.ts +40 -0
  96. package/packages/skills/dist/index.d.ts.map +1 -0
  97. package/packages/skills/dist/index.js +84 -0
  98. package/packages/skills/dist/index.js.map +1 -0
  99. package/packages/skills/dist/linter.d.ts +38 -0
  100. package/packages/skills/dist/linter.d.ts.map +1 -0
  101. package/packages/skills/dist/linter.js +323 -0
  102. package/packages/skills/dist/linter.js.map +1 -0
  103. package/packages/skills/dist/loader.d.ts +74 -0
  104. package/packages/skills/dist/loader.d.ts.map +1 -0
  105. package/packages/skills/dist/loader.js +280 -0
  106. package/packages/skills/dist/loader.js.map +1 -0
  107. package/packages/skills/dist/parser.d.ts +32 -0
  108. package/packages/skills/dist/parser.d.ts.map +1 -0
  109. package/packages/skills/dist/parser.js +459 -0
  110. package/packages/skills/dist/parser.js.map +1 -0
  111. package/packages/skills/dist/scanner.d.ts +33 -0
  112. package/packages/skills/dist/scanner.d.ts.map +1 -0
  113. package/packages/skills/dist/scanner.js +257 -0
  114. package/packages/skills/dist/scanner.js.map +1 -0
  115. package/packages/skills/dist/skill-commands.d.ts +33 -0
  116. package/packages/skills/dist/skill-commands.d.ts.map +1 -0
  117. package/packages/skills/dist/skill-commands.js +239 -0
  118. package/packages/skills/dist/skill-commands.js.map +1 -0
  119. package/packages/skills/dist/skill-tool.d.ts +120 -0
  120. package/packages/skills/dist/skill-tool.d.ts.map +1 -0
  121. package/packages/skills/dist/skill-tool.js +239 -0
  122. package/packages/skills/dist/skill-tool.js.map +1 -0
  123. package/packages/skills/dist/skill-versioning.d.ts +123 -0
  124. package/packages/skills/dist/skill-versioning.d.ts.map +1 -0
  125. package/packages/skills/dist/skill-versioning.js +240 -0
  126. package/packages/skills/dist/skill-versioning.js.map +1 -0
  127. package/packages/skills/dist/types.d.ts +226 -0
  128. package/packages/skills/dist/types.d.ts.map +1 -0
  129. package/packages/skills/dist/types.js +21 -0
  130. package/packages/skills/dist/types.js.map +1 -0
  131. package/packages/skills/package.json +46 -0
  132. package/packages/skills/src/index.ts +95 -0
  133. package/packages/skills/src/linter.ts +371 -0
  134. package/packages/skills/src/loader.ts +335 -0
  135. package/packages/skills/src/parser.ts +548 -0
  136. package/packages/skills/src/scanner.ts +322 -0
  137. package/packages/skills/src/skill-commands.ts +318 -0
  138. package/packages/skills/src/skill-tool.ts +324 -0
  139. package/packages/skills/src/skill-versioning.ts +345 -0
  140. package/packages/skills/src/types.ts +259 -0
  141. package/packages/skills/test/init-skills.test.ts +118 -0
  142. package/packages/sprint-planner/package.json +1 -1
  143. package/packages/subscription-engine/package.json +1 -1
  144. package/packages/task-compiler/package.json +1 -1
  145. package/packages/tencentdb-memory/package.json +1 -1
  146. package/packages/tencentdb-sync/package.json +1 -1
  147. package/packages/test-data-generator/package.json +1 -1
  148. package/packages/test-generator/package.json +1 -1
  149. package/packages/todo-bd-sync/package.json +1 -1
  150. package/packages/token-estimation/package.json +1 -1
  151. package/packages/token-optimizer/package.json +1 -1
  152. package/packages/trajectory/dist/src/curator.d.ts +189 -0
  153. package/packages/trajectory/dist/src/curator.d.ts.map +1 -0
  154. package/packages/trajectory/dist/src/curator.js +340 -0
  155. package/packages/trajectory/dist/src/curator.js.map +1 -0
  156. package/packages/trajectory/dist/src/journey-renderer.d.ts +53 -0
  157. package/packages/trajectory/dist/src/journey-renderer.d.ts.map +1 -0
  158. package/packages/trajectory/dist/src/journey-renderer.js +324 -0
  159. package/packages/trajectory/dist/src/journey-renderer.js.map +1 -0
  160. package/packages/trajectory/dist/src/learning-graph.d.ts +159 -0
  161. package/packages/trajectory/dist/src/learning-graph.d.ts.map +1 -0
  162. package/packages/trajectory/dist/src/learning-graph.js +258 -0
  163. package/packages/trajectory/dist/src/learning-graph.js.map +1 -0
  164. package/packages/trajectory/dist/src/memory-store.d.ts +146 -0
  165. package/packages/trajectory/dist/src/memory-store.d.ts.map +1 -0
  166. package/packages/trajectory/dist/src/memory-store.js +472 -0
  167. package/packages/trajectory/dist/src/memory-store.js.map +1 -0
  168. package/packages/trajectory/dist/src/review-fork.d.ts +164 -0
  169. package/packages/trajectory/dist/src/review-fork.d.ts.map +1 -0
  170. package/packages/trajectory/dist/src/review-fork.js +332 -0
  171. package/packages/trajectory/dist/src/review-fork.js.map +1 -0
  172. package/packages/trajectory/dist/src/write-approval.d.ts +136 -0
  173. package/packages/trajectory/dist/src/write-approval.d.ts.map +1 -0
  174. package/packages/trajectory/dist/src/write-approval.js +281 -0
  175. package/packages/trajectory/dist/src/write-approval.js.map +1 -0
  176. package/packages/trajectory/package.json +1 -1
  177. package/packages/trajectory/src/curator.ts +485 -0
  178. package/packages/trajectory/src/journey-renderer.ts +399 -0
  179. package/packages/trajectory/src/learning-graph.ts +363 -0
  180. package/packages/trajectory/src/memory-store.ts +578 -0
  181. package/packages/trajectory/src/review-fork.ts +450 -0
  182. package/packages/trajectory/src/write-approval.ts +372 -0
  183. package/packages/tui/package.json +1 -1
  184. package/packages/types/package.json +1 -1
  185. package/packages/workflow-events/package.json +1 -1
  186. package/packages/workspace-scanner/package.json +1 -1
  187. package/packages/worktree/package.json +1 -1
  188. package/packages/write-review/dist/blackboard.d.ts +98 -0
  189. package/packages/write-review/dist/blackboard.d.ts.map +1 -0
  190. package/packages/write-review/dist/blackboard.js +289 -0
  191. package/packages/write-review/dist/blackboard.js.map +1 -0
  192. package/packages/write-review/dist/event-bus.d.ts +106 -0
  193. package/packages/write-review/dist/event-bus.d.ts.map +1 -0
  194. package/packages/write-review/dist/event-bus.js +294 -0
  195. package/packages/write-review/dist/event-bus.js.map +1 -0
  196. package/packages/write-review/dist/gate.d.ts +37 -0
  197. package/packages/write-review/dist/gate.d.ts.map +1 -0
  198. package/packages/write-review/dist/gate.js +138 -0
  199. package/packages/write-review/dist/gate.js.map +1 -0
  200. package/packages/write-review/dist/index.d.ts +22 -0
  201. package/packages/write-review/dist/index.d.ts.map +1 -0
  202. package/packages/write-review/dist/index.js +60 -0
  203. package/packages/write-review/dist/index.js.map +1 -0
  204. package/packages/write-review/dist/injection.d.ts +40 -0
  205. package/packages/write-review/dist/injection.d.ts.map +1 -0
  206. package/packages/write-review/dist/injection.js +179 -0
  207. package/packages/write-review/dist/injection.js.map +1 -0
  208. package/packages/write-review/dist/review.d.ts +54 -0
  209. package/packages/write-review/dist/review.d.ts.map +1 -0
  210. package/packages/write-review/dist/review.js +154 -0
  211. package/packages/write-review/dist/review.js.map +1 -0
  212. package/packages/write-review/dist/trigger.d.ts +39 -0
  213. package/packages/write-review/dist/trigger.d.ts.map +1 -0
  214. package/packages/write-review/dist/trigger.js +107 -0
  215. package/packages/write-review/dist/trigger.js.map +1 -0
  216. package/packages/write-review/dist/types.d.ts +78 -0
  217. package/packages/write-review/dist/types.d.ts.map +1 -0
  218. package/packages/write-review/dist/types.js +26 -0
  219. package/packages/write-review/dist/types.js.map +1 -0
  220. package/packages/write-review/package.json +1 -1
  221. package/packages/write-review/src/event-bus.ts +351 -0
  222. package/packages/write-review/src/index.ts +1 -0
  223. package/packages/write-review/src/injection.ts +25 -3
  224. package/proactive-compact.ts +51 -7
  225. package/scripts/sync-skills.ts +275 -0
  226. package/scripts/sync-to-qdrant.ts +398 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAwEH;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IAC1B,MAAM,EAAE,QAAQ,EAAE,oCAAoC;IACtD,QAAQ,EAAE,UAAU,EAAE,gCAAgC;IACtD,MAAM,EAAE,QAAQ,EAAE,+BAA+B;CACxC,CAAC;AAIX;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,KAAkB;IACjD,OAAO,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,SAAS,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAkB;IAC7C,OAAO,KAAK,KAAK,gBAAgB,IAAI,KAAK,KAAK,WAAW,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAkB;IAC1C,OAAO,CACN,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,mBAAmB,CACxE,CAAC;AACH,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/write-review",
3
- "version": "1.1.95",
3
+ "version": "1.1.97",
4
4
  "description": "Two-agent write-review loop with blackboard coordination",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Ping-Pong Event Bus
3
+ *
4
+ * Lightweight event bus for two-agent coordination.
5
+ * Watches blackboard status.json for state changes.
6
+ * Emits events and notifies agents via ExtensionAPI.
7
+ *
8
+ * NO AI NEEDED - deterministic file watching + state machine.
9
+ */
10
+
11
+ import { existsSync, readFileSync, watch } from "node:fs";
12
+ import { join } from "node:path";
13
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
+
15
+ /**
16
+ * Ping-Pong Events
17
+ */
18
+ export enum PingPongEvent {
19
+ SESSION_STARTED = "session_started",
20
+ WRITING_STARTED = "writing_started",
21
+ WRITER_DONE = "writer_done",
22
+ REVIEW_NEEDED = "review_needed",
23
+ REVIEW_STARTED = "review_started",
24
+ REVIEW_COMPLETE = "review_complete",
25
+ APPROVED = "approved",
26
+ CHANGES_REQUESTED = "changes_requested",
27
+ BLOCKED = "blocked",
28
+ SESSION_ENDED = "session_ended",
29
+ }
30
+
31
+ /**
32
+ * Event payload
33
+ */
34
+ export interface PingPongEventPayload {
35
+ event: PingPongEvent;
36
+ phase: string;
37
+ iteration: number;
38
+ timestamp: string;
39
+ data?: Record<string, unknown>;
40
+ }
41
+
42
+ /**
43
+ * Event listener callback
44
+ */
45
+ export type PingPongListener = (payload: PingPongEventPayload) => void | Promise<void>;
46
+
47
+ /**
48
+ * Phase to event mapping
49
+ */
50
+ const PHASE_TO_EVENT: Record<string, PingPongEvent> = {
51
+ idle: PingPongEvent.SESSION_STARTED,
52
+ writing: PingPongEvent.WRITING_STARTED,
53
+ pending_review: PingPongEvent.REVIEW_NEEDED,
54
+ reviewing: PingPongEvent.REVIEW_STARTED,
55
+ approved: PingPongEvent.APPROVED,
56
+ blocked: PingPongEvent.BLOCKED,
57
+ changes_requested: PingPongEvent.CHANGES_REQUESTED,
58
+ };
59
+
60
+ /**
61
+ * Simple Event Bus for ping-pong coordination
62
+ */
63
+ export class PingPongEventBus {
64
+ private listeners: Map<PingPongEvent, Set<PingPongListener>> = new Map();
65
+ private watchers: Map<string, ReturnType<typeof watch>> = new Map();
66
+ private lastStatusHash: string | null = null;
67
+ private pi: ExtensionAPI | null = null;
68
+ private projectPath: string | null = null;
69
+
70
+ /**
71
+ * Register an event listener
72
+ */
73
+ on(event: PingPongEvent, listener: PingPongListener): () => void {
74
+ if (!this.listeners.has(event)) {
75
+ this.listeners.set(event, new Set());
76
+ }
77
+ this.listeners.get(event)!.add(listener);
78
+
79
+ // Return unsubscribe function
80
+ return () => {
81
+ this.listeners.get(event)?.delete(listener);
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Register a listener for all events
87
+ */
88
+ onAny(listener: PingPongListener): () => void {
89
+ const wrappedListener = (payload: PingPongEventPayload) => {
90
+ return listener(payload);
91
+ };
92
+
93
+ // Listen to all events
94
+ for (const event of Object.values(PingPongEvent)) {
95
+ this.on(event, wrappedListener);
96
+ }
97
+
98
+ // Return unsubscribe for all
99
+ return () => {
100
+ for (const event of Object.values(PingPongEvent)) {
101
+ this.listeners.get(event)?.delete(wrappedListener);
102
+ }
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Emit an event
108
+ */
109
+ private emit(event: PingPongEvent, payload: Omit<PingPongEventPayload, "event">): void {
110
+ const fullPayload: PingPongEventPayload = {
111
+ event,
112
+ ...payload,
113
+ };
114
+
115
+ // Call specific listeners
116
+ const listeners = this.listeners.get(event);
117
+ if (listeners) {
118
+ for (const listener of listeners) {
119
+ try {
120
+ const result = listener(fullPayload);
121
+ if (result instanceof Promise) {
122
+ result.catch((err) => {
123
+ console.error(`[PingPong] Listener error:`, err);
124
+ });
125
+ }
126
+ } catch (err) {
127
+ console.error(`[PingPong] Listener error:`, err);
128
+ }
129
+ }
130
+ }
131
+
132
+ // Notify via ExtensionAPI if available
133
+ if (this.pi) {
134
+ this.notifyAgents(fullPayload);
135
+ }
136
+
137
+ // Log for debugging
138
+ console.error(
139
+ `[PingPong] Event: ${event}, phase=${payload.phase}, iteration=${payload.iteration}`
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Notify agents via ExtensionAPI
145
+ */
146
+ private notifyAgents(payload: PingPongEventPayload): void {
147
+ if (!this.pi) return;
148
+
149
+ const { event, phase, iteration } = payload;
150
+
151
+ // Map events to user-facing notifications
152
+ const messages: Record<PingPongEvent, string> = {
153
+ [PingPongEvent.SESSION_STARTED]: `Write-review session started`,
154
+ [PingPongEvent.WRITING_STARTED]: `Writer agent started (iteration ${iteration})`,
155
+ [PingPongEvent.WRITER_DONE]: `Writer done! Code ready for review`,
156
+ [PingPongEvent.REVIEW_NEEDED]: `Review needed - iteration ${iteration}`,
157
+ [PingPongEvent.REVIEW_STARTED]: `Reviewer agent started`,
158
+ [PingPongEvent.REVIEW_COMPLETE]: `Review complete - verdict: ${phase}`,
159
+ [PingPongEvent.APPROVED]: `APPROVED! Code ready to build/commit`,
160
+ [PingPongEvent.CHANGES_REQUESTED]: `Changes requested - iteration ${iteration}`,
161
+ [PingPongEvent.BLOCKED]: `BLOCKED - human intervention needed`,
162
+ [PingPongEvent.SESSION_ENDED]: `Write-review session ended`,
163
+ };
164
+
165
+ const message = messages[event];
166
+ if (message) {
167
+ const level = event === PingPongEvent.APPROVED
168
+ ? "success"
169
+ : event === PingPongEvent.BLOCKED
170
+ ? "error"
171
+ : "info";
172
+
173
+ // Use ui.notify if available on ExtensionContext
174
+ try {
175
+ // SAFETY: ExtensionAPI doesn't export ui type, we access it via dynamic lookup
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ const ui = (this.pi as any)?.ui;
178
+ if (ui?.notify) {
179
+ ui.notify(message, level);
180
+ }
181
+ } catch {
182
+ // ExtensionAPI ui.notify not available
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Start watching a blackboard status.json
189
+ */
190
+ startWatching(blackboardPath: string): void {
191
+ // Stop any existing watcher
192
+ this.stopWatching();
193
+
194
+ const statusPath = join(blackboardPath, "status.json");
195
+ this.projectPath = blackboardPath.replace("/.write-review", "");
196
+
197
+ if (!existsSync(statusPath)) {
198
+ console.error(`[PingPong] status.json not found at ${statusPath}`);
199
+ return;
200
+ }
201
+
202
+ // Read initial status
203
+ this.readAndEmit(statusPath);
204
+
205
+ // Watch for changes
206
+ try {
207
+ const watcher = watch(blackboardPath, { persistent: true }, (_eventType, filename) => {
208
+ if (filename === "status.json") {
209
+ this.readAndEmit(statusPath);
210
+ }
211
+ });
212
+
213
+ this.watchers.set(statusPath, watcher);
214
+ console.error(`[PingPong] Started watching ${blackboardPath}`);
215
+ } catch (err) {
216
+ console.error(`[PingPong] Failed to watch ${statusPath}:`, err);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Read status.json and emit events if changed
222
+ */
223
+ private readAndEmit(statusPath: string): void {
224
+ try {
225
+ const content = readFileSync(statusPath, "utf-8");
226
+ const status = JSON.parse(content);
227
+
228
+ // Check if status changed
229
+ const statusHash = JSON.stringify({
230
+ phase: status.phase,
231
+ writerDone: status.writerDone,
232
+ iteration: status.iteration,
233
+ verdict: status.verdict,
234
+ });
235
+
236
+ if (statusHash === this.lastStatusHash) {
237
+ return; // No change
238
+ }
239
+
240
+ const previousHash = this.lastStatusHash;
241
+ this.lastStatusHash = statusHash;
242
+
243
+ // Emit events based on phase transition
244
+ const phase = status.phase as string;
245
+ const iteration = status.iteration || 1;
246
+
247
+ // Detect writer done (was false, now true)
248
+ const wasWriterDone = previousHash ? previousHash.includes('"writerDone":true') : false;
249
+ if (status.writerDone && !wasWriterDone) {
250
+ this.emit(PingPongEvent.WRITER_DONE, {
251
+ phase,
252
+ iteration,
253
+ timestamp: new Date().toISOString(),
254
+ data: { writerMessage: status.writerMessage },
255
+ });
256
+ }
257
+
258
+ // Emit phase event
259
+ const event = PHASE_TO_EVENT[phase];
260
+ if (event) {
261
+ this.emit(event, {
262
+ phase,
263
+ iteration,
264
+ timestamp: new Date().toISOString(),
265
+ data: { verdict: status.verdict, verdictMessage: status.verdictMessage },
266
+ });
267
+ }
268
+
269
+ } catch (err) {
270
+ console.error(`[PingPong] Failed to read status.json:`, err);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Stop watching
276
+ */
277
+ stopWatching(): void {
278
+ for (const [path, watcher] of this.watchers) {
279
+ watcher.close();
280
+ this.watchers.delete(path);
281
+ }
282
+ console.error(`[PingPong] Stopped watching`);
283
+ }
284
+
285
+ /**
286
+ * Set ExtensionAPI for notifications
287
+ */
288
+ setExtensionAPI(pi: ExtensionAPI): void {
289
+ this.pi = pi;
290
+ }
291
+
292
+ /**
293
+ * Get current status hash
294
+ */
295
+ getLastStatusHash(): string | null {
296
+ return this.lastStatusHash;
297
+ }
298
+
299
+ /**
300
+ * Clean up
301
+ */
302
+ destroy(): void {
303
+ this.stopWatching();
304
+ this.listeners.clear();
305
+ this.pi = null;
306
+ }
307
+ }
308
+
309
+ // Singleton instance
310
+ let globalEventBus: PingPongEventBus | null = null;
311
+
312
+ /**
313
+ * Get global event bus instance
314
+ */
315
+ export function getPingPongEventBus(): PingPongEventBus {
316
+ if (!globalEventBus) {
317
+ globalEventBus = new PingPongEventBus();
318
+ }
319
+ return globalEventBus;
320
+ }
321
+
322
+ /**
323
+ * Create a new event bus instance
324
+ */
325
+ export function createPingPongEventBus(): PingPongEventBus {
326
+ return new PingPongEventBus();
327
+ }
328
+
329
+ /**
330
+ * Helper: Check if status changed from previous
331
+ */
332
+ export function hasStatusChanged(
333
+ current: Record<string, unknown>,
334
+ previous: string | null
335
+ ): boolean {
336
+ const currentHash = JSON.stringify({
337
+ phase: current.phase,
338
+ writerDone: current.writerDone,
339
+ iteration: current.iteration,
340
+ verdict: current.verdict,
341
+ });
342
+
343
+ return currentHash !== previous;
344
+ }
345
+
346
+ /**
347
+ * Helper: Format event for logging
348
+ */
349
+ export function formatPingPongEvent(event: PingPongEventPayload): string {
350
+ return `[PingPong:${event.event}] phase=${event.phase}, iteration=${event.iteration}, time=${event.timestamp}`;
351
+ }
@@ -63,3 +63,4 @@ export * from "./trigger.js";
63
63
  export * from "./injection.js";
64
64
  export * from "./gate.js";
65
65
  export * from "./review.js";
66
+ export * from "./event-bus.js";
@@ -113,7 +113,9 @@ export function getWriteReviewHints(projectPath: string): {
113
113
  * Register write-review extension with pi
114
114
  */
115
115
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
116
+ import type { BeforeAgentStartEventResult } from "@earendil-works/pi-coding-agent";
116
117
  import { createBlackboard } from "./blackboard.js";
118
+ import { getPingPongEventBus, PingPongEvent } from "./event-bus.js";
117
119
 
118
120
  export function injectWriterInstructions(
119
121
  pi: ExtensionAPI,
@@ -123,8 +125,25 @@ export function injectWriterInstructions(
123
125
  let blackboard: ReturnType<typeof createBlackboard> | null = null;
124
126
  let hintsInjected = false;
125
127
 
128
+ // Initialize event bus with ExtensionAPI
129
+ const eventBus = getPingPongEventBus();
130
+ eventBus.setExtensionAPI(pi);
131
+
132
+ // Register event listeners for debugging
133
+ eventBus.on(PingPongEvent.WRITER_DONE, (payload) => {
134
+ if (config?.debug) {
135
+ console.log(`[write-review] Writer done: ${JSON.stringify(payload)}`);
136
+ }
137
+ });
138
+
139
+ eventBus.on(PingPongEvent.REVIEW_NEEDED, (payload) => {
140
+ if (config?.debug) {
141
+ console.log(`[write-review] Review needed: ${JSON.stringify(payload)}`);
142
+ }
143
+ });
144
+
126
145
  // System prompt injection
127
- pi.on("before_agent_start", async (event) => {
146
+ pi.on("before_agent_start", async (event): Promise<BeforeAgentStartEventResult | undefined> => {
128
147
  const cwd = findProjectRoot(process.cwd?.() ?? "/");
129
148
  if (cwd !== currentProject) {
130
149
  currentProject = cwd;
@@ -135,9 +154,10 @@ export function injectWriterInstructions(
135
154
  if (!hintsInjected && blackboard) {
136
155
  const status = blackboard.load();
137
156
  if (status && status.phase !== "idle") {
138
- event.systemPrompt += formatWriteReviewHint(cwd);
157
+ let systemPrompt = event.systemPrompt;
158
+ systemPrompt += formatWriteReviewHint(cwd);
139
159
  if (status.phase !== "approved") {
140
- event.systemPrompt += formatReviewReminder(cwd);
160
+ systemPrompt += formatReviewReminder(cwd);
141
161
  }
142
162
  hintsInjected = true;
143
163
  if (config?.debug) {
@@ -145,8 +165,10 @@ export function injectWriterInstructions(
145
165
  `[write-review] Hints injected for ${cwd}, phase: ${status.phase}`,
146
166
  );
147
167
  }
168
+ return { systemPrompt };
148
169
  }
149
170
  }
171
+ return undefined;
150
172
  });
151
173
 
152
174
  // Build gate
@@ -7,7 +7,7 @@ export const MAX_PROACTIVE_COMPACT_FAILURES = 3;
7
7
  export const OUTPUT_LIMIT_AUTO_RESUME_LIMIT = 3;
8
8
  export const OUTPUT_LIMIT_RESUME_PROMPT =
9
9
  "Output token limit hit. Resume directly — no apology, no recap. Pick up mid-thought if the cut happened there. Break remaining work into smaller pieces.";
10
- export const PROVIDER_OVERLOAD_AUTO_RESUME_LIMIT = 3;
10
+ export const PROVIDER_OVERLOAD_AUTO_RESUME_LIMIT = 100;
11
11
  export const PROVIDER_OVERLOAD_AUTO_RESUME_MIN_MS = 60_000;
12
12
  export const PROVIDER_OVERLOAD_AUTO_RESUME_MAX_MS = 5 * 60_000;
13
13
  export const PROVIDER_OVERLOAD_RESUME_PROMPT = "resume";
@@ -104,6 +104,43 @@ export function shouldQueueOutputLimitResume(
104
104
  );
105
105
  }
106
106
 
107
+ /**
108
+ * Check if this is a FRESH 529 overload error (not a "Retry failed" report)
109
+ * Fresh errors should trigger retries; "Retry failed" messages should NOT.
110
+ */
111
+ export function isFreshProviderOverloadError(
112
+ message: AssistantStopLike,
113
+ ): boolean {
114
+ if (message.role !== "assistant") {
115
+ return false;
116
+ }
117
+ const text = readAssistantStopText(message);
118
+ // Fresh overload: contains overload indicator + 529 code
119
+ const isFreshOverload =
120
+ text.includes("overloaded_error") ||
121
+ text.includes("peak-hour surge") ||
122
+ text.includes("temporarily busy") ||
123
+ text.includes("server is temporarily busy") ||
124
+ text.includes("try again shortly");
125
+ // But NOT if it's a "Retry failed" report (that means our retry already failed)
126
+ const isRetryFailedReport = /Retry failed after \d+ attempts:/i.test(text);
127
+ // Extract request_id for logging
128
+ const requestIdMatch = text.match(/request_id[^"]*"([^"]+)"/i) || text.match(/request_id[^"]*([a-f0-9]+)/i);
129
+ const requestId = requestIdMatch ? requestIdMatch[1] : "unknown";
130
+ if (isFreshOverload) {
131
+ console.error(
132
+ `[pi-harness] Fresh 529 detected (request=${requestId}), retry_failed_report=${isRetryFailedReport}`,
133
+ );
134
+ } else if (text.includes("529") || text.includes("overload")) {
135
+ console.error(`[pi-harness] 529-related but NOT fresh (stopReason or other): ${text.slice(0, 100)}`);
136
+ }
137
+ return isFreshOverload && !isRetryFailedReport;
138
+ }
139
+
140
+ /**
141
+ * Legacy check - returns true for BOTH fresh overloads AND "Retry failed" reports
142
+ * @deprecated Use isFreshProviderOverloadError for retry logic
143
+ */
107
144
  export function isProviderOverloadAssistantMessage(
108
145
  message: AssistantStopLike,
109
146
  ): boolean {
@@ -111,7 +148,7 @@ export function isProviderOverloadAssistantMessage(
111
148
  return false;
112
149
  }
113
150
 
114
- return /overloaded_error|peak-hour surge|temporarily busy|server is temporarily busy|try again shortly|Retry failed after \d+ attempts:.*529|\b529\b|\b2064\b/i.test(
151
+ return /overloaded_error|peak-hour surge|temporarily busy|server is temporarily busy|try again shortly|\b529\b|\b2064\b/i.test(
115
152
  readAssistantStopText(message),
116
153
  );
117
154
  }
@@ -124,11 +161,18 @@ export function shouldQueueProviderOverloadResume(
124
161
  ): boolean {
125
162
  const maxAttempts =
126
163
  options?.maxAttempts ?? PROVIDER_OVERLOAD_AUTO_RESUME_LIMIT;
127
- return (
128
- isProviderOverloadAssistantMessage(message) &&
129
- resumeAttempts < maxAttempts &&
130
- !hasPendingMessages
131
- );
164
+ const isFresh = isFreshProviderOverloadError(message);
165
+ const underLimit = resumeAttempts < maxAttempts;
166
+ const noPending = !hasPendingMessages;
167
+ const shouldQueue = isFresh && underLimit && noPending;
168
+
169
+ if (isFresh) {
170
+ console.error(
171
+ `[pi-harness] shouldQueue=${shouldQueue} attempts=${resumeAttempts}/${maxAttempts} hasPending=${hasPendingMessages}`,
172
+ );
173
+ }
174
+
175
+ return shouldQueue;
132
176
  }
133
177
 
134
178
  export function getProviderOverloadResumeDelayMs(options?: {