@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.
- package/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
- package/RELEASE_NOTES.md +10 -29
- package/app/api/terminal-bell/route.ts +6 -2
- package/app/api/terminal-cwd/route.ts +7 -4
- package/components/CodeViewer.tsx +3 -31
- package/components/Dashboard.tsx +34 -20
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- 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
|
+
}
|