@aion0/forge 0.5.26 → 0.5.28

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 (255) hide show
  1. package/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
  2. package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
  3. package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
  4. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
  5. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
  6. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
  7. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
  8. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
  9. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
  10. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
  11. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
  12. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
  13. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
  14. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
  15. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
  16. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
  17. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
  18. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
  19. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
  20. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
  21. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
  22. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
  23. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
  24. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
  25. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
  26. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
  27. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
  28. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
  29. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
  30. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
  31. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
  32. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
  33. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
  34. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
  35. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
  36. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
  37. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
  38. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
  39. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
  40. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
  41. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
  42. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
  43. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
  44. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
  45. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
  46. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
  47. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
  48. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
  49. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
  50. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
  51. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
  52. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
  53. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
  54. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
  55. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
  56. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
  57. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
  58. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
  59. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
  60. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
  61. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
  62. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
  63. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
  64. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
  65. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
  66. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
  67. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
  68. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
  69. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
  70. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
  71. package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
  72. package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
  73. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
  74. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
  75. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
  76. package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
  77. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
  78. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
  79. package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
  80. package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
  81. package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
  82. package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
  83. package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
  84. package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
  85. package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
  86. package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
  87. package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
  88. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
  89. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
  90. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
  91. package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
  92. package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
  93. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
  94. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
  95. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
  96. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
  97. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
  98. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
  99. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
  100. package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
  101. package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
  102. package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
  103. package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
  104. package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
  105. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
  106. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
  107. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
  108. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
  109. package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
  110. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
  111. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
  112. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
  113. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
  114. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
  115. package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
  116. package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
  117. package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
  118. package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
  119. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
  120. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
  121. package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
  122. package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
  123. package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
  124. package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
  125. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
  126. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
  127. package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
  128. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
  129. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
  130. package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
  131. package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
  132. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
  133. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
  134. package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
  135. package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
  136. package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
  137. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
  138. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
  139. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
  140. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
  141. package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
  142. package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
  143. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
  144. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
  145. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
  146. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
  147. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
  148. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
  149. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
  150. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
  151. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
  152. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
  153. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
  154. package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
  155. package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
  156. package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
  157. package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
  158. package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
  159. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
  160. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
  161. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
  162. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
  163. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
  164. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
  165. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
  166. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
  167. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
  168. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
  169. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
  170. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
  171. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
  172. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
  173. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
  174. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
  175. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
  176. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
  177. package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
  178. package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
  179. package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
  180. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
  181. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
  182. package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
  183. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
  184. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
  185. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
  186. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
  187. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
  188. package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
  189. package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
  190. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
  191. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
  192. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
  193. package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
  194. package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
  195. package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
  196. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
  197. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
  198. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
  199. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
  200. package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
  201. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
  202. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
  203. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
  204. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
  205. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
  206. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
  207. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
  208. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
  209. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
  210. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
  211. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
  212. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
  213. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
  214. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
  215. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
  216. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
  217. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
  218. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
  219. package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
  220. package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
  221. package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
  222. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
  223. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
  224. package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
  225. package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
  226. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
  227. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
  228. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
  229. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
  230. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
  231. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
  232. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
  233. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
  234. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
  235. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
  236. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
  237. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
  238. package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
  239. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
  240. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
  241. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
  242. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
  243. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
  244. package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
  245. package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
  246. package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
  247. package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
  248. package/RELEASE_NOTES.md +10 -29
  249. package/app/api/terminal-bell/route.ts +6 -2
  250. package/app/api/terminal-cwd/route.ts +7 -4
  251. package/components/CodeViewer.tsx +3 -31
  252. package/components/Dashboard.tsx +34 -20
  253. package/components/WebTerminal.tsx +36 -2
  254. package/lib/terminal-standalone.ts +19 -2
  255. package/package.json +1 -1
@@ -0,0 +1,635 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Benchmark Runner — Compare Claude Code single-agent vs Forge multi-smith workspace.
4
+ *
5
+ * Runs all tasks under `tasks/<task-name>/` through both harnesses.
6
+ * Tracks tokens + cost per run.
7
+ *
8
+ * Usage: pnpm tsx scripts/bench/run.ts [task-name]
9
+ * Without args: runs all tasks.
10
+ * With task name (e.g. "01-text-utils"): runs only that task.
11
+ */
12
+
13
+ import { execSync, spawn } from 'node:child_process';
14
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, realpathSync } from 'node:fs';
15
+ import { join, dirname } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { homedir } from 'node:os';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ // Use isolated bench project to avoid touching user's real workspaces.
21
+ // Create with: mkdir -p /tmp/forge-bench-project/src && cd /tmp/forge-bench-project && git init
22
+ const PROJECT = process.env.BENCH_PROJECT || '/tmp/forge-bench-project';
23
+ const FORGE_URL = process.env.FORGE_URL || 'http://localhost:8403';
24
+ const TASKS_DIR = join(__dirname, 'tasks');
25
+ const RESULTS_DIR = join(__dirname, 'results');
26
+ const TASK_TIMEOUT_MS = 25 * 60 * 1000; // 25 min per run
27
+ const POLL_INTERVAL_MS = 10_000;
28
+
29
+ // Claude Code session dirs: root + any sub-workdirs smiths might use.
30
+ // Claude resolves symlinks (e.g. /tmp → /private/tmp on macOS) when encoding project paths.
31
+ const CLAUDE_PROJECTS_ROOT = join(homedir(), '.claude', 'projects');
32
+ const encodePath = (p: string) => p.replace(/[^a-zA-Z0-9]/g, '-');
33
+ const PROJECT_REAL = (() => { try { return realpathSync(PROJECT); } catch { return PROJECT; } })();
34
+ const CLAUDE_PROJECT_DIRS = [
35
+ join(CLAUDE_PROJECTS_ROOT, encodePath(PROJECT_REAL)),
36
+ join(CLAUDE_PROJECTS_ROOT, encodePath(PROJECT_REAL + '/src')),
37
+ join(CLAUDE_PROJECTS_ROOT, encodePath(PROJECT_REAL + '/qa')),
38
+ join(CLAUDE_PROJECTS_ROOT, encodePath(PROJECT_REAL + '/review')),
39
+ ];
40
+
41
+ if (!existsSync(RESULTS_DIR)) mkdirSync(RESULTS_DIR, { recursive: true });
42
+
43
+ interface Usage {
44
+ inputTokens: number;
45
+ outputTokens: number;
46
+ cacheReadInputTokens: number;
47
+ cacheCreationInputTokens: number;
48
+ costUSD: number;
49
+ }
50
+
51
+ interface Result {
52
+ task: string;
53
+ harness: 'claude' | 'forge';
54
+ branch: string;
55
+ pass: boolean;
56
+ durationMs: number;
57
+ filesChanged: string[];
58
+ usage: Usage;
59
+ errorDetails?: string;
60
+ validatorTail?: string;
61
+ }
62
+
63
+ // ─── Git helpers ──────────────────────────────────────────
64
+
65
+ function git(cmd: string): string {
66
+ return execSync(`git ${cmd}`, { cwd: PROJECT, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
67
+ }
68
+
69
+ function ensureCleanBranch(): void {
70
+ console.log('[bench] Preparing harness_test repo...');
71
+ try { git('stash -u'); } catch {}
72
+ git('checkout main');
73
+ try { git('clean -fdx --exclude=.idea --exclude=node_modules --exclude=.forge'); } catch {}
74
+ const branches = git('branch --list bench/start');
75
+ if (branches) git('branch -D bench/start');
76
+ git('checkout -b bench/start');
77
+ console.log('[bench] bench/start branch ready at', git('rev-parse HEAD').slice(0, 7));
78
+ }
79
+
80
+ function createBranch(name: string): void {
81
+ try { git(`branch -D ${name}`); } catch {}
82
+ git(`checkout -B ${name} bench/start`);
83
+ }
84
+
85
+ function getChangedFiles(): string[] {
86
+ const diff = git(`diff --name-only bench/start...HEAD`);
87
+ return diff.split('\n').filter(Boolean);
88
+ }
89
+
90
+ // ─── Validator ────────────────────────────────────────────
91
+
92
+ function runValidator(validatorPath: string): { pass: boolean; output: string } {
93
+ try {
94
+ const output = execSync(`bash ${validatorPath} ${PROJECT}`, {
95
+ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
96
+ });
97
+ return { pass: true, output };
98
+ } catch (err: any) {
99
+ const output = (err.stdout || '') + (err.stderr || '') + '\n' + (err.message || '');
100
+ return { pass: false, output };
101
+ }
102
+ }
103
+
104
+ function runSetup(setupPath: string): void {
105
+ if (!existsSync(setupPath)) return;
106
+ console.log(`[bench] Running setup: ${setupPath}`);
107
+ execSync(`bash ${setupPath} ${PROJECT}`, { encoding: 'utf-8', stdio: 'inherit' });
108
+ // Commit setup output so bench branches start from the post-setup state
109
+ git('add -A');
110
+ try { git(`commit -m "task-setup" --allow-empty`); } catch {}
111
+ }
112
+
113
+ // ─── Token tracking ───────────────────────────────────────
114
+
115
+ function snapshotClaudeSessions(): Map<string, number> {
116
+ // Key: full file path, Value: byte size at snapshot time
117
+ const sizes = new Map<string, number>();
118
+ for (const dir of CLAUDE_PROJECT_DIRS) {
119
+ if (!existsSync(dir)) continue;
120
+ for (const f of readdirSync(dir)) {
121
+ if (!f.endsWith('.jsonl')) continue;
122
+ const fp = join(dir, f);
123
+ try { sizes.set(fp, statSync(fp).size); } catch {}
124
+ }
125
+ }
126
+ return sizes;
127
+ }
128
+
129
+ function computeForgeUsage(before: Map<string, number>): Usage {
130
+ const usage: Usage = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, cacheCreationInputTokens: 0, costUSD: 0 };
131
+ let newFiles = 0, modifiedFiles = 0, assistCount = 0;
132
+ for (const dir of CLAUDE_PROJECT_DIRS) {
133
+ if (!existsSync(dir)) continue;
134
+ for (const f of readdirSync(dir)) {
135
+ if (!f.endsWith('.jsonl')) continue;
136
+ const fp = join(dir, f);
137
+ const prevSize = before.has(fp) ? before.get(fp)! : 0;
138
+ const curSize = statSync(fp).size;
139
+ if (curSize <= prevSize) continue;
140
+ if (prevSize === 0) newFiles++; else modifiedFiles++;
141
+
142
+ // Read as buffer and slice by bytes (jsonl may have multi-byte chars)
143
+ const buf = readFileSync(fp);
144
+ const newContent = buf.slice(prevSize).toString('utf-8');
145
+ for (const line of newContent.split('\n')) {
146
+ if (!line.trim()) continue;
147
+ try {
148
+ const obj = JSON.parse(line);
149
+ const u = obj.message?.usage;
150
+ if (!u || obj.type !== 'assistant') continue;
151
+ assistCount++;
152
+ usage.inputTokens += u.input_tokens || 0;
153
+ usage.outputTokens += u.output_tokens || 0;
154
+ usage.cacheReadInputTokens += u.cache_read_input_tokens || 0;
155
+ usage.cacheCreationInputTokens += u.cache_creation_input_tokens || 0;
156
+ } catch {}
157
+ }
158
+ }
159
+ }
160
+ console.log(`[forge-usage] dirs=${CLAUDE_PROJECT_DIRS.length} new=${newFiles} modified=${modifiedFiles} entries=${assistCount}`);
161
+ // Rough cost estimate (Sonnet 4.6 pricing: $3/M input, $15/M output, $0.30/M cache read, $3.75/M cache creation)
162
+ // Numbers approximate, update if pricing changes
163
+ const COST_IN_PER_M = 3;
164
+ const COST_OUT_PER_M = 15;
165
+ const COST_CACHE_READ_PER_M = 0.3;
166
+ const COST_CACHE_CREATE_PER_M = 3.75;
167
+ usage.costUSD =
168
+ (usage.inputTokens * COST_IN_PER_M +
169
+ usage.outputTokens * COST_OUT_PER_M +
170
+ usage.cacheReadInputTokens * COST_CACHE_READ_PER_M +
171
+ usage.cacheCreationInputTokens * COST_CACHE_CREATE_PER_M) / 1_000_000;
172
+ return usage;
173
+ }
174
+
175
+ // ─── Claude Code headless runner ──────────────────────────
176
+
177
+ async function runClaudeCode(taskName: string, taskPrompt: string, branch: string): Promise<Result> {
178
+ console.log(`\n[claude] === Running Claude Code on branch ${branch} ===`);
179
+ createBranch(branch);
180
+ const start = Date.now();
181
+
182
+ return new Promise((resolve) => {
183
+ const child = spawn('claude', [
184
+ '-p',
185
+ '--dangerously-skip-permissions',
186
+ '--output-format', 'json',
187
+ taskPrompt,
188
+ ], {
189
+ cwd: PROJECT,
190
+ stdio: ['ignore', 'pipe', 'pipe'],
191
+ env: { ...process.env },
192
+ });
193
+
194
+ let stdout = '';
195
+ let stderr = '';
196
+ let timedOut = false;
197
+
198
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
199
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
200
+
201
+ const timer = setTimeout(() => {
202
+ timedOut = true;
203
+ child.kill('SIGTERM');
204
+ }, TASK_TIMEOUT_MS);
205
+
206
+ child.on('close', (code) => {
207
+ clearTimeout(timer);
208
+ const durationMs = Date.now() - start;
209
+ console.log(`[claude] Exit: ${code}, duration: ${Math.round(durationMs / 1000)}s${timedOut ? ' (TIMEOUT)' : ''}`);
210
+
211
+ // Parse usage from JSON output
212
+ const usage: Usage = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, cacheCreationInputTokens: 0, costUSD: 0 };
213
+ try {
214
+ const parsed = JSON.parse(stdout);
215
+ if (parsed.usage) {
216
+ usage.inputTokens = parsed.usage.input_tokens || 0;
217
+ usage.outputTokens = parsed.usage.output_tokens || 0;
218
+ usage.cacheReadInputTokens = parsed.usage.cache_read_input_tokens || 0;
219
+ usage.cacheCreationInputTokens = parsed.usage.cache_creation_input_tokens || 0;
220
+ }
221
+ usage.costUSD = parsed.total_cost_usd || 0;
222
+ } catch {}
223
+
224
+ // Commit and validate
225
+ try { git('add -A'); git('commit -m "claude-code run" --allow-empty'); } catch {}
226
+ const changed = getChangedFiles();
227
+
228
+ resolve({
229
+ task: taskName,
230
+ harness: 'claude',
231
+ branch,
232
+ pass: false,
233
+ durationMs,
234
+ filesChanged: changed,
235
+ usage,
236
+ errorDetails: timedOut ? 'TIMEOUT' : (code !== 0 ? `exit ${code}: ${stderr.slice(-300)}` : undefined),
237
+ });
238
+ });
239
+ });
240
+ }
241
+
242
+ // ─── Forge workspace API ──────────────────────────────────
243
+
244
+ async function api(path: string, opts: RequestInit = {}): Promise<any> {
245
+ const res = await fetch(`${FORGE_URL}${path}`, {
246
+ ...opts, headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) },
247
+ });
248
+ const text = await res.text();
249
+ let data: any; try { data = JSON.parse(text); } catch { data = text; }
250
+ if (!res.ok) throw new Error(`${res.status} ${path}: ${text.slice(0, 200)}`);
251
+ return data;
252
+ }
253
+
254
+ function makeAgent(opts: {
255
+ id: string; label: string; icon: string; workDir: string;
256
+ dependsOn: string[]; outputs: string[]; role: string;
257
+ steps: { id: string; label: string; prompt: string }[];
258
+ primary?: boolean;
259
+ }) {
260
+ return {
261
+ id: opts.id, label: opts.label, icon: opts.icon,
262
+ type: 'agent' as const, primary: opts.primary || false,
263
+ backend: 'cli', agentId: 'claude',
264
+ dependsOn: opts.dependsOn, workDir: opts.workDir, outputs: opts.outputs,
265
+ role: opts.role, steps: opts.steps,
266
+ persistentSession: true, skipPermissions: true,
267
+ };
268
+ }
269
+
270
+ function cleanStaleRequests(): void {
271
+ const reqDir = join(PROJECT, '.forge', 'requests');
272
+ if (existsSync(reqDir)) {
273
+ try {
274
+ execSync(`rm -rf ${reqDir}`, { encoding: 'utf-8' });
275
+ console.log('[bench] Cleaned stale .forge/requests/');
276
+ } catch {}
277
+ }
278
+ }
279
+
280
+ function killStaleTmuxSessions(): void {
281
+ try {
282
+ const list = execSync('tmux list-sessions 2>/dev/null || true', { encoding: 'utf-8' });
283
+ for (const line of list.split('\n')) {
284
+ const name = line.split(':')[0];
285
+ // Only kill sessions for the bench project — safe isolation
286
+ if (name.startsWith('mw-forge-forge-bench-project') || name.startsWith('mw-forge-bench-project')) {
287
+ try { execSync(`tmux kill-session -t "${name}" 2>/dev/null`, { timeout: 3000 }); } catch {}
288
+ }
289
+ }
290
+ console.log('[bench] Killed stale tmux sessions');
291
+ } catch {}
292
+ }
293
+
294
+ async function deleteExistingWorkspace(): Promise<void> {
295
+ try {
296
+ const existing = await api(`/api/workspace?projectPath=${encodeURIComponent(PROJECT)}`);
297
+ if (existing && existing.id) {
298
+ console.log(`[forge] Deleting existing workspace ${existing.id}`);
299
+ try { await api(`/api/workspace/${existing.id}/agents`, { method: 'POST', body: JSON.stringify({ action: 'stop_daemon' }) }); } catch {}
300
+ await new Promise(r => setTimeout(r, 2000));
301
+ await api(`/api/workspace?id=${existing.id}`, { method: 'DELETE' });
302
+ await new Promise(r => setTimeout(r, 1000));
303
+ }
304
+ } catch {}
305
+ killStaleTmuxSessions();
306
+ }
307
+
308
+ async function runForgeWorkspace(taskName: string, taskPrompt: string, branch: string, validatorPath: string): Promise<Result> {
309
+ console.log(`\n[forge] === Running Forge workspace on branch ${branch} ===`);
310
+ createBranch(branch);
311
+
312
+ const tokenSnapshot = snapshotClaudeSessions();
313
+ const start = Date.now();
314
+ let wsId: string | null = null;
315
+
316
+ try {
317
+ cleanStaleRequests();
318
+ await deleteExistingWorkspace();
319
+
320
+ const ws = await api('/api/workspace', {
321
+ method: 'POST',
322
+ body: JSON.stringify({ projectPath: PROJECT, projectName: 'forge-bench' }),
323
+ });
324
+ wsId = ws.id;
325
+ console.log(`[forge] Created workspace: ${wsId}`);
326
+
327
+ const ts = Date.now();
328
+ const inputId = `input-${ts}`;
329
+ const leadId = `lead-${ts}`;
330
+ const engId = `engineer-${ts}`;
331
+ const reviewerId = `reviewer-${ts}`;
332
+ const qaId = `qa-${ts}`;
333
+
334
+ // Input
335
+ await api(`/api/workspace/${wsId}/agents`, {
336
+ method: 'POST',
337
+ body: JSON.stringify({
338
+ action: 'add',
339
+ config: {
340
+ id: inputId, label: 'Requirements', icon: '📝',
341
+ type: 'input', content: '', entries: [],
342
+ role: '', backend: 'cli', dependsOn: [], outputs: [], steps: [],
343
+ },
344
+ }),
345
+ });
346
+
347
+ // Lead (Primary)
348
+ await api(`/api/workspace/${wsId}/agents`, {
349
+ method: 'POST',
350
+ body: JSON.stringify({
351
+ action: 'add',
352
+ config: makeAgent({
353
+ id: leadId, label: 'Lead', icon: '👑', primary: true, workDir: './',
354
+ dependsOn: [inputId], outputs: ['docs/lead/'],
355
+ role: 'Lead coordinator. Read upstream task VERBATIM. Create one request with the FULL original task text as description AND every single requirement extracted into acceptance_criteria. Do NOT summarize or simplify — preserve all details.',
356
+ steps: [
357
+ { id: 'create-req', label: 'Create Request', prompt: 'Read the upstream task. Create ONE request via create_request:\n- title: short descriptive\n- description: THE ENTIRE ORIGINAL TASK TEXT copied verbatim (do not summarize)\n- acceptance_criteria: extract EVERY requirement as a testable bullet. Include every field, every validation rule, every edge case mentioned in the task. Do not drop any detail.\n- priority: "high"\nThen verify with list_requests.' },
358
+ ],
359
+ }),
360
+ }),
361
+ });
362
+
363
+ // Engineer
364
+ await api(`/api/workspace/${wsId}/agents`, {
365
+ method: 'POST',
366
+ body: JSON.stringify({
367
+ action: 'add',
368
+ config: makeAgent({
369
+ id: engId, label: 'Engineer', icon: '🔨', workDir: './src',
370
+ dependsOn: [leadId], outputs: ['src/'],
371
+ role: 'Engineer. Claim open requests. Implement COMPLETELY — every acceptance_criterion, every validation rule, every field specified. If Reviewer requests changes, read their findings and fix ALL items listed.',
372
+ steps: [
373
+ { id: 'claim', label: 'Claim', prompt: 'list_requests(status: "open"). If the request has a review section with result "changes_requested", skip the claim step and go directly to fixing. Otherwise claim_request on the first open item.' },
374
+ { id: 'implement', label: 'Implement', prompt: 'get_request for the item. Read the ORIGINAL task description AND every single acceptance_criterion. Implement ALL of them — do not skip any. Every field, every validation, every edge case. Write tests covering each criterion. Run tests to verify pass.' },
375
+ { id: 'report', label: 'Report', prompt: 'update_response(section: "engineer", data: { files_changed: [...], notes: "list each criterion and how you implemented it" }).' },
376
+ ],
377
+ }),
378
+ }),
379
+ });
380
+
381
+ // Reviewer — checks Engineer's work against the ORIGINAL task spec, not just Engineer's tests
382
+ await api(`/api/workspace/${wsId}/agents`, {
383
+ method: 'POST',
384
+ body: JSON.stringify({
385
+ action: 'add',
386
+ config: makeAgent({
387
+ id: reviewerId, label: 'Reviewer', icon: '🔍', workDir: './review',
388
+ dependsOn: [engId], outputs: ['docs/review/'],
389
+ role: 'Reviewer. Verify the Engineer\'s implementation matches the ORIGINAL task spec (not just the tests they wrote). Read the request description AND acceptance_criteria. Check for missing features, missing validation, missing edge cases.',
390
+ steps: [
391
+ { id: 'review', label: 'Review', prompt: 'list_requests(status: "review"). get_request for details. READ the ORIGINAL task description carefully. Then read the files the Engineer created. Check: (1) every acceptance_criterion is covered, (2) all error handling specified in the task is implemented, (3) all return/output fields specified are present. If anything is missing, list specific issues. update_response(section: "review") with result: "approved" if all matches, or "changes_requested" with findings listing each missing piece.' },
392
+ ],
393
+ }),
394
+ }),
395
+ });
396
+
397
+ // QA
398
+ await api(`/api/workspace/${wsId}/agents`, {
399
+ method: 'POST',
400
+ body: JSON.stringify({
401
+ action: 'add',
402
+ config: makeAgent({
403
+ id: qaId, label: 'QA', icon: '🧪', workDir: './qa',
404
+ dependsOn: [reviewerId], outputs: ['docs/qa/'],
405
+ role: 'QA. After Reviewer approves, write INDEPENDENT test cases based on the ORIGINAL task spec. Test every acceptance_criterion, including edge cases and validation. Do not just run engineer\'s existing tests — verify the spec yourself.',
406
+ steps: [
407
+ { id: 'test', label: 'Test', prompt: 'list_requests(status: "qa"). get_request for details. Read the ORIGINAL task description. Write your own independent tests that verify EACH acceptance_criterion and error case from the spec. Run them. update_response(section: "qa") with result passed/failed and findings.' },
408
+ ],
409
+ }),
410
+ }),
411
+ });
412
+ console.log('[forge] Added 5 agents (Input + Lead + Engineer + Reviewer + QA)');
413
+
414
+ await api(`/api/workspace/${wsId}/agents`, { method: 'POST', body: JSON.stringify({ action: 'start_daemon' }) });
415
+ console.log('[forge] Daemon started');
416
+ await new Promise(r => setTimeout(r, 10_000));
417
+
418
+ await api(`/api/workspace/${wsId}/agents`, {
419
+ method: 'POST', body: JSON.stringify({ action: 'complete_input', agentId: inputId, content: taskPrompt }),
420
+ });
421
+ console.log('[forge] Input submitted — bus auto-notifies Lead via persistent session (with MCP)');
422
+
423
+ // Poll strategy: validator + all-done stability check.
424
+ // Break when validator passes, or when all smiths stable done for 90s (they gave up).
425
+ const deadline = Date.now() + TASK_TIMEOUT_MS;
426
+ const IDLE_GIVEUP_MS = 90_000;
427
+ let pollCount = 0;
428
+ let validatorPassed = false;
429
+ let firstAllIdleAt: number | null = null;
430
+ while (Date.now() < deadline) {
431
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
432
+ pollCount++;
433
+ const state = await api(`/api/workspace/${wsId}/agents`);
434
+ const agentStates = state.states || state.agentStates || {};
435
+ const smiths = (state.agents || []).filter((a: any) => a.type !== 'input');
436
+ const statuses = smiths.map((a: any) => {
437
+ const s = agentStates[a.id];
438
+ return `${a.label}=${s?.taskStatus || '?'}`;
439
+ }).join(' ');
440
+ const allIdle = smiths.every((a: any) => {
441
+ const ts = agentStates[a.id]?.taskStatus;
442
+ return ts === 'done' || ts === 'failed' || ts === 'idle';
443
+ });
444
+ if (allIdle) {
445
+ if (firstAllIdleAt === null) firstAllIdleAt = Date.now();
446
+ } else {
447
+ firstAllIdleAt = null;
448
+ }
449
+ const v = runValidator(validatorPath);
450
+ const idleFor = firstAllIdleAt ? Date.now() - firstAllIdleAt : 0;
451
+ console.log(`[forge] Poll ${pollCount}: ${statuses} | validator=${v.pass ? 'PASS' : 'fail'}${firstAllIdleAt ? ` idle=${Math.round(idleFor/1000)}s` : ''}`);
452
+ if (v.pass) { validatorPassed = true; break; }
453
+ if (firstAllIdleAt && idleFor >= IDLE_GIVEUP_MS) { console.log('[forge] All smiths idle too long — giving up'); break; }
454
+ }
455
+ console.log(`[forge] Exit: validator ${validatorPassed ? 'passed' : 'FAILED/TIMEOUT'}`);
456
+ const durationMs = Date.now() - start;
457
+
458
+ try { await api(`/api/workspace/${wsId}/agents`, { method: 'POST', body: JSON.stringify({ action: 'stop_daemon' }) }); } catch {}
459
+ try { git('add -A'); git('commit -m "forge-workspace run" --allow-empty'); } catch {}
460
+
461
+ const changed = getChangedFiles();
462
+ const usage = computeForgeUsage(tokenSnapshot);
463
+
464
+ return {
465
+ task: taskName,
466
+ harness: 'forge',
467
+ branch,
468
+ pass: false,
469
+ durationMs,
470
+ filesChanged: changed,
471
+ usage,
472
+ };
473
+ } catch (err: any) {
474
+ console.error('[forge] Error:', err.message);
475
+ if (wsId) {
476
+ try { await api(`/api/workspace/${wsId}/agents`, { method: 'POST', body: JSON.stringify({ action: 'stop_daemon' }) }); } catch {}
477
+ }
478
+ return {
479
+ task: taskName,
480
+ harness: 'forge',
481
+ branch,
482
+ pass: false,
483
+ durationMs: Date.now() - start,
484
+ filesChanged: [],
485
+ usage: computeForgeUsage(tokenSnapshot),
486
+ errorDetails: err.message,
487
+ };
488
+ }
489
+ }
490
+
491
+ // ─── Task loader ──────────────────────────────────────────
492
+
493
+ interface Task {
494
+ name: string;
495
+ dir: string;
496
+ promptPath: string;
497
+ validatorPath: string;
498
+ setupPath?: string;
499
+ }
500
+
501
+ function loadTasks(filter?: string): Task[] {
502
+ const dirs = readdirSync(TASKS_DIR, { withFileTypes: true })
503
+ .filter(d => d.isDirectory() && d.name !== 'results')
504
+ .map(d => d.name)
505
+ .sort();
506
+ const tasks: Task[] = [];
507
+ for (const name of dirs) {
508
+ if (filter && !name.includes(filter)) continue;
509
+ const dir = join(TASKS_DIR, name);
510
+ const promptPath = join(dir, 'task.md');
511
+ const validatorPath = join(dir, 'validator.sh');
512
+ const setupPath = join(dir, 'setup.sh');
513
+ if (!existsSync(promptPath) || !existsSync(validatorPath)) continue;
514
+ tasks.push({
515
+ name, dir, promptPath, validatorPath,
516
+ setupPath: existsSync(setupPath) ? setupPath : undefined,
517
+ });
518
+ }
519
+ return tasks;
520
+ }
521
+
522
+ // ─── Report ───────────────────────────────────────────────
523
+
524
+ function fmtTokens(u: Usage): string {
525
+ const total = u.inputTokens + u.outputTokens + u.cacheReadInputTokens + u.cacheCreationInputTokens;
526
+ return `${(total / 1000).toFixed(1)}K (in=${u.inputTokens}, out=${u.outputTokens}, cache_r=${(u.cacheReadInputTokens/1000).toFixed(1)}K, cache_w=${(u.cacheCreationInputTokens/1000).toFixed(1)}K)`;
527
+ }
528
+
529
+ function writeReport(results: Result[]): string {
530
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
531
+ const byTask = new Map<string, Result[]>();
532
+ for (const r of results) {
533
+ if (!byTask.has(r.task)) byTask.set(r.task, []);
534
+ byTask.get(r.task)!.push(r);
535
+ }
536
+
537
+ // Summary table
538
+ let md = `# Benchmark Run — ${new Date().toLocaleString()}\n\n## Summary\n\n`;
539
+ md += `| Task | Harness | Pass | Duration | Tokens | Cost | Files |\n`;
540
+ md += `|------|---------|------|----------|--------|------|-------|\n`;
541
+ for (const r of results) {
542
+ const total = r.usage.inputTokens + r.usage.outputTokens + r.usage.cacheReadInputTokens + r.usage.cacheCreationInputTokens;
543
+ md += `| ${r.task} | ${r.harness} | ${r.pass ? '✅' : '❌'} | ${Math.round(r.durationMs / 1000)}s | ${(total/1000).toFixed(1)}K | $${r.usage.costUSD.toFixed(3)} | ${r.filesChanged.length} |\n`;
544
+ }
545
+
546
+ // Per-task comparison
547
+ md += `\n## Per-Task Comparison\n\n`;
548
+ for (const [task, taskResults] of byTask) {
549
+ const claude = taskResults.find(r => r.harness === 'claude');
550
+ const forge = taskResults.find(r => r.harness === 'forge');
551
+ md += `### ${task}\n\n`;
552
+ md += `| Metric | Claude Code | Forge Workspace | Ratio |\n`;
553
+ md += `|--------|-------------|-----------------|-------|\n`;
554
+ if (claude && forge) {
555
+ const cTot = claude.usage.inputTokens + claude.usage.outputTokens + claude.usage.cacheReadInputTokens + claude.usage.cacheCreationInputTokens;
556
+ const fTot = forge.usage.inputTokens + forge.usage.outputTokens + forge.usage.cacheReadInputTokens + forge.usage.cacheCreationInputTokens;
557
+ md += `| Result | ${claude.pass ? '✅' : '❌'} | ${forge.pass ? '✅' : '❌'} | - |\n`;
558
+ md += `| Duration | ${Math.round(claude.durationMs / 1000)}s | ${Math.round(forge.durationMs / 1000)}s | ${(forge.durationMs / claude.durationMs).toFixed(1)}x |\n`;
559
+ md += `| Total tokens | ${(cTot/1000).toFixed(1)}K | ${(fTot/1000).toFixed(1)}K | ${cTot > 0 ? (fTot / cTot).toFixed(1) + 'x' : '-'} |\n`;
560
+ md += `| Cost | $${claude.usage.costUSD.toFixed(3)} | $${forge.usage.costUSD.toFixed(3)} | ${claude.usage.costUSD > 0 ? (forge.usage.costUSD / claude.usage.costUSD).toFixed(1) + 'x' : '-'} |\n`;
561
+ md += `| Files changed | ${claude.filesChanged.length} | ${forge.filesChanged.length} | - |\n`;
562
+ md += `\n**Claude files**: ${claude.filesChanged.join(', ') || '(none)'}\n\n`;
563
+ md += `**Forge files**: ${forge.filesChanged.join(', ') || '(none)'}\n\n`;
564
+ if (claude.errorDetails) md += `**Claude error**: ${claude.errorDetails}\n\n`;
565
+ if (forge.errorDetails) md += `**Forge error**: ${forge.errorDetails}\n\n`;
566
+ }
567
+ md += '\n';
568
+ }
569
+
570
+ // Details — validator tails
571
+ md += `## Validator Output Tails\n\n`;
572
+ for (const r of results) {
573
+ const tail = (r.validatorTail || '').slice(-600);
574
+ md += `### ${r.task} / ${r.harness}\n\n\`\`\`\n${tail}\n\`\`\`\n\n`;
575
+ }
576
+
577
+ const outPath = join(RESULTS_DIR, `report-${ts}.md`);
578
+ writeFileSync(outPath, md);
579
+ writeFileSync(join(RESULTS_DIR, `report-${ts}.json`), JSON.stringify(results, null, 2));
580
+ return outPath;
581
+ }
582
+
583
+ // ─── Main ─────────────────────────────────────────────────
584
+
585
+ async function main() {
586
+ const filter = process.argv[2]; // optional task filter
587
+ const tasks = loadTasks(filter);
588
+ if (tasks.length === 0) { console.error('No tasks found'); process.exit(1); }
589
+ console.log(`[bench] Running ${tasks.length} task(s): ${tasks.map(t => t.name).join(', ')}`);
590
+
591
+ ensureCleanBranch();
592
+ const results: Result[] = [];
593
+
594
+ for (const task of tasks) {
595
+ console.log(`\n${'='.repeat(60)}\n[bench] TASK: ${task.name}\n${'='.repeat(60)}`);
596
+ const prompt = readFileSync(task.promptPath, 'utf-8');
597
+
598
+ // Run setup (on bench/start so both harnesses inherit same starting state)
599
+ git('checkout bench/start');
600
+ if (task.setupPath) runSetup(task.setupPath);
601
+
602
+ const ts = Date.now();
603
+
604
+ // Claude Code
605
+ const claudeResult = await runClaudeCode(task.name, prompt, `bench/${task.name}-claude-${ts}`);
606
+ const cv = runValidator(task.validatorPath);
607
+ claudeResult.pass = cv.pass;
608
+ claudeResult.validatorTail = cv.output;
609
+ console.log(`[claude] ${task.name}: ${cv.pass ? 'PASS' : 'FAIL'}`);
610
+ results.push(claudeResult);
611
+
612
+ // Forge Workspace
613
+ const forgeResult = await runForgeWorkspace(task.name, prompt, `bench/${task.name}-forge-${ts}`, task.validatorPath);
614
+ const fv = runValidator(task.validatorPath);
615
+ forgeResult.pass = fv.pass;
616
+ forgeResult.validatorTail = fv.output;
617
+ console.log(`[forge] ${task.name}: ${fv.pass ? 'PASS' : 'FAIL'}`);
618
+ results.push(forgeResult);
619
+
620
+ // If setup existed, undo it from bench/start for subsequent tasks
621
+ if (task.setupPath) {
622
+ git('checkout bench/start');
623
+ try { git('reset --hard HEAD~1'); } catch {}
624
+ }
625
+ }
626
+
627
+ const reportPath = writeReport(results);
628
+ console.log(`\n[bench] Report written: ${reportPath}`);
629
+ console.log('\n' + readFileSync(reportPath, 'utf-8').split('## Per-Task')[0]);
630
+ }
631
+
632
+ main().catch(err => {
633
+ console.error('[bench] Fatal:', err);
634
+ process.exit(1);
635
+ });
@@ -0,0 +1,26 @@
1
+ # Task: Text Utility Module
2
+
3
+ Add a text utility module to the project.
4
+
5
+ ## Requirements
6
+
7
+ 1. Create a new file `src/utils/text.js` in the harness_test project.
8
+ 2. Export two functions:
9
+ - `capitalize(word)`: Returns the word with its first character uppercased. If input is not a string or is empty, throw a `TypeError`.
10
+ - `reverseWords(sentence)`: Returns the sentence with the order of words reversed. Words are separated by one or more whitespace characters. Leading/trailing whitespace should be trimmed. If input is not a string, throw a `TypeError`. An empty string returns an empty string.
11
+
12
+ 3. Create a test file `src/utils/text.test.js` using Node.js built-in `node:test` and `node:assert/strict`. Tests should cover:
13
+ - `capitalize`: normal words, single characters, unicode words
14
+ - `capitalize`: throws on empty string, null, undefined, numbers
15
+ - `reverseWords`: basic sentences, multiple spaces, leading/trailing spaces
16
+ - `reverseWords`: empty string, single word
17
+ - `reverseWords`: throws on non-string inputs
18
+
19
+ 4. The tests must pass when running: `cd src && node --test utils/text.test.js`
20
+
21
+ ## Constraints
22
+
23
+ - Use ES module syntax (`export`, `import`). The project's package.json already has `"type": "module"`.
24
+ - No external dependencies — use only Node built-ins.
25
+ - Keep functions pure (no side effects).
26
+ - Use `strict` mode assertions (`node:assert/strict`).