pi-harness-runtime 0.9.25 → 1.1.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 (350) hide show
  1. package/CHANGELOG.md +49 -7
  2. package/README.md +173 -51
  3. package/harness/auto-compact.ts +1 -1
  4. package/harness/autonomous-review.ts +88 -0
  5. package/harness/context-compact-orchestrator.ts +23 -23
  6. package/harness/context-window-manager.ts +5 -5
  7. package/harness/continue-prompt.ts +3 -3
  8. package/harness/e2e/minimax-quota-scraper.ts +5 -5
  9. package/harness/e2e/openai-quota-scraper.ts +13 -8
  10. package/harness/forked-summarizer.ts +4 -4
  11. package/harness/herdr-agents.ts +91 -0
  12. package/harness/index.ts +5 -5
  13. package/harness/loop-code-agent.ts +284 -0
  14. package/harness/loop-controller.ts +201 -0
  15. package/harness/loop-review-agent.ts +295 -0
  16. package/harness/loop-runtime.ts +10 -10
  17. package/harness/loop-types.ts +118 -0
  18. package/harness/notification-events.ts +3 -3
  19. package/harness/output-limit-handler.ts +1 -1
  20. package/harness/partial-recovery.ts +3 -3
  21. package/harness/session-memory.ts +5 -5
  22. package/index.ts +139 -34
  23. package/package.json +1 -1
  24. package/packages/a2a-adapter/package.json +1 -1
  25. package/packages/a2a-adapter/src/types.ts +5 -5
  26. package/packages/auth/package.json +1 -1
  27. package/packages/autonomous-refactor/package.json +1 -1
  28. package/packages/autonomous-runtime/dist/inbox.d.ts +3 -3
  29. package/packages/autonomous-runtime/dist/inbox.js +12 -12
  30. package/packages/autonomous-runtime/dist/lease.js +14 -14
  31. package/packages/autonomous-runtime/dist/types.js +1 -1
  32. package/packages/autonomous-runtime/package.json +1 -1
  33. package/packages/autonomous-runtime/src/inbox.ts +12 -12
  34. package/packages/autonomous-runtime/src/lease.ts +15 -15
  35. package/packages/autonomous-runtime/src/types.ts +13 -13
  36. package/packages/autonomous-runtime/tsconfig.tsbuildinfo +1 -1
  37. package/packages/cache-strategy/package.json +1 -1
  38. package/packages/capability-registry/dist/types.js +1 -1
  39. package/packages/capability-registry/package.json +1 -1
  40. package/packages/capability-registry/src/types.ts +5 -5
  41. package/packages/checkpoint/README.md +6 -6
  42. package/packages/checkpoint/dist/differ.js +2 -2
  43. package/packages/checkpoint/dist/engine.js +3 -3
  44. package/packages/checkpoint/dist/index.js +5 -5
  45. package/packages/checkpoint/dist/storage.js +3 -3
  46. package/packages/checkpoint/dist/types.js +1 -1
  47. package/packages/checkpoint/package.json +1 -1
  48. package/packages/checkpoint/src/differ.ts +2 -2
  49. package/packages/checkpoint/src/engine.ts +3 -3
  50. package/packages/checkpoint/src/index.ts +5 -5
  51. package/packages/checkpoint/src/storage.ts +3 -3
  52. package/packages/checkpoint/src/types.ts +10 -10
  53. package/packages/checkpoint/tsconfig.tsbuildinfo +1 -1
  54. package/packages/cli-plugin-sdk/package.json +1 -1
  55. package/packages/clipboard/dist/src/clipboard.d.ts.map +1 -1
  56. package/packages/clipboard/dist/src/clipboard.js +15 -7
  57. package/packages/clipboard/dist/src/clipboard.js.map +1 -1
  58. package/packages/clipboard/dist/src/copy-sync.d.ts +27 -0
  59. package/packages/clipboard/dist/src/copy-sync.d.ts.map +1 -0
  60. package/packages/clipboard/dist/src/copy-sync.js +170 -0
  61. package/packages/clipboard/dist/src/copy-sync.js.map +1 -0
  62. package/packages/clipboard/dist/src/gist-relay.d.ts +48 -0
  63. package/packages/clipboard/dist/src/gist-relay.d.ts.map +1 -0
  64. package/packages/clipboard/dist/src/gist-relay.js +186 -0
  65. package/packages/clipboard/dist/src/gist-relay.js.map +1 -0
  66. package/packages/clipboard/dist/src/github-login.d.ts +16 -0
  67. package/packages/clipboard/dist/src/github-login.d.ts.map +1 -0
  68. package/packages/clipboard/dist/src/github-login.js +69 -0
  69. package/packages/clipboard/dist/src/github-login.js.map +1 -0
  70. package/packages/clipboard/dist/src/index.d.ts +2 -0
  71. package/packages/clipboard/dist/src/index.d.ts.map +1 -1
  72. package/packages/clipboard/dist/src/index.js +2 -1
  73. package/packages/clipboard/dist/src/index.js.map +1 -1
  74. package/packages/clipboard/package.json +1 -1
  75. package/packages/clipboard/src/clipboard.ts +16 -9
  76. package/packages/clipboard/src/copy-sync.ts +201 -0
  77. package/packages/clipboard/src/gist-relay.ts +240 -0
  78. package/packages/clipboard/src/github-login.ts +107 -0
  79. package/packages/clipboard/src/index.ts +2 -0
  80. package/packages/clipboard/test/mojibake.test.mjs +143 -0
  81. package/packages/code-generation/README.md +13 -13
  82. package/packages/code-generation/dist/faker.js +4 -4
  83. package/packages/code-generation/dist/generator.js +3 -3
  84. package/packages/code-generation/dist/index.js +3 -3
  85. package/packages/code-generation/dist/templates/registry.js +1 -1
  86. package/packages/code-generation/dist/types.js +1 -1
  87. package/packages/code-generation/package.json +1 -1
  88. package/packages/code-generation/src/faker.ts +5 -5
  89. package/packages/code-generation/src/generator.ts +4 -4
  90. package/packages/code-generation/src/index.ts +3 -3
  91. package/packages/code-generation/src/templates/registry.ts +1 -1
  92. package/packages/code-generation/src/types.ts +10 -10
  93. package/packages/code-generation/tsconfig.tsbuildinfo +1 -1
  94. package/packages/code-review/README.md +13 -13
  95. package/packages/code-review/dist/engine.js +3 -3
  96. package/packages/code-review/dist/index.js +3 -3
  97. package/packages/code-review/dist/types.js +1 -1
  98. package/packages/code-review/package.json +1 -1
  99. package/packages/code-review/src/engine.ts +3 -3
  100. package/packages/code-review/src/index.ts +3 -3
  101. package/packages/code-review/src/types.ts +10 -10
  102. package/packages/code-review/tsconfig.tsbuildinfo +1 -1
  103. package/packages/codex-adapter/package.json +1 -1
  104. package/packages/context-compiler/dist/compiler.js +1 -1
  105. package/packages/context-compiler/package.json +1 -1
  106. package/packages/context-compiler/src/compiler.ts +1 -1
  107. package/packages/context-compiler/src/types.ts +11 -11
  108. package/packages/context-compiler/test/context-compiler.test.ts +11 -11
  109. package/packages/context-discovery/package.json +1 -1
  110. package/packages/context-manager/package.json +1 -1
  111. package/packages/cookie-sanitizer/README.md +27 -27
  112. package/packages/cookie-sanitizer/package.json +1 -1
  113. package/packages/cost-optimizer/package.json +1 -1
  114. package/packages/cost-optimizer/src/types.ts +5 -5
  115. package/packages/dependency-analyzer/package.json +1 -1
  116. package/packages/django-plugin/dist/analyzer.js +7 -7
  117. package/packages/django-plugin/package.json +1 -1
  118. package/packages/django-plugin/src/analyzer.ts +7 -7
  119. package/packages/doc-generator/package.json +1 -1
  120. package/packages/evaluation-engine/dist/engine.js +9 -9
  121. package/packages/evaluation-engine/package.json +1 -1
  122. package/packages/evaluation-engine/src/engine.ts +9 -9
  123. package/packages/evaluation-engine/src/types.ts +3 -3
  124. package/packages/evaluation-runner/package.json +1 -1
  125. package/packages/event-bus/dist/src/herdr-bus.d.ts +121 -0
  126. package/packages/event-bus/dist/src/herdr-bus.d.ts.map +1 -0
  127. package/packages/event-bus/dist/src/herdr-bus.js +322 -0
  128. package/packages/event-bus/dist/src/herdr-bus.js.map +1 -0
  129. package/packages/event-bus/dist/src/index.d.ts +2 -0
  130. package/packages/event-bus/dist/src/index.d.ts.map +1 -1
  131. package/packages/event-bus/dist/src/index.js +1 -0
  132. package/packages/event-bus/dist/src/index.js.map +1 -1
  133. package/packages/event-bus/package.json +1 -1
  134. package/packages/event-bus/src/herdr-bus.ts +525 -0
  135. package/packages/event-bus/src/index.ts +29 -0
  136. package/packages/event-store/package.json +1 -1
  137. package/packages/experience-replay/dist/replay.js +7 -7
  138. package/packages/experience-replay/package.json +1 -1
  139. package/packages/experience-replay/src/replay.ts +7 -7
  140. package/packages/experience-replay/src/types.ts +5 -5
  141. package/packages/feedback-collector/package.json +1 -1
  142. package/packages/framework-detector/README.md +18 -18
  143. package/packages/framework-detector/dist/detector.js +3 -3
  144. package/packages/framework-detector/dist/index.js +3 -3
  145. package/packages/framework-detector/dist/signatures/registry.js +24 -24
  146. package/packages/framework-detector/dist/types.js +1 -1
  147. package/packages/framework-detector/package.json +1 -1
  148. package/packages/framework-detector/src/detector.ts +3 -3
  149. package/packages/framework-detector/src/index.ts +3 -3
  150. package/packages/framework-detector/src/signatures/registry.ts +24 -24
  151. package/packages/framework-detector/src/types.ts +7 -7
  152. package/packages/framework-detector/tsconfig.tsbuildinfo +1 -1
  153. package/packages/framework-plugin-sdk/README.md +23 -23
  154. package/packages/framework-plugin-sdk/dist/index.js +2 -2
  155. package/packages/framework-plugin-sdk/dist/manager.js +3 -3
  156. package/packages/framework-plugin-sdk/dist/types.js +2 -2
  157. package/packages/framework-plugin-sdk/package.json +1 -1
  158. package/packages/framework-plugin-sdk/src/index.ts +2 -2
  159. package/packages/framework-plugin-sdk/src/manager.ts +3 -3
  160. package/packages/framework-plugin-sdk/src/types.ts +10 -10
  161. package/packages/framework-plugin-sdk/tsconfig.tsbuildinfo +1 -1
  162. package/packages/frappe-plugin/dist/analyzer.js +8 -8
  163. package/packages/frappe-plugin/dist/types.js +1 -1
  164. package/packages/frappe-plugin/package.json +1 -1
  165. package/packages/frappe-plugin/src/analyzer.ts +8 -8
  166. package/packages/frappe-plugin/src/types.ts +10 -10
  167. package/packages/generic-web-plugin/dist/analyzer.js +5 -5
  168. package/packages/generic-web-plugin/package.json +1 -1
  169. package/packages/generic-web-plugin/src/analyzer.ts +5 -5
  170. package/packages/health-monitor/package.json +1 -1
  171. package/packages/health-monitor/src/monitor.ts +1 -1
  172. package/packages/intent-analyzer/package.json +1 -1
  173. package/packages/laravel-plugin/dist/analyzer.js +11 -11
  174. package/packages/laravel-plugin/package.json +1 -1
  175. package/packages/laravel-plugin/src/analyzer.ts +11 -11
  176. package/packages/learning-engine/dist/engine.js +5 -5
  177. package/packages/learning-engine/dist/types.js +1 -1
  178. package/packages/learning-engine/package.json +1 -1
  179. package/packages/learning-engine/src/engine.ts +5 -5
  180. package/packages/learning-engine/src/types.ts +6 -6
  181. package/packages/mcp-adapter/package.json +1 -1
  182. package/packages/memory-engine/dist/engine.js +8 -8
  183. package/packages/memory-engine/dist/types.js +2 -2
  184. package/packages/memory-engine/package.json +1 -1
  185. package/packages/memory-engine/src/engine.ts +8 -8
  186. package/packages/memory-engine/src/types.ts +9 -9
  187. package/packages/model-registry/package.json +1 -1
  188. package/packages/model-registry/src/types.ts +4 -4
  189. package/packages/nextjs-plugin/dist/analyzer.js +9 -9
  190. package/packages/nextjs-plugin/package.json +1 -1
  191. package/packages/nextjs-plugin/src/analyzer.ts +9 -9
  192. package/packages/notification/dist/notification-center.js +1 -1
  193. package/packages/notification/package.json +1 -1
  194. package/packages/notification/src/notification-center.ts +1 -1
  195. package/packages/observability/dist/alerts.js +2 -2
  196. package/packages/observability/dist/health.js +4 -4
  197. package/packages/observability/dist/index.js +6 -6
  198. package/packages/observability/dist/logger.js +7 -7
  199. package/packages/observability/dist/metrics.js +11 -11
  200. package/packages/observability/dist/tracer.js +3 -3
  201. package/packages/observability/dist/types.js +1 -1
  202. package/packages/observability/package.json +1 -1
  203. package/packages/observability/src/alerts.ts +2 -2
  204. package/packages/observability/src/health.ts +4 -4
  205. package/packages/observability/src/index.ts +6 -6
  206. package/packages/observability/src/logger.ts +7 -7
  207. package/packages/observability/src/metrics.ts +11 -11
  208. package/packages/observability/src/tracer.ts +3 -3
  209. package/packages/observability/src/types.ts +7 -7
  210. package/packages/observability/test/observability.test.ts +4 -4
  211. package/packages/observability/tsconfig.tsbuildinfo +1 -1
  212. package/packages/performance-optimizer/dist/optimizer.js +1 -1
  213. package/packages/performance-optimizer/package.json +1 -1
  214. package/packages/performance-optimizer/src/optimizer.ts +1 -1
  215. package/packages/performance-optimizer/src/types.ts +4 -4
  216. package/packages/privilege-broker/package.json +1 -1
  217. package/packages/project-analyzer/dist/analyzer.js +4 -4
  218. package/packages/project-analyzer/dist/index.js +8 -8
  219. package/packages/project-analyzer/dist/signals.js +3 -3
  220. package/packages/project-analyzer/dist/types.js +1 -1
  221. package/packages/project-analyzer/package.json +1 -1
  222. package/packages/project-analyzer/src/analyzer.ts +4 -4
  223. package/packages/project-analyzer/src/index.ts +9 -9
  224. package/packages/project-analyzer/src/signals.ts +4 -4
  225. package/packages/project-analyzer/src/types.ts +15 -15
  226. package/packages/project-analyzer/test/index.test.ts +17 -17
  227. package/packages/project-analyzer/tsconfig.tsbuildinfo +1 -1
  228. package/packages/projection-engine/package.json +1 -1
  229. package/packages/prompt-compiler/dist/section-builder.js +1 -1
  230. package/packages/prompt-compiler/dist/types.js +3 -3
  231. package/packages/prompt-compiler/package.json +1 -1
  232. package/packages/prompt-compiler/src/section-builder.ts +1 -1
  233. package/packages/prompt-compiler/src/types.ts +12 -12
  234. package/packages/prompt-compiler/test/prompt-compiler.test.ts +12 -12
  235. package/packages/prompt-versioning/package.json +1 -1
  236. package/packages/provider-adapter-sdk/dist/builder.js +1 -1
  237. package/packages/provider-adapter-sdk/dist/index.js +5 -5
  238. package/packages/provider-adapter-sdk/dist/registry.js +1 -1
  239. package/packages/provider-adapter-sdk/dist/tester.js +1 -1
  240. package/packages/provider-adapter-sdk/dist/types.js +1 -1
  241. package/packages/provider-adapter-sdk/package.json +1 -1
  242. package/packages/provider-adapter-sdk/src/builder.ts +1 -1
  243. package/packages/provider-adapter-sdk/src/index.ts +6 -6
  244. package/packages/provider-adapter-sdk/src/registry.ts +1 -1
  245. package/packages/provider-adapter-sdk/src/tester.ts +1 -1
  246. package/packages/provider-adapter-sdk/src/types.ts +5 -5
  247. package/packages/provider-adapter-sdk/tsconfig.tsbuildinfo +1 -1
  248. package/packages/provider-router/package.json +1 -1
  249. package/packages/provider-router/src/types.ts +4 -4
  250. package/packages/provider-selector/package.json +1 -1
  251. package/packages/providers/dist/deepagents-adapter.js +1 -1
  252. package/packages/providers/package.json +1 -1
  253. package/packages/providers/src/deepagents-adapter.ts +1 -1
  254. package/packages/quota-manager/package.json +1 -1
  255. package/packages/rate-limiter/package.json +1 -1
  256. package/packages/react-vite-plugin/dist/analyzer.js +7 -7
  257. package/packages/react-vite-plugin/package.json +1 -1
  258. package/packages/react-vite-plugin/src/analyzer.ts +7 -7
  259. package/packages/release-manager/package.json +1 -1
  260. package/packages/requirement-compiler/dist/compiler.js +17 -17
  261. package/packages/requirement-compiler/dist/types.js +2 -2
  262. package/packages/requirement-compiler/package.json +1 -1
  263. package/packages/requirement-compiler/src/compiler.ts +17 -17
  264. package/packages/requirement-compiler/src/types.ts +21 -21
  265. package/packages/requirement-compiler/test/requirement-compiler.test.ts +17 -17
  266. package/packages/runtime/dist/command-executor.js +1 -1
  267. package/packages/runtime/dist/local-runtime-agent.js +1 -1
  268. package/packages/runtime/dist/policy-engine.js +1 -1
  269. package/packages/runtime/dist/runtime-api.js +1 -1
  270. package/packages/runtime/dist/workspace-manager.js +1 -1
  271. package/packages/runtime/package.json +1 -1
  272. package/packages/runtime/src/command-executor.ts +1 -1
  273. package/packages/runtime/src/local-runtime-agent.ts +1 -1
  274. package/packages/runtime/src/policy-engine.ts +1 -1
  275. package/packages/runtime/src/runtime-api.ts +1 -1
  276. package/packages/runtime/src/workspace-manager.ts +1 -1
  277. package/packages/scheduler/package.json +1 -1
  278. package/packages/scheduler-adapter/package.json +1 -1
  279. package/packages/session/README.md +18 -18
  280. package/packages/session/dist/history.js +3 -3
  281. package/packages/session/dist/index.js +5 -5
  282. package/packages/session/dist/manager.js +3 -3
  283. package/packages/session/dist/policy.js +2 -2
  284. package/packages/session/dist/store.js +2 -2
  285. package/packages/session/dist/types.js +1 -1
  286. package/packages/session/package.json +1 -1
  287. package/packages/session/src/history.ts +3 -3
  288. package/packages/session/src/index.ts +5 -5
  289. package/packages/session/src/manager.ts +3 -3
  290. package/packages/session/src/policy.ts +4 -4
  291. package/packages/session/src/store.ts +3 -3
  292. package/packages/session/src/types.ts +10 -10
  293. package/packages/session/tsconfig.tsbuildinfo +1 -1
  294. package/packages/session-api/package.json +1 -1
  295. package/packages/session-export/package.json +1 -1
  296. package/packages/shared-context/package.json +1 -1
  297. package/packages/skill-registry/package.json +1 -1
  298. package/packages/skill-registry/src/types.ts +4 -4
  299. package/packages/sprint-planner/package.json +1 -1
  300. package/packages/subscription-engine/package.json +1 -1
  301. package/packages/task-compiler/dist/compiler.js +7 -7
  302. package/packages/task-compiler/dist/decompose.js +11 -11
  303. package/packages/task-compiler/dist/file-scope.js +6 -6
  304. package/packages/task-compiler/dist/graph.js +6 -6
  305. package/packages/task-compiler/dist/index.js +2 -2
  306. package/packages/task-compiler/dist/types.js +5 -5
  307. package/packages/task-compiler/dist/validate.js +2 -2
  308. package/packages/task-compiler/dist/verification.js +2 -2
  309. package/packages/task-compiler/package.json +1 -1
  310. package/packages/task-compiler/src/command-policy.ts +1 -1
  311. package/packages/task-compiler/src/compiler.ts +7 -7
  312. package/packages/task-compiler/src/decompose.ts +11 -11
  313. package/packages/task-compiler/src/file-scope.ts +6 -6
  314. package/packages/task-compiler/src/graph.ts +6 -6
  315. package/packages/task-compiler/src/index.ts +3 -3
  316. package/packages/task-compiler/src/types.ts +14 -14
  317. package/packages/task-compiler/src/validate.ts +2 -2
  318. package/packages/task-compiler/src/verification.ts +2 -2
  319. package/packages/task-compiler/test/task-compiler.test.ts +10 -10
  320. package/packages/test-data-generator/README.md +13 -13
  321. package/packages/test-data-generator/dist/faker-adapter.js +4 -4
  322. package/packages/test-data-generator/dist/generator.js +3 -3
  323. package/packages/test-data-generator/dist/index.js +3 -3
  324. package/packages/test-data-generator/dist/types.js +1 -1
  325. package/packages/test-data-generator/package.json +1 -1
  326. package/packages/test-data-generator/src/faker-adapter.ts +4 -4
  327. package/packages/test-data-generator/src/generator.ts +3 -3
  328. package/packages/test-data-generator/src/index.ts +3 -3
  329. package/packages/test-data-generator/src/types.ts +9 -9
  330. package/packages/test-data-generator/tsconfig.tsbuildinfo +1 -1
  331. package/packages/test-generator/package.json +1 -1
  332. package/packages/todo-bd-sync/package.json +22 -0
  333. package/packages/todo-bd-sync/src/detector.ts +150 -0
  334. package/packages/todo-bd-sync/src/extension.ts +101 -0
  335. package/packages/todo-bd-sync/src/index.ts +401 -0
  336. package/packages/todo-bd-sync/src/sync.ts +322 -0
  337. package/packages/todo-bd-sync/src/todo-reminder.ts +418 -0
  338. package/packages/todo-bd-sync/src/types.ts +137 -0
  339. package/packages/token-estimation/package.json +1 -1
  340. package/packages/token-optimizer/package.json +1 -1
  341. package/packages/tui/package.json +1 -1
  342. package/packages/types/package.json +1 -1
  343. package/packages/types/src/agent-worker-types.ts +5 -5
  344. package/packages/types/src/runtime-types.ts +12 -12
  345. package/packages/types/tsconfig.tsbuildinfo +1 -1
  346. package/packages/workflow-events/package.json +1 -1
  347. package/packages/workspace-scanner/package.json +1 -1
  348. package/packages/worktree/package.json +1 -1
  349. package/renderer.ts +7 -7
  350. package/skills/harness-runtime/SKILL.md +15 -15
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Loop Controller — blackboard-coordinated write-review loops.
3
+ *
4
+ * Human types /review-loop <write_count> <review_count> --prompt="..."
5
+ * Controller creates blackboard, sets first nextAction, monitors progress.
6
+ */
7
+
8
+ import { writeFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { randomUUID } from "crypto";
11
+ import {
12
+ createHerdrBus,
13
+ ensureHerdrWorkspace,
14
+ publishLoopStarted,
15
+ type LoopConfig,
16
+ } from "../packages/event-bus/src/index.js";
17
+ import { createBlackboard } from "./blackboard.js";
18
+ import { buildLoopTaskGraph } from "./loop-types.js";
19
+ import type { SharedBlackboard } from "./blackboard.js";
20
+
21
+ function encodeNextAction(la: {
22
+ taskId: string;
23
+ agentType: string;
24
+ iteration: number;
25
+ prompt?: string;
26
+ codeFiles?: string[];
27
+ }) {
28
+ return {
29
+ taskId: la.taskId,
30
+ instruction: "LOOP:" + JSON.stringify(la),
31
+ priority: "high" as const,
32
+ createdAt: new Date().toISOString(),
33
+ };
34
+ }
35
+
36
+ function sleep(ms: number): Promise<void> {
37
+ return new Promise((r) => setTimeout(r, ms));
38
+ }
39
+
40
+ // ─── Monitor ────────────────────────────────────────────────────────────────
41
+
42
+ async function monitorLoop(
43
+ blackboard: SharedBlackboard,
44
+ loopId: string,
45
+ ): Promise<void> {
46
+ const paths = ensureHerdrWorkspace();
47
+
48
+ while (true) {
49
+ blackboard.load();
50
+ const record = blackboard.getRecord();
51
+ if (!record) {
52
+ await sleep(500);
53
+ continue;
54
+ }
55
+
56
+ // Check if report is done
57
+ const reportNode = record.tasks.nodes["report"];
58
+ if (reportNode?.status === "done") {
59
+ const reviews = Object.values(record.tasks.nodes).filter(
60
+ (n) => n.id.startsWith("review-") && n.status === "done",
61
+ );
62
+ const lastReview = reviews.sort((a, b) =>
63
+ (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""),
64
+ )[0];
65
+ const verdict = (lastReview as { result?: string }).result ?? "unknown";
66
+ const summary =
67
+ verdict !== "unknown" ? `Verdict: ${verdict}` : "Loop completed";
68
+
69
+ const reportPath = join(
70
+ paths.reviews,
71
+ `loop-${loopId.slice(0, 8)}-final.md`,
72
+ );
73
+ writeFileSync(
74
+ reportPath,
75
+ `# Loop Result — ${loopId.slice(0, 8)}
76
+
77
+ ## Result
78
+ ${summary}
79
+
80
+ ## Verdict: **${verdict.toUpperCase()}**
81
+ `,
82
+ );
83
+ console.log(`[controller] VERDICT: ${verdict.toUpperCase()}`);
84
+ console.log(`[controller] Report: ${reportPath}`);
85
+ return;
86
+ }
87
+
88
+ // Check for blocked
89
+ const blocked = Object.values(record.tasks.nodes).find(
90
+ (n) => n.status === "blocked",
91
+ );
92
+ if (blocked) {
93
+ const reportPath = join(
94
+ paths.reviews,
95
+ `loop-${loopId.slice(0, 8)}-final.md`,
96
+ );
97
+ writeFileSync(
98
+ reportPath,
99
+ `# Loop Result — ${loopId.slice(0, 8)}
100
+
101
+ ## Result
102
+ Blocked at: ${blocked.id}
103
+
104
+ ## Verdict: **BLOCKED**
105
+ `,
106
+ );
107
+ console.log(`[controller] VERDICT: BLOCKED (${blocked.id})`);
108
+ return;
109
+ }
110
+
111
+ await sleep(500);
112
+ }
113
+ }
114
+
115
+ // ─── Run ──────────────────────────────────────────────────────────────────
116
+
117
+ async function runLoop(args: string[]): Promise<void> {
118
+ const writeCount = parseInt(args[0] ?? "3", 10);
119
+ const reviewCount = parseInt(args[1] ?? "2", 10);
120
+ const prompt =
121
+ (args.find((a) => a.startsWith("--prompt=")) ?? "")
122
+ .slice("--prompt=".length)
123
+ .replace(/^["']|["']$/g, "") || "No prompt";
124
+
125
+ if (isNaN(writeCount) || writeCount < 1 || writeCount > 20) {
126
+ console.error("write_count must be 1-20");
127
+ process.exit(1);
128
+ }
129
+ if (isNaN(reviewCount) || reviewCount < 0 || reviewCount > 20) {
130
+ console.error("review_count must be 0-20");
131
+ process.exit(1);
132
+ }
133
+
134
+ const loopId = randomUUID();
135
+ const paths = ensureHerdrWorkspace();
136
+ const taskGraph = buildLoopTaskGraph(writeCount, reviewCount, loopId);
137
+
138
+ console.log("=".repeat(60));
139
+ console.log(`LOOP STARTED — ${loopId.slice(0, 8)}`);
140
+ console.log(` Writes: ${writeCount} Reviews: ${reviewCount}`);
141
+ console.log(` Prompt: ${prompt}`);
142
+ console.log("=".repeat(60));
143
+
144
+ // Create blackboard
145
+ const blackboard = createBlackboard(loopId, paths.root, taskGraph);
146
+ console.log(`[controller] Blackboard: ${blackboard.getPath()}`);
147
+
148
+ // Event bus for cross-process signaling
149
+ const bus = createHerdrBus(`ctrl-${loopId.slice(0, 8)}`);
150
+
151
+ // Save config
152
+ // Clean old loop configs
153
+ for (const f of (await import("fs")).readdirSync(paths.root)) {
154
+ if (f.startsWith("loop-") && f.endsWith(".config.json")) {
155
+ (await import("fs")).unlinkSync(join(paths.root, f));
156
+ }
157
+ }
158
+
159
+ const config: LoopConfig = {
160
+ loopId,
161
+ writeCount,
162
+ reviewCount,
163
+ prompt,
164
+ nextReviewAfter: Math.max(1, Math.floor(writeCount / (reviewCount || 1))),
165
+ createdAt: new Date().toISOString(),
166
+ };
167
+ writeFileSync(
168
+ join(paths.root, `loop-${loopId}.config.json`),
169
+ JSON.stringify(config, null, 2),
170
+ );
171
+ publishLoopStarted(bus, config);
172
+
173
+ // Set first action: code agent writes iteration 1
174
+ blackboard.setNextAction(
175
+ encodeNextAction({
176
+ taskId: "write-1",
177
+ agentType: "code",
178
+ iteration: 1,
179
+ prompt,
180
+ }),
181
+ );
182
+ console.log("[controller] nextAction: code write-1");
183
+
184
+ // Monitor until done
185
+ await monitorLoop(blackboard, loopId);
186
+ }
187
+
188
+ // ─── CLI ──────────────────────────────────────────────────────────────────
189
+
190
+ const args = process.argv.slice(2);
191
+ if (!args.length || args[0] === "--help" || args[0] === "-h") {
192
+ console.log(
193
+ 'Usage: bun harness/loop-controller.ts <write_count> <review_count> --prompt="..."\nExample: bun harness/loop-controller.ts 3 2 --prompt="build REST API"',
194
+ );
195
+ process.exit(0);
196
+ }
197
+
198
+ runLoop(args).catch((err) => {
199
+ console.error("[controller] Fatal:", err);
200
+ process.exit(1);
201
+ });
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Loop Review Agent (GPT) — coordinates via blackboard.
3
+ *
4
+ * Polls blackboard for nextAction where agentType="review".
5
+ * Reviews code, writes verdict, updates node status.
6
+ *
7
+ * Usage: bun harness/loop-review-agent.ts
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
11
+ import { join } from "path";
12
+ import {
13
+ ensureHerdrWorkspace,
14
+ parseVerdict,
15
+ } from "../packages/event-bus/src/herdr-bus.js";
16
+ import { SharedBlackboard } from "./blackboard.js";
17
+ import type {
18
+ LoopNextAction,
19
+ LoopAgentReport,
20
+ LoopVerdict,
21
+ } from "./loop-types.js";
22
+
23
+ const AGENT_ID = "review-agent";
24
+ const AGENT_TYPE = "review";
25
+ const POLL_MS = 1000;
26
+ const REVIEW_TIMEOUT_MS = 5 * 60 * 1000;
27
+
28
+ async function main(): Promise<void> {
29
+ console.log(`[${AGENT_ID}] Starting...`);
30
+ const paths = ensureHerdrWorkspace();
31
+
32
+ // Find active loop config
33
+ const loopId = await findActiveLoop(paths.root);
34
+ if (!loopId) console.log(`[${AGENT_ID}] No active loop found. Waiting...`);
35
+
36
+ const blackboard = new SharedBlackboard(loopId ?? "no-loop", paths.root);
37
+ blackboard.load();
38
+ blackboard.registerAgent(AGENT_ID, "Review Agent", "openai", "gpt-4o");
39
+ blackboard.updateAgentStatus(AGENT_ID, "idle");
40
+ console.log(`[${AGENT_ID}] Registered. Blackboard: ${blackboard.getPath()}`);
41
+
42
+ while (true) {
43
+ await sleep(POLL_MS);
44
+
45
+ blackboard.load();
46
+ const record = blackboard.getRecord();
47
+ if (!record) continue;
48
+ if (record.jobId !== loopId) continue;
49
+
50
+ // Check for early exit
51
+ const earlyExit = checkEarlyExit(record);
52
+ if (earlyExit) {
53
+ console.log(`[${AGENT_ID}] Loop ended: ${earlyExit}`);
54
+ break;
55
+ }
56
+
57
+ // Check nextAction for review agent
58
+ const action = parseNextAction(record.nextAction);
59
+ if (!action || action.agentType !== AGENT_TYPE) continue;
60
+
61
+ // Claim the task
62
+ const locked = blackboard.acquireLock(action.taskId, AGENT_ID);
63
+ if (!locked) continue;
64
+
65
+ blackboard.updateAgentStatus(AGENT_ID, "working", action.taskId);
66
+ console.log(`[${AGENT_ID}] Claimed: ${action.taskId}`);
67
+
68
+ // Handle report task
69
+ if (action.taskId === "report") {
70
+ await finishReport(blackboard, record);
71
+ break;
72
+ }
73
+
74
+ // Do the review
75
+ const verdict = await doReview(action, paths, record.jobId);
76
+
77
+ // Update task status
78
+ updateTaskStatus(blackboard, record, action.taskId, verdict);
79
+
80
+ // Release and report
81
+ blackboard.releaseLock(action.taskId, AGENT_ID);
82
+ blackboard.writeReport({
83
+ agentId: AGENT_ID,
84
+ taskId: action.taskId,
85
+ status: "success",
86
+ message: verdict,
87
+ } as unknown as Parameters<typeof blackboard.writeReport>[0]);
88
+ blackboard.updateAgentStatus(AGENT_ID, "idle");
89
+
90
+ // Decide next action based on verdict
91
+ if (verdict === "approved" || verdict === "blocked") {
92
+ // Early exit — go to report
93
+ blackboard.setNextAction(
94
+ encodeNextAction({
95
+ taskId: "report",
96
+ agentType: "review",
97
+ iteration: 0,
98
+ }),
99
+ );
100
+ console.log(
101
+ `[${AGENT_ID}] ${verdict.toUpperCase()} — nextAction: report`,
102
+ );
103
+ } else {
104
+ // Changes requested — continue to next write
105
+ const nextWrite = getNextWriteTask(record, action.iteration);
106
+ if (nextWrite) {
107
+ blackboard.setNextAction(
108
+ encodeNextAction({
109
+ taskId: nextWrite,
110
+ agentType: "code",
111
+ iteration: getWriteIteration(nextWrite),
112
+ prompt: action.prompt,
113
+ }),
114
+ );
115
+ console.log(`[${AGENT_ID}] CHANGES — nextAction: code ${nextWrite}`);
116
+ } else {
117
+ blackboard.setNextAction({
118
+ taskId: "report",
119
+ agentType: "review",
120
+ iteration: 0,
121
+ } as unknown as ReturnType<typeof encodeNextAction>);
122
+ console.log(`[${AGENT_ID}] CHANGES — nextAction: report`);
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ // ─── Review Logic ────────────────────────────────────────────────────────
129
+
130
+ async function doReview(
131
+ action: LoopNextAction,
132
+ paths: { reviews: string },
133
+ loopId: string,
134
+ ): Promise<LoopVerdict> {
135
+ const reviewDir = join(paths.reviews, loopId);
136
+ mkdirSync(reviewDir, { recursive: true });
137
+
138
+ const reviewFile = join(reviewDir, `review-${action.iteration}.md`);
139
+
140
+ // Read code files
141
+ const codeBlocks = await Promise.all(
142
+ (action.codeFiles ?? []).map(async (file) => {
143
+ const content = existsSync(file)
144
+ ? readFileSync(file, "utf-8").slice(0, 3000)
145
+ : `[Not found: ${file}]`;
146
+ return `## ${file}\n\n\`\`\`\n${content}\n\`\`\`\n`;
147
+ }),
148
+ );
149
+
150
+ // Write review stub
151
+ const review = `# Review — Iteration ${action.iteration}
152
+
153
+ > GPT tab: Edit this file and add your verdict at the bottom.
154
+ > Use: \`## Verdict: APPROVED\`, \`## Verdict: CHANGES_REQUESTED\`, or \`## Verdict: BLOCKED\`
155
+ > GPT tab has ${REVIEW_TIMEOUT_MS / 1000 / 60} minutes to complete.
156
+
157
+ ${codeBlocks.join("\n")}
158
+ `;
159
+ writeFileSync(reviewFile, review);
160
+ console.log(`[${AGENT_ID}] Review stub: ${reviewFile}`);
161
+ console.log(`[${AGENT_ID}] Waiting for GPT verdict...`);
162
+
163
+ // Poll for verdict
164
+ return await pollVerdict(reviewFile);
165
+ }
166
+
167
+ async function pollVerdict(reviewFile: string): Promise<LoopVerdict> {
168
+ const deadline = Date.now() + REVIEW_TIMEOUT_MS;
169
+ while (Date.now() < deadline) {
170
+ if (existsSync(reviewFile)) {
171
+ const content = readFileSync(reviewFile, "utf-8");
172
+ const verdict = parseVerdict(content);
173
+ if (verdict) return verdict;
174
+ }
175
+ await sleep(3000);
176
+ }
177
+ console.log(
178
+ `[${AGENT_ID}] Verdict timeout — defaulting to CHANGES_REQUESTED`,
179
+ );
180
+ return "changes_requested";
181
+ }
182
+
183
+ async function finishReport(
184
+ blackboard: SharedBlackboard,
185
+ record: { tasks: { nodes: Record<string, { status: string }> } },
186
+ ): Promise<void> {
187
+ const reportNode = record.tasks.nodes["report"] as { status: string };
188
+ reportNode.status = "done";
189
+ blackboard.save();
190
+ console.log(`[${AGENT_ID}] Report complete. Loop finished.`);
191
+ }
192
+
193
+ // ─── Helpers ────────────────────────────────────────────────────────────────
194
+
195
+ async function findActiveLoop(rootDir: string): Promise<string | null> {
196
+ const { readdirSync } = await import("fs");
197
+ try {
198
+ const files = readdirSync(rootDir).filter(
199
+ (f) => f.startsWith("loop-") && f.endsWith(".config.json"),
200
+ );
201
+ if (files.length > 0) {
202
+ const config = JSON.parse(readFileSync(join(rootDir, files[0]), "utf-8"));
203
+ return config.loopId;
204
+ }
205
+ } catch {
206
+ // no loop yet
207
+ }
208
+ return null;
209
+ }
210
+
211
+ function checkEarlyExit(record: {
212
+ tasks: { nodes: Record<string, { status: string }> };
213
+ jobId: string;
214
+ }): string | null {
215
+ const reportNode = record.tasks.nodes["report"];
216
+ if (
217
+ reportNode &&
218
+ (reportNode.status === "done" || reportNode.status === "blocked")
219
+ ) {
220
+ return reportNode.status === "done" ? "finished" : "blocked";
221
+ }
222
+ return null;
223
+ }
224
+
225
+ function updateTaskStatus(
226
+ blackboard: SharedBlackboard,
227
+ record: {
228
+ tasks: { nodes: Record<string, { status: string; result?: string }> };
229
+ },
230
+ taskId: string,
231
+ verdict: LoopVerdict,
232
+ ): void {
233
+ const node = record.tasks.nodes[taskId] as
234
+ | { status: string; result?: string }
235
+ | undefined;
236
+ if (node) {
237
+ node.status = verdict === "blocked" ? "blocked" : "done";
238
+ node.result = verdict;
239
+ node.updatedAt = new Date().toISOString();
240
+ blackboard.save();
241
+ }
242
+ }
243
+
244
+ function encodeNextAction(action: Partial<LoopNextAction>) {
245
+ return {
246
+ taskId: action.taskId,
247
+ instruction: "LOOP:" + JSON.stringify(action),
248
+ priority: "high" as const,
249
+ createdAt: new Date().toISOString(),
250
+ };
251
+ }
252
+
253
+ function parseNextAction(nextAction: unknown): LoopNextAction | null {
254
+ if (!nextAction) return null;
255
+ const a = nextAction as Record<string, unknown>;
256
+ if (!a.taskId) return null;
257
+ if (typeof a.instruction === "string" && a.instruction.startsWith("LOOP:")) {
258
+ try {
259
+ return JSON.parse(a.instruction.slice(5)) as LoopNextAction;
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+
267
+ function getNextWriteTask(
268
+ record: { tasks: { nodes: Record<string, { status: string }> } },
269
+ currentReviewIteration: number,
270
+ ): string | null {
271
+ const writeCount = Object.keys(record.tasks.nodes).filter((id) =>
272
+ id.startsWith("write-"),
273
+ ).length;
274
+ // After review N, next write is N+1
275
+ const nextWrite = currentReviewIteration + 1;
276
+ const taskId = `write-${nextWrite}`;
277
+ if (
278
+ record.tasks.nodes[taskId] &&
279
+ record.tasks.nodes[taskId].status === "pending"
280
+ ) {
281
+ return taskId;
282
+ }
283
+ return null;
284
+ }
285
+
286
+ function getWriteIteration(taskId: string): number {
287
+ const m = taskId.match(/^write-(\d+)$/);
288
+ return m ? parseInt(m[1], 10) : 0;
289
+ }
290
+
291
+ function sleep(ms: number): Promise<void> {
292
+ return new Promise((r) => setTimeout(r, ms));
293
+ }
294
+
295
+ main().catch(console.error);
@@ -46,12 +46,12 @@ import {
46
46
  createForkedSummarizer,
47
47
  } from "./forked-summarizer.js";
48
48
 
49
- // ─── Constants ────────────────────────────────────────────────────────────────
49
+ // --- Constants ----------------------------------------------------------------
50
50
 
51
51
  const DEFAULT_MAX_COMPACT_RETRIES = 3;
52
52
  const DEFAULT_MAX_OUTPUT_TOKENS = 4096;
53
53
 
54
- // ─── Lifecycle Callbacks ────────────────────────────────────────────────────
54
+ // --- Lifecycle Callbacks ----------------------------------------------------
55
55
 
56
56
  export interface LoopCallbacks {
57
57
  /** Fetch next task from queue */
@@ -97,7 +97,7 @@ export interface LoopCallbacks {
97
97
  onCompaction?: (result: CompactResult) => void;
98
98
  }
99
99
 
100
- // ─── Loop Result ───────────────────────────────────────────────────────────
100
+ // --- Loop Result -----------------------------------------------------------
101
101
 
102
102
  export interface LoopResult {
103
103
  success: boolean;
@@ -109,7 +109,7 @@ export interface LoopResult {
109
109
  totalCompactions?: number;
110
110
  }
111
111
 
112
- // ─── Loop Runtime ───────────────────────────────────────────────────────────
112
+ // --- Loop Runtime -----------------------------------------------------------
113
113
 
114
114
  export class LoopRuntime {
115
115
  private readonly config: Required<LoopConfig>;
@@ -221,12 +221,12 @@ export class LoopRuntime {
221
221
  }
222
222
  }
223
223
 
224
- // ─── Public API ─────────────────────────────────────────────────────────
224
+ // --- Public API ---------------------------------------------------------
225
225
 
226
226
  async run(): Promise<LoopResult> {
227
227
  this.running = true;
228
228
 
229
- // ─── Auto-resume from quota pause ──────────────────────────────────
229
+ // --- Auto-resume from quota pause ----------------------------------
230
230
  const checkpoint = await this.callbacks.onGetCheckpoint?.();
231
231
  if (checkpoint?.status === "paused_quota" && checkpoint.resumeAt) {
232
232
  const resumeMs = new Date(checkpoint.resumeAt).getTime();
@@ -348,7 +348,7 @@ export class LoopRuntime {
348
348
  return this.sessionMemory;
349
349
  }
350
350
 
351
- // ─── Task Execution with Compact ───────────────────────────────────────
351
+ // --- Task Execution with Compact ---------------------------------------
352
352
 
353
353
  private async executeTaskWithCompact(task: RuntimeTask): Promise<void> {
354
354
  // 1. Pick model
@@ -690,7 +690,7 @@ export class LoopRuntime {
690
690
  return parts.join("\n");
691
691
  }
692
692
 
693
- // ─── Quota Handling ────────────────────────────────────────────────────
693
+ // --- Quota Handling ----------------------------------------------------
694
694
 
695
695
  private async handleQuotaPause(): Promise<void> {
696
696
  this.paused = true;
@@ -717,7 +717,7 @@ export class LoopRuntime {
717
717
  await this.saveCheckpoint();
718
718
  }
719
719
 
720
- // ─── Persistence ────────────────────────────────────────────────────────
720
+ // --- Persistence --------------------------------------------------------
721
721
 
722
722
  private async saveCheckpoint(): Promise<void> {
723
723
  const now = new Date().toISOString();
@@ -736,7 +736,7 @@ export class LoopRuntime {
736
736
  await this.callbacks.onCheckpoint?.(checkpoint);
737
737
  }
738
738
 
739
- // ─── Resume from Checkpoint ────────────────────────────────────────────
739
+ // --- Resume from Checkpoint --------------------------------------------
740
740
 
741
741
  async resumeFromCheckpoint(
742
742
  checkpoint: RuntimeCheckpoint,
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Shared types for loop coordination via blackboard.
3
+ */
4
+
5
+ import type {
6
+ TaskGraph,
7
+ TaskNode,
8
+ } from "../packages/types/src/runtime-types.ts";
9
+
10
+ export type LoopVerdict = "approved" | "changes_requested" | "blocked";
11
+ export type LoopNodeType = "write" | "review" | "decide" | "report";
12
+ export type LoopNodeStatus = "pending" | "running" | "done" | "blocked";
13
+
14
+ export interface LoopNextAction {
15
+ taskId: string;
16
+ agentType: "code" | "review";
17
+ iteration: number;
18
+ prompt?: string;
19
+ codeFiles?: string[];
20
+ }
21
+
22
+ export interface LoopAgentReport {
23
+ agentId: string;
24
+ taskId: string;
25
+ status: "running" | "done" | "failed";
26
+ message?: string;
27
+ files?: string[];
28
+ verdict?: LoopVerdict;
29
+ }
30
+
31
+ export interface LoopTaskGraph {
32
+ nodes: Record<string, TaskNode>;
33
+ topologicalOrder: string[];
34
+ }
35
+
36
+ export function buildLoopTaskGraph(
37
+ writeCount: number,
38
+ reviewCount: number,
39
+ loopId: string,
40
+ ): TaskGraph {
41
+ const now = new Date().toISOString();
42
+ const nodes: Record<string, TaskNode> = {};
43
+ const order: string[] = [];
44
+
45
+ // Write tasks
46
+ for (let i = 1; i <= writeCount; i++) {
47
+ const id = `write-${i}`;
48
+ nodes[id] = {
49
+ id,
50
+ title: `Code Write ${i}`,
51
+ description: `Write iteration ${i}`,
52
+ status: "pending",
53
+ dependencies: [],
54
+ dependents: [],
55
+ createdAt: now,
56
+ updatedAt: now,
57
+ };
58
+ order.push(id);
59
+ }
60
+
61
+ // Review tasks
62
+ const step = Math.max(1, Math.floor(writeCount / (reviewCount || 1)));
63
+ for (let i = 1; i <= reviewCount; i++) {
64
+ const id = `review-${i}`;
65
+ const writeIdx = i * step;
66
+ nodes[id] = {
67
+ id,
68
+ title: `Code Review ${i}`,
69
+ description: `Review iteration ${i}`,
70
+ status: "pending",
71
+ dependencies: writeIdx <= writeCount ? [`write-${writeIdx}`] : [],
72
+ dependents: [],
73
+ createdAt: now,
74
+ updatedAt: now,
75
+ };
76
+ // Wire dependencies
77
+ if (writeIdx <= writeCount) {
78
+ nodes[`write-${writeIdx}`].dependents.push(id);
79
+ }
80
+ order.push(id);
81
+ }
82
+
83
+ // Report task
84
+ const reportId = "report";
85
+ nodes[reportId] = {
86
+ id: reportId,
87
+ title: "Loop Report",
88
+ description: "Final summary",
89
+ status: "pending",
90
+ dependencies: [],
91
+ dependents: [],
92
+ createdAt: now,
93
+ updatedAt: now,
94
+ };
95
+ order.push(reportId);
96
+
97
+ return {
98
+ jobId: loopId,
99
+ nodes,
100
+ topologicalOrder: order,
101
+ };
102
+ }
103
+
104
+ export function getNodeStatus(
105
+ graph: TaskGraph,
106
+ nodeId: string,
107
+ ): LoopNodeStatus | undefined {
108
+ return graph.nodes[nodeId]?.status as LoopNodeStatus | undefined;
109
+ }
110
+
111
+ export function getReviewVerdict(
112
+ graph: TaskGraph,
113
+ reviewId: string,
114
+ ): LoopVerdict | undefined {
115
+ const node = graph.nodes[reviewId];
116
+ if (!node) return undefined;
117
+ return (node as unknown as { result?: LoopVerdict }).result;
118
+ }