@aion0/forge 0.5.26 → 0.5.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) 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 +11 -28
  249. package/app/api/terminal-bell/route.ts +6 -2
  250. package/components/WebTerminal.tsx +36 -2
  251. package/lib/terminal-standalone.ts +19 -2
  252. package/next-env.d.ts +1 -1
  253. package/package.json +1 -1
@@ -0,0 +1,560 @@
1
+ /**
2
+ * WatchManager — autonomous periodic monitoring for workspace agents.
3
+ *
4
+ * Fully independent from message bus / worker / state management.
5
+ * Detects file changes, git diffs, agent outputs, or custom commands.
6
+ * Writes results to agent log + emits SSE events. Never sends messages.
7
+ */
8
+
9
+ import { EventEmitter } from 'node:events';
10
+ import { existsSync, readdirSync, statSync, readFileSync, openSync, readSync, fstatSync, closeSync } from 'node:fs';
11
+ import { join, relative } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+ import { execSync } from 'node:child_process';
14
+ import type { WorkspaceAgentConfig, WatchTarget, WatchConfig } from './types';
15
+ import { appendAgentLog } from './persistence';
16
+
17
+ // ─── Snapshot types ──────────────────────────────────────
18
+
19
+ interface WatchSnapshot {
20
+ lastCheckTime: number; // timestamp ms — only files modified after this are "changed"
21
+ gitHash?: string;
22
+ commandOutput?: string;
23
+ logLineCount?: number; // last known line count in agent's logs.jsonl
24
+ agentStatus?: string; // last known taskStatus of monitored agent
25
+ sessionFileSize?: number; // last known file size of session JSONL (bytes)
26
+ }
27
+
28
+ interface WatchChange {
29
+ targetType: WatchTarget['type'];
30
+ description: string;
31
+ files: string[];
32
+ }
33
+
34
+ // ─── Detection functions ─────────────────────────────────
35
+
36
+ /** Find files modified after `since` timestamp in a directory */
37
+ function findModifiedFiles(dir: string, since: number, pattern?: string, maxDepth = 3): string[] {
38
+ const modified: string[] = [];
39
+ if (!existsSync(dir)) return modified;
40
+
41
+ const globMatch = pattern
42
+ ? (name: string) => {
43
+ if (pattern.startsWith('*.')) return name.endsWith(pattern.slice(1));
44
+ return name.includes(pattern);
45
+ }
46
+ : () => true;
47
+
48
+ function walk(current: string, depth: number) {
49
+ if (depth > maxDepth) return;
50
+ try {
51
+ for (const entry of readdirSync(current)) {
52
+ if (entry.startsWith('.') || entry === 'node_modules') continue;
53
+ const full = join(current, entry);
54
+ try {
55
+ const st = statSync(full);
56
+ if (st.isDirectory()) {
57
+ walk(full, depth + 1);
58
+ } else if (globMatch(entry) && st.mtimeMs > since) {
59
+ modified.push(relative(dir, full));
60
+ }
61
+ } catch {}
62
+ }
63
+ } catch {}
64
+ }
65
+
66
+ walk(dir, 0);
67
+ return modified;
68
+ }
69
+
70
+ function detectDirectoryChanges(projectPath: string, target: WatchTarget, since: number): { changes: WatchChange | null } {
71
+ const dir = join(projectPath, target.path || '.');
72
+ const files = findModifiedFiles(dir, since, target.pattern);
73
+
74
+ if (files.length === 0) return { changes: null };
75
+
76
+ return {
77
+ changes: {
78
+ targetType: 'directory',
79
+ description: `${target.path || '.'}: ${files.length} file(s) changed`,
80
+ files: files.slice(0, 20),
81
+ },
82
+ };
83
+ }
84
+
85
+ function detectGitChanges(projectPath: string, prevHash?: string): { changes: WatchChange | null; gitHash: string } {
86
+ try {
87
+ const hash = execSync('git rev-parse HEAD', { cwd: projectPath, timeout: 5000 }).toString().trim();
88
+ if (hash === prevHash) return { changes: null, gitHash: hash };
89
+
90
+ // Get diff summary
91
+ let diffStat = '';
92
+ try {
93
+ const cmd = prevHash ? `git diff --stat ${prevHash}..${hash}` : 'git diff --stat HEAD~1';
94
+ diffStat = execSync(cmd, { cwd: projectPath, timeout: 5000 }).toString().trim();
95
+ } catch {}
96
+
97
+ const files = diffStat.split('\n')
98
+ .filter(l => l.includes('|'))
99
+ .map(l => l.trim().split(/\s+\|/)[0].trim())
100
+ .slice(0, 20);
101
+
102
+ return {
103
+ changes: {
104
+ targetType: 'git',
105
+ description: `New commit ${hash.slice(0, 8)}${files.length ? `: ${files.length} files changed` : ''}`,
106
+ files,
107
+ },
108
+ gitHash: hash,
109
+ };
110
+ } catch {
111
+ return { changes: null, gitHash: prevHash || '' };
112
+ }
113
+ }
114
+
115
+ function detectCommandChanges(projectPath: string, target: WatchTarget, prevOutput?: string): { changes: WatchChange | null; commandOutput: string } {
116
+ if (!target.cmd) return { changes: null, commandOutput: prevOutput || '' };
117
+ try {
118
+ const output = execSync(target.cmd, { cwd: projectPath, timeout: 30000 }).toString().trim();
119
+ if (output === prevOutput) return { changes: null, commandOutput: output };
120
+
121
+ // Check pattern match if specified
122
+ if (target.pattern && !output.includes(target.pattern)) {
123
+ return { changes: null, commandOutput: output };
124
+ }
125
+
126
+ return {
127
+ changes: {
128
+ targetType: 'command',
129
+ description: `Command "${target.cmd.slice(0, 40)}": output changed`,
130
+ files: [],
131
+ },
132
+ commandOutput: output,
133
+ };
134
+ } catch {
135
+ return { changes: null, commandOutput: prevOutput || '' };
136
+ }
137
+ }
138
+
139
+ function detectAgentLogChanges(workspaceId: string, targetAgentId: string, pattern: string | undefined, prevLineCount: number, contextChars = 500): { changes: WatchChange | null; lineCount: number } {
140
+ const logFile = join(homedir(), '.forge', 'workspaces', workspaceId, 'agents', targetAgentId, 'logs.jsonl');
141
+ if (!existsSync(logFile)) return { changes: null, lineCount: 0 };
142
+
143
+ try {
144
+ const lines = readFileSync(logFile, 'utf-8').split('\n').filter(Boolean);
145
+ const currentCount = lines.length;
146
+ if (currentCount <= prevLineCount) return { changes: null, lineCount: currentCount };
147
+
148
+ // Get new lines since last check
149
+ const newLines = lines.slice(prevLineCount);
150
+ const newEntries: string[] = [];
151
+
152
+ // Build matcher: try regex first, fallback to case-insensitive includes
153
+ let matcher: ((text: string) => boolean) | null = null;
154
+ if (pattern) {
155
+ try {
156
+ const re = new RegExp(pattern, 'i');
157
+ matcher = (text: string) => re.test(text);
158
+ } catch {
159
+ const lower = pattern.toLowerCase();
160
+ matcher = (text: string) => text.toLowerCase().includes(lower);
161
+ }
162
+ }
163
+
164
+ // Extract content around match (contextChars before + after match point)
165
+ const extractContext = (text: string): string => {
166
+ if (!pattern || text.length <= contextChars) return text.slice(0, contextChars);
167
+ // Find match position for context window
168
+ let matchIdx = 0;
169
+ try {
170
+ const re = new RegExp(pattern, 'i');
171
+ const m = re.exec(text);
172
+ if (m) matchIdx = m.index;
173
+ } catch {
174
+ matchIdx = text.toLowerCase().indexOf(pattern.toLowerCase());
175
+ if (matchIdx === -1) matchIdx = 0;
176
+ }
177
+ const half = Math.floor(contextChars / 2);
178
+ const start = Math.max(0, matchIdx - half);
179
+ const end = Math.min(text.length, start + contextChars);
180
+ return (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '');
181
+ };
182
+
183
+ for (const line of newLines) {
184
+ try {
185
+ const entry = JSON.parse(line);
186
+ const content = entry.content || '';
187
+ if (matcher && !matcher(content)) continue;
188
+ newEntries.push(extractContext(content));
189
+ } catch {
190
+ if (matcher && !matcher(line)) continue;
191
+ newEntries.push(extractContext(line));
192
+ }
193
+ }
194
+
195
+ if (newEntries.length === 0) return { changes: null, lineCount: currentCount };
196
+
197
+ return {
198
+ changes: {
199
+ targetType: 'agent_log',
200
+ description: `${newEntries.length} new log entries${pattern ? ` matching "${pattern}"` : ''}`,
201
+ files: newEntries.slice(0, 10),
202
+ },
203
+ lineCount: currentCount,
204
+ };
205
+ } catch {
206
+ return { changes: null, lineCount: prevLineCount };
207
+ }
208
+ }
209
+
210
+ // Track which session file was used last (to detect file switch)
211
+ const lastSessionFile = new Map<string, string>();
212
+
213
+ function detectSessionChanges(projectPath: string, pattern: string | undefined, prevLineCount: number, contextChars = 500, sessionId?: string): { changes: WatchChange | null; lineCount: number } {
214
+ const claudeHome = join(homedir(), '.claude', 'projects');
215
+ const encoded = projectPath.replace(/[^a-zA-Z0-9]/g, '-');
216
+ const sessionDir = join(claudeHome, encoded);
217
+ if (!existsSync(sessionDir)) return { changes: null, lineCount: prevLineCount };
218
+
219
+ try {
220
+ let latestFile: string;
221
+
222
+ if (sessionId) {
223
+ latestFile = join(sessionDir, `${sessionId}.jsonl`);
224
+ if (!existsSync(latestFile)) return { changes: null, lineCount: prevLineCount };
225
+ } else {
226
+ const files = readdirSync(sessionDir)
227
+ .filter(f => f.endsWith('.jsonl'))
228
+ .map(f => ({ name: f, mtime: statSync(join(sessionDir, f)).mtimeMs }))
229
+ .sort((a, b) => b.mtime - a.mtime);
230
+ if (files.length === 0) return { changes: null, lineCount: prevLineCount };
231
+ latestFile = join(sessionDir, files[0].name);
232
+ }
233
+ // Detect if session file changed (user started new session) → reset tracking
234
+ const cacheKey = `${projectPath}:${sessionId || 'latest'}`;
235
+ const prevFile = lastSessionFile.get(cacheKey);
236
+ if (prevFile && prevFile !== latestFile) {
237
+ // Session file switched — reset prevLineCount to read from start of new file
238
+ prevLineCount = 0;
239
+ console.log(`[watch] Session file switched: ${prevFile.split('/').pop()} → ${latestFile.split('/').pop()}`);
240
+ }
241
+ lastSessionFile.set(cacheKey, latestFile);
242
+
243
+ // Only read new bytes since last check (efficient for large files)
244
+ const fd = openSync(latestFile, 'r');
245
+ const fileSize = fstatSync(fd).size;
246
+ if (fileSize <= prevLineCount) { closeSync(fd); return { changes: null, lineCount: fileSize }; }
247
+
248
+ const readFrom = Math.max(0, prevLineCount > 0 ? prevLineCount - 1 : 0);
249
+ const readSize = Math.min(fileSize - readFrom, 500_000); // max 500KB per check
250
+ const buf = Buffer.alloc(readSize);
251
+ readSync(fd, buf, 0, readSize, readFrom);
252
+ closeSync(fd);
253
+
254
+ const tail = buf.toString('utf-8');
255
+ const newLines = tail.split('\n').filter(Boolean);
256
+ if (prevLineCount > 0 && newLines.length > 0) newLines.shift(); // skip partial first line
257
+
258
+ // Build matcher
259
+ let matcher: ((text: string) => boolean) | null = null;
260
+ if (pattern) {
261
+ try {
262
+ const re = new RegExp(pattern, 'i');
263
+ matcher = (text: string) => re.test(text);
264
+ } catch {
265
+ const lower = pattern.toLowerCase();
266
+ matcher = (text: string) => text.toLowerCase().includes(lower);
267
+ }
268
+ }
269
+
270
+ // Only extract the LAST assistant/result text (not all entries)
271
+ const entries: string[] = [];
272
+ for (const line of [...newLines].reverse()) {
273
+ try {
274
+ const parsed = JSON.parse(line);
275
+ let text = '';
276
+ if (parsed.type === 'assistant' && parsed.message?.content) {
277
+ for (const block of (Array.isArray(parsed.message.content) ? parsed.message.content : [parsed.message.content])) {
278
+ if (typeof block === 'string') text += block;
279
+ else if (block.type === 'text' && block.text) text += block.text;
280
+ }
281
+ } else if (parsed.type === 'result' && parsed.result) {
282
+ text = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
283
+ } else if (parsed.type === 'human' || parsed.type === 'user') {
284
+ const content = parsed.content || parsed.message?.content;
285
+ text = typeof content === 'string' ? content : '';
286
+ }
287
+
288
+ if (!text) continue;
289
+ if (matcher && !matcher(text)) continue;
290
+
291
+ // Context extraction around match
292
+ if (text.length > contextChars && pattern) {
293
+ let matchIdx = 0;
294
+ try { matchIdx = new RegExp(pattern, 'i').exec(text)?.index || 0; } catch {}
295
+ const half = Math.floor(contextChars / 2);
296
+ const start = Math.max(0, matchIdx - half);
297
+ const end = Math.min(text.length, start + contextChars);
298
+ text = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '');
299
+ } else {
300
+ text = text.slice(0, contextChars);
301
+ }
302
+ entries.push(text);
303
+ break; // only take the last matching entry (we're scanning in reverse)
304
+ } catch {}
305
+ }
306
+
307
+ if (entries.length === 0) return { changes: null, lineCount: fileSize };
308
+
309
+ return {
310
+ changes: {
311
+ targetType: 'session',
312
+ description: `${entries.length} new session entries${pattern ? ` matching "${pattern}"` : ''}`,
313
+ files: entries.slice(0, 10),
314
+ },
315
+ lineCount: fileSize, // actually bytes, reusing field name
316
+ };
317
+ } catch {
318
+ return { changes: null, lineCount: prevLineCount };
319
+ }
320
+ }
321
+
322
+ // ─── WatchManager class ──────────────────────────────────
323
+
324
+ export class WatchManager extends EventEmitter {
325
+ private timers = new Map<string, NodeJS.Timeout>();
326
+ private snapshots = new Map<string, WatchSnapshot>();
327
+ private pendingAlert = new Map<string, { changes: WatchChange[]; summary: string; timestamp: number }>();
328
+ private debounceTimers = new Map<string, NodeJS.Timeout>();
329
+
330
+ constructor(
331
+ private workspaceId: string,
332
+ private projectPath: string,
333
+ private getAgents: () => Map<string, { config: WorkspaceAgentConfig; state: { smithStatus: string; taskStatus: string; mode: string } }>,
334
+ ) {
335
+ super();
336
+ }
337
+
338
+ /** Start watch loops for all agents with watch config */
339
+ start(): void {
340
+ for (const [id, entry] of this.getAgents()) {
341
+ if (entry.config.watch?.enabled) {
342
+ this.startWatch(id, entry.config);
343
+ }
344
+ }
345
+ }
346
+
347
+ /** Stop all watch loops */
348
+ stop(): void {
349
+ for (const [id, timer] of this.timers) {
350
+ clearInterval(timer);
351
+ }
352
+ this.timers.clear();
353
+ console.log(`[watch] All watch loops stopped`);
354
+ }
355
+
356
+ /** Start/restart watch for a specific agent */
357
+ startWatch(agentId: string, config: WorkspaceAgentConfig): void {
358
+ this.stopWatch(agentId);
359
+ if (!config.watch?.enabled || config.watch.targets.length === 0) return;
360
+
361
+ const interval = Math.max(config.watch.interval || 60, 10) * 1000; // min 10s
362
+ console.log(`[watch] ${config.label}: started (interval=${interval / 1000}s, targets=${config.watch.targets.length})`);
363
+
364
+ // Initialize snapshot on first run (don't alert on existing state)
365
+ this.runCheck(agentId, config, true);
366
+
367
+ const timer = setInterval(() => {
368
+ const agents = this.getAgents();
369
+ const entry = agents.get(agentId);
370
+ if (!entry || entry.state.smithStatus !== 'active') return;
371
+ // Skip if agent is busy
372
+ if (entry.state.taskStatus === 'running') return;
373
+
374
+ this.runCheck(agentId, config, false);
375
+ }, interval);
376
+
377
+ timer.unref();
378
+ this.timers.set(agentId, timer);
379
+ }
380
+
381
+ /** Stop watch for a specific agent */
382
+ stopWatch(agentId: string): void {
383
+ const timer = this.timers.get(agentId);
384
+ if (timer) {
385
+ clearInterval(timer);
386
+ this.timers.delete(agentId);
387
+ }
388
+ }
389
+
390
+ /** Run a single check cycle */
391
+ private runCheck(agentId: string, config: WorkspaceAgentConfig, initialRun: boolean): void {
392
+ const now = Date.now();
393
+ const prev = this.snapshots.get(agentId) || { lastCheckTime: now };
394
+ const allChanges: WatchChange[] = [];
395
+ const newSnapshot: WatchSnapshot = { lastCheckTime: now };
396
+
397
+ for (const target of config.watch!.targets) {
398
+ switch (target.type) {
399
+ case 'directory': {
400
+ const { changes } = detectDirectoryChanges(this.projectPath, target, prev.lastCheckTime);
401
+ if (changes) allChanges.push(changes);
402
+ break;
403
+ }
404
+ case 'git': {
405
+ const { changes, gitHash } = detectGitChanges(this.projectPath, prev.gitHash);
406
+ newSnapshot.gitHash = gitHash;
407
+ if (changes) allChanges.push(changes);
408
+ break;
409
+ }
410
+ case 'agent_output': {
411
+ const agents = this.getAgents();
412
+ const targetAgent = target.path ? agents.get(target.path) : null;
413
+ if (targetAgent) {
414
+ for (const outputPath of targetAgent.config.outputs) {
415
+ const { changes } = detectDirectoryChanges(this.projectPath, { ...target, path: outputPath }, prev.lastCheckTime);
416
+ if (changes) allChanges.push({ ...changes, targetType: 'agent_output', description: `${targetAgent.config.label} output: ${changes.description}` });
417
+ }
418
+ }
419
+ break;
420
+ }
421
+ case 'agent_log': {
422
+ if (target.path) {
423
+ const agentLabel = this.getAgents().get(target.path)?.config.label || target.path;
424
+ const { changes, lineCount } = detectAgentLogChanges(this.workspaceId, target.path, target.pattern, prev.logLineCount || 0, target.contextChars || 500);
425
+ newSnapshot.logLineCount = lineCount;
426
+ if (changes) allChanges.push({ ...changes, description: `${agentLabel} log: ${changes.description}` });
427
+ }
428
+ break;
429
+ }
430
+ case 'session': {
431
+ // Resolve session ID: explicit (cmd field) > agent's cliSessionId > latest file
432
+ let sessionId: string | undefined;
433
+ if (target.cmd) {
434
+ // Explicit session ID selected by user
435
+ sessionId = target.cmd;
436
+ } else if (target.path) {
437
+ // Agent selected — use its current cliSessionId
438
+ const agents = this.getAgents();
439
+ const targetAgent = agents.get(target.path);
440
+ if (targetAgent) {
441
+ sessionId = (targetAgent.state as any).cliSessionId;
442
+ if (!sessionId) {
443
+ console.log(`[watch] Agent ${targetAgent.config.label} has no cliSessionId — falling back to latest session file`);
444
+ }
445
+ }
446
+ }
447
+ const { changes, lineCount: newFileSize } = detectSessionChanges(this.projectPath, target.pattern, prev.sessionFileSize || 0, target.contextChars || 500, sessionId);
448
+ newSnapshot.sessionFileSize = newFileSize;
449
+ if (changes) allChanges.push(changes);
450
+ break;
451
+ }
452
+ case 'command': {
453
+ const { changes, commandOutput } = detectCommandChanges(this.projectPath, target, prev.commandOutput);
454
+ newSnapshot.commandOutput = commandOutput;
455
+ if (changes) allChanges.push(changes);
456
+ break;
457
+ }
458
+ case 'agent_status': {
459
+ // Monitor another agent's task status (running → done/failed)
460
+ const targetAgentId = target.path; // path = agent ID to monitor
461
+ if (targetAgentId) {
462
+ const agents = this.getAgents();
463
+ const targetEntry = agents.get(targetAgentId);
464
+ if (targetEntry) {
465
+ const currentStatus = targetEntry.state.taskStatus;
466
+ const prevStatus = prev.agentStatus;
467
+ newSnapshot.agentStatus = currentStatus;
468
+ if (prevStatus && prevStatus !== currentStatus) {
469
+ const label = targetEntry.config.label;
470
+ // Match pattern if specified (e.g., "done" or "failed")
471
+ const pattern = target.pattern;
472
+ if (!pattern || currentStatus.match(new RegExp(pattern, 'i'))) {
473
+ allChanges.push({ targetType: 'agent_status', description: `Agent ${label} status: ${prevStatus} → ${currentStatus}`, files: [] });
474
+ }
475
+ }
476
+ }
477
+ }
478
+ break;
479
+ }
480
+ }
481
+ }
482
+
483
+ this.snapshots.set(agentId, newSnapshot);
484
+
485
+ if (initialRun) {
486
+ console.log(`[watch] ${config.label}: baseline set (checkTime=${new Date(now).toLocaleTimeString()})`);
487
+ return;
488
+ }
489
+
490
+ if (allChanges.length === 0) {
491
+ console.log(`[watch] ${config.label}: checked — no changes`);
492
+ // Heartbeat: only log to console, don't write to logs.jsonl or agent history
493
+ // (prevents disk/memory bloat from frequent no-change checks)
494
+ return;
495
+ }
496
+
497
+ // Build report
498
+ const summary = allChanges.map(c =>
499
+ `[${c.targetType}] ${c.description}${c.files.length ? '\n ' + c.files.join('\n ') : ''}`
500
+ ).join('\n');
501
+
502
+ console.log(`[watch] ${config.label}: detected ${allChanges.length} change(s)`);
503
+
504
+ // Get debounce from first target that has it, or default 10s
505
+ const debounceMs = (config.watch!.targets.find(t => t.debounce !== undefined)?.debounce ?? 10) * 1000;
506
+
507
+ if (debounceMs > 0) {
508
+ // Accumulate changes, reset timer each time
509
+ const existing = this.pendingAlert.get(agentId);
510
+ const merged = existing ? [...existing.changes, ...allChanges] : allChanges;
511
+ const mergedSummary = merged.map(c =>
512
+ `[${c.targetType}] ${c.description}${c.files.length ? '\n ' + c.files.join('\n ') : ''}`
513
+ ).join('\n');
514
+ this.pendingAlert.set(agentId, { changes: merged, summary: mergedSummary, timestamp: Date.now() });
515
+
516
+ // Clear previous debounce timer, set new one
517
+ const prevTimer = this.debounceTimers.get(agentId);
518
+ if (prevTimer) clearTimeout(prevTimer);
519
+
520
+ this.debounceTimers.set(agentId, setTimeout(() => {
521
+ const pending = this.pendingAlert.get(agentId);
522
+ if (!pending) return;
523
+ this.pendingAlert.delete(agentId);
524
+ this.debounceTimers.delete(agentId);
525
+ this.emitAlert(agentId, config, pending.changes, pending.summary);
526
+ }, debounceMs));
527
+
528
+ console.log(`[watch] ${config.label}: debouncing ${debounceMs / 1000}s...`);
529
+ } else {
530
+ this.emitAlert(agentId, config, allChanges, summary);
531
+ }
532
+ }
533
+
534
+ private emitAlert(agentId: string, config: WorkspaceAgentConfig, allChanges: WatchChange[], summary: string): void {
535
+ const entry = { type: 'system' as const, subtype: 'watch_detected', content: `🔍 Watch detected changes:\n${summary}`, timestamp: new Date().toISOString() };
536
+ appendAgentLog(this.workspaceId, agentId, entry).catch(() => {});
537
+
538
+ this.emit('watch_alert', {
539
+ type: 'watch_alert',
540
+ agentId,
541
+ entry,
542
+ changes: allChanges,
543
+ summary,
544
+ timestamp: Date.now(),
545
+ });
546
+ }
547
+
548
+ /** Manual trigger: run check now and return results */
549
+ triggerCheck(agentId: string): { changes: WatchChange[] } | null {
550
+ const agents = this.getAgents();
551
+ const entry = agents.get(agentId);
552
+ if (!entry?.config.watch?.enabled) return null;
553
+
554
+ const prev = this.snapshots.get(agentId) || { files: {} };
555
+ const allChanges: WatchChange[] = [];
556
+ // Reuse runCheck logic but capture results
557
+ this.runCheck(agentId, entry.config, false);
558
+ return { changes: allChanges };
559
+ }
560
+ }