@evermore.work/server 2026.509.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/builtin-adapter-types.d.ts +5 -0
- package/dist/adapters/builtin-adapter-types.d.ts.map +1 -0
- package/dist/adapters/builtin-adapter-types.js +17 -0
- package/dist/adapters/builtin-adapter-types.js.map +1 -0
- package/dist/adapters/codex-models.d.ts +5 -0
- package/dist/adapters/codex-models.d.ts.map +1 -0
- package/dist/adapters/codex-models.js +105 -0
- package/dist/adapters/codex-models.js.map +1 -0
- package/dist/adapters/cursor-models.d.ts +13 -0
- package/dist/adapters/cursor-models.d.ts.map +1 -0
- package/dist/adapters/cursor-models.js +148 -0
- package/dist/adapters/cursor-models.js.map +1 -0
- package/dist/adapters/http/execute.d.ts +3 -0
- package/dist/adapters/http/execute.d.ts.map +1 -0
- package/dist/adapters/http/execute.js +51 -0
- package/dist/adapters/http/execute.js.map +1 -0
- package/dist/adapters/http/execute.test.d.ts +2 -0
- package/dist/adapters/http/execute.test.d.ts.map +1 -0
- package/dist/adapters/http/execute.test.js +40 -0
- package/dist/adapters/http/execute.test.js.map +1 -0
- package/dist/adapters/http/index.d.ts +3 -0
- package/dist/adapters/http/index.d.ts.map +1 -0
- package/dist/adapters/http/index.js +20 -0
- package/dist/adapters/http/index.js.map +1 -0
- package/dist/adapters/http/test.d.ts +3 -0
- package/dist/adapters/http/test.d.ts.map +1 -0
- package/dist/adapters/http/test.js +106 -0
- package/dist/adapters/http/test.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/plugin-loader.d.ts +28 -0
- package/dist/adapters/plugin-loader.d.ts.map +1 -0
- package/dist/adapters/plugin-loader.js +196 -0
- package/dist/adapters/plugin-loader.js.map +1 -0
- package/dist/adapters/process/execute.d.ts +3 -0
- package/dist/adapters/process/execute.d.ts.map +1 -0
- package/dist/adapters/process/execute.js +70 -0
- package/dist/adapters/process/execute.js.map +1 -0
- package/dist/adapters/process/index.d.ts +3 -0
- package/dist/adapters/process/index.d.ts.map +1 -0
- package/dist/adapters/process/index.js +23 -0
- package/dist/adapters/process/index.js.map +1 -0
- package/dist/adapters/process/test.d.ts +3 -0
- package/dist/adapters/process/test.d.ts.map +1 -0
- package/dist/adapters/process/test.js +77 -0
- package/dist/adapters/process/test.js.map +1 -0
- package/dist/adapters/registry.d.ts +69 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +566 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/adapters/utils.d.ts +43 -0
- package/dist/adapters/utils.d.ts.map +1 -0
- package/dist/adapters/utils.js +52 -0
- package/dist/adapters/utils.js.map +1 -0
- package/dist/agent-auth-jwt.d.ts +14 -0
- package/dist/agent-auth-jwt.d.ts.map +1 -0
- package/dist/agent-auth-jwt.js +117 -0
- package/dist/agent-auth-jwt.js.map +1 -0
- package/dist/app.d.ts +43 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +373 -0
- package/dist/app.js.map +1 -0
- package/dist/attachment-types.d.ts +23 -0
- package/dist/attachment-types.d.ts.map +1 -0
- package/dist/attachment-types.js +91 -0
- package/dist/attachment-types.js.map +1 -0
- package/dist/auth/better-auth.d.ts +33 -0
- package/dist/auth/better-auth.d.ts.map +1 -0
- package/dist/auth/better-auth.js +133 -0
- package/dist/auth/better-auth.js.map +1 -0
- package/dist/board-claim.d.ts +23 -0
- package/dist/board-claim.d.ts.map +1 -0
- package/dist/board-claim.js +115 -0
- package/dist/board-claim.js.map +1 -0
- package/dist/config-file.d.ts +3 -0
- package/dist/config-file.d.ts.map +1 -0
- package/dist/config-file.js +16 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +226 -0
- package/dist/config.js.map +1 -0
- package/dist/dev-runner-worktree.d.ts +15 -0
- package/dist/dev-runner-worktree.d.ts.map +1 -0
- package/dist/dev-runner-worktree.js +68 -0
- package/dist/dev-runner-worktree.js.map +1 -0
- package/dist/dev-server-status.d.ts +27 -0
- package/dist/dev-server-status.d.ts.map +1 -0
- package/dist/dev-server-status.js +74 -0
- package/dist/dev-server-status.js.map +1 -0
- package/dist/dev-watch-ignore.d.ts +2 -0
- package/dist/dev-watch-ignore.d.ts.map +1 -0
- package/dist/dev-watch-ignore.js +36 -0
- package/dist/dev-watch-ignore.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/home-paths.d.ts +17 -0
- package/dist/home-paths.d.ts.map +1 -0
- package/dist/home-paths.js +75 -0
- package/dist/home-paths.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +753 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/join-request-dedupe.d.ts +11 -0
- package/dist/lib/join-request-dedupe.d.ts.map +1 -0
- package/dist/lib/join-request-dedupe.js +49 -0
- package/dist/lib/join-request-dedupe.js.map +1 -0
- package/dist/log-redaction.d.ts +11 -0
- package/dist/log-redaction.d.ts.map +1 -0
- package/dist/log-redaction.js +122 -0
- package/dist/log-redaction.js.map +1 -0
- package/dist/middleware/auth.d.ts +12 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +302 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/board-mutation-guard.d.ts +3 -0
- package/dist/middleware/board-mutation-guard.d.ts.map +1 -0
- package/dist/middleware/board-mutation-guard.js +67 -0
- package/dist/middleware/board-mutation-guard.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +17 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +45 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/http-log-policy.d.ts +2 -0
- package/dist/middleware/http-log-policy.d.ts.map +1 -0
- package/dist/middleware/http-log-policy.js +52 -0
- package/dist/middleware/http-log-policy.js.map +1 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +4 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +4 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +92 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/private-hostname-guard.d.ts +11 -0
- package/dist/middleware/private-hostname-guard.d.ts.map +1 -0
- package/dist/middleware/private-hostname-guard.js +78 -0
- package/dist/middleware/private-hostname-guard.js.map +1 -0
- package/dist/middleware/validate.d.ts +4 -0
- package/dist/middleware/validate.d.ts.map +1 -0
- package/dist/middleware/validate.js +7 -0
- package/dist/middleware/validate.js.map +1 -0
- package/dist/onboarding-assets/ceo/AGENTS.md +59 -0
- package/dist/onboarding-assets/ceo/HEARTBEAT.md +85 -0
- package/dist/onboarding-assets/ceo/SOUL.md +33 -0
- package/dist/onboarding-assets/ceo/TOOLS.md +3 -0
- package/dist/onboarding-assets/default/AGENTS.md +17 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +31 -0
- package/dist/paths.js.map +1 -0
- package/dist/realtime/live-events-ws.d.ts +28 -0
- package/dist/realtime/live-events-ws.d.ts.map +1 -0
- package/dist/realtime/live-events-ws.js +187 -0
- package/dist/realtime/live-events-ws.js.map +1 -0
- package/dist/redaction.d.ts +5 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +98 -0
- package/dist/redaction.js.map +1 -0
- package/dist/routes/access.d.ts +82 -0
- package/dist/routes/access.d.ts.map +1 -0
- package/dist/routes/access.js +3411 -0
- package/dist/routes/access.js.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +90 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/adapters.d.ts +16 -0
- package/dist/routes/adapters.d.ts.map +1 -0
- package/dist/routes/adapters.js +527 -0
- package/dist/routes/adapters.js.map +1 -0
- package/dist/routes/agents.d.ts +6 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +2753 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/approvals.d.ts +6 -0
- package/dist/routes/approvals.d.ts.map +1 -0
- package/dist/routes/approvals.js +300 -0
- package/dist/routes/approvals.js.map +1 -0
- package/dist/routes/assets.d.ts +4 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +309 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/auth.d.ts +3 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +82 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/authz.d.ts +19 -0
- package/dist/routes/authz.d.ts.map +1 -0
- package/dist/routes/authz.js +75 -0
- package/dist/routes/authz.js.map +1 -0
- package/dist/routes/companies.d.ts +4 -0
- package/dist/routes/companies.d.ts.map +1 -0
- package/dist/routes/companies.js +359 -0
- package/dist/routes/companies.js.map +1 -0
- package/dist/routes/company-skills.d.ts +3 -0
- package/dist/routes/company-skills.d.ts.map +1 -0
- package/dist/routes/company-skills.js +258 -0
- package/dist/routes/company-skills.js.map +1 -0
- package/dist/routes/costs.d.ts +11 -0
- package/dist/routes/costs.d.ts.map +1 -0
- package/dist/routes/costs.js +285 -0
- package/dist/routes/costs.js.map +1 -0
- package/dist/routes/dashboard.d.ts +3 -0
- package/dist/routes/dashboard.d.ts.map +1 -0
- package/dist/routes/dashboard.js +15 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/environment-selection.d.ts +13 -0
- package/dist/routes/environment-selection.d.ts.map +1 -0
- package/dist/routes/environment-selection.js +30 -0
- package/dist/routes/environment-selection.js.map +1 -0
- package/dist/routes/environments.d.ts +6 -0
- package/dist/routes/environments.d.ts.map +1 -0
- package/dist/routes/environments.js +401 -0
- package/dist/routes/environments.js.map +1 -0
- package/dist/routes/execution-workspaces.d.ts +3 -0
- package/dist/routes/execution-workspaces.d.ts.map +1 -0
- package/dist/routes/execution-workspaces.js +536 -0
- package/dist/routes/execution-workspaces.js.map +1 -0
- package/dist/routes/goals.d.ts +3 -0
- package/dist/routes/goals.d.ts.map +1 -0
- package/dist/routes/goals.js +101 -0
- package/dist/routes/goals.js.map +1 -0
- package/dist/routes/health.d.ts +9 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +114 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/inbox-dismissals.d.ts +3 -0
- package/dist/routes/inbox-dismissals.d.ts.map +1 -0
- package/dist/routes/inbox-dismissals.js +58 -0
- package/dist/routes/inbox-dismissals.js.map +1 -0
- package/dist/routes/index.d.ts +22 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +22 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/instance-database-backups.d.ts +15 -0
- package/dist/routes/instance-database-backups.d.ts.map +1 -0
- package/dist/routes/instance-database-backups.js +12 -0
- package/dist/routes/instance-database-backups.js.map +1 -0
- package/dist/routes/instance-settings.d.ts +3 -0
- package/dist/routes/instance-settings.d.ts.map +1 -0
- package/dist/routes/instance-settings.js +110 -0
- package/dist/routes/instance-settings.js.map +1 -0
- package/dist/routes/issue-tree-control.d.ts +3 -0
- package/dist/routes/issue-tree-control.d.ts.map +1 -0
- package/dist/routes/issue-tree-control.js +363 -0
- package/dist/routes/issue-tree-control.js.map +1 -0
- package/dist/routes/issues-checkout-wakeup.d.ts +9 -0
- package/dist/routes/issues-checkout-wakeup.d.ts.map +1 -0
- package/dist/routes/issues-checkout-wakeup.js +12 -0
- package/dist/routes/issues-checkout-wakeup.js.map +1 -0
- package/dist/routes/issues.d.ts +23 -0
- package/dist/routes/issues.d.ts.map +1 -0
- package/dist/routes/issues.js +3886 -0
- package/dist/routes/issues.js.map +1 -0
- package/dist/routes/llms.d.ts +3 -0
- package/dist/routes/llms.d.ts.map +1 -0
- package/dist/routes/llms.js +80 -0
- package/dist/routes/llms.js.map +1 -0
- package/dist/routes/org-chart-svg.d.ts +25 -0
- package/dist/routes/org-chart-svg.d.ts.map +1 -0
- package/dist/routes/org-chart-svg.js +656 -0
- package/dist/routes/org-chart-svg.js.map +1 -0
- package/dist/routes/plugin-ui-static.d.ts +69 -0
- package/dist/routes/plugin-ui-static.d.ts.map +1 -0
- package/dist/routes/plugin-ui-static.js +411 -0
- package/dist/routes/plugin-ui-static.js.map +1 -0
- package/dist/routes/plugins.d.ts +121 -0
- package/dist/routes/plugins.d.ts.map +1 -0
- package/dist/routes/plugins.js +2192 -0
- package/dist/routes/plugins.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +566 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/routines.d.ts +6 -0
- package/dist/routes/routines.d.ts.map +1 -0
- package/dist/routes/routines.js +417 -0
- package/dist/routes/routines.js.map +1 -0
- package/dist/routes/secrets.d.ts +3 -0
- package/dist/routes/secrets.d.ts.map +1 -0
- package/dist/routes/secrets.js +128 -0
- package/dist/routes/secrets.js.map +1 -0
- package/dist/routes/sidebar-badges.d.ts +3 -0
- package/dist/routes/sidebar-badges.d.ts.map +1 -0
- package/dist/routes/sidebar-badges.js +68 -0
- package/dist/routes/sidebar-badges.js.map +1 -0
- package/dist/routes/sidebar-preferences.d.ts +3 -0
- package/dist/routes/sidebar-preferences.d.ts.map +1 -0
- package/dist/routes/sidebar-preferences.js +63 -0
- package/dist/routes/sidebar-preferences.js.map +1 -0
- package/dist/routes/user-profiles.d.ts +3 -0
- package/dist/routes/user-profiles.d.ts.map +1 -0
- package/dist/routes/user-profiles.js +337 -0
- package/dist/routes/user-profiles.js.map +1 -0
- package/dist/routes/workspace-command-authz.d.ts +14 -0
- package/dist/routes/workspace-command-authz.d.ts.map +1 -0
- package/dist/routes/workspace-command-authz.js +83 -0
- package/dist/routes/workspace-command-authz.js.map +1 -0
- package/dist/routes/workspace-runtime-service-authz.d.ts +12 -0
- package/dist/routes/workspace-runtime-service-authz.d.ts.map +1 -0
- package/dist/routes/workspace-runtime-service-authz.js +96 -0
- package/dist/routes/workspace-runtime-service-authz.js.map +1 -0
- package/dist/runtime-api.d.ts +19 -0
- package/dist/runtime-api.d.ts.map +1 -0
- package/dist/runtime-api.js +137 -0
- package/dist/runtime-api.js.map +1 -0
- package/dist/secrets/external-stub-providers.d.ts +5 -0
- package/dist/secrets/external-stub-providers.d.ts.map +1 -0
- package/dist/secrets/external-stub-providers.js +21 -0
- package/dist/secrets/external-stub-providers.js.map +1 -0
- package/dist/secrets/local-encrypted-provider.d.ts +3 -0
- package/dist/secrets/local-encrypted-provider.d.ts.map +1 -0
- package/dist/secrets/local-encrypted-provider.js +116 -0
- package/dist/secrets/local-encrypted-provider.js.map +1 -0
- package/dist/secrets/provider-registry.d.ts +5 -0
- package/dist/secrets/provider-registry.d.ts.map +1 -0
- package/dist/secrets/provider-registry.js +20 -0
- package/dist/secrets/provider-registry.js.map +1 -0
- package/dist/secrets/types.d.ts +21 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +2 -0
- package/dist/secrets/types.js.map +1 -0
- package/dist/services/access.d.ts +171 -0
- package/dist/services/access.d.ts.map +1 -0
- package/dist/services/access.js +522 -0
- package/dist/services/access.js.map +1 -0
- package/dist/services/activity-log.d.ts +19 -0
- package/dist/services/activity-log.d.ts.map +1 -0
- package/dist/services/activity-log.js +99 -0
- package/dist/services/activity-log.js.map +1 -0
- package/dist/services/activity.d.ts +462 -0
- package/dist/services/activity.d.ts.map +1 -0
- package/dist/services/activity.js +443 -0
- package/dist/services/activity.js.map +1 -0
- package/dist/services/adapter-plugin-store.d.ts +36 -0
- package/dist/services/adapter-plugin-store.d.ts.map +1 -0
- package/dist/services/adapter-plugin-store.js +154 -0
- package/dist/services/adapter-plugin-store.js.map +1 -0
- package/dist/services/agent-instructions.d.ts +91 -0
- package/dist/services/agent-instructions.d.ts.map +1 -0
- package/dist/services/agent-instructions.js +580 -0
- package/dist/services/agent-instructions.js.map +1 -0
- package/dist/services/agent-permissions.d.ts +6 -0
- package/dist/services/agent-permissions.d.ts.map +1 -0
- package/dist/services/agent-permissions.js +18 -0
- package/dist/services/agent-permissions.js.map +1 -0
- package/dist/services/agent-start-lock.d.ts +2 -0
- package/dist/services/agent-start-lock.d.ts.map +1 -0
- package/dist/services/agent-start-lock.js +43 -0
- package/dist/services/agent-start-lock.js.map +1 -0
- package/dist/services/agents.d.ts +2253 -0
- package/dist/services/agents.d.ts.map +1 -0
- package/dist/services/agents.js +609 -0
- package/dist/services/agents.js.map +1 -0
- package/dist/services/approvals.d.ts +546 -0
- package/dist/services/approvals.d.ts.map +1 -0
- package/dist/services/approvals.js +212 -0
- package/dist/services/approvals.js.map +1 -0
- package/dist/services/assets.d.ts +33 -0
- package/dist/services/assets.d.ts.map +1 -0
- package/dist/services/assets.js +17 -0
- package/dist/services/assets.js.map +1 -0
- package/dist/services/board-auth.d.ts +239 -0
- package/dist/services/board-auth.d.ts.map +1 -0
- package/dist/services/board-auth.js +300 -0
- package/dist/services/board-auth.js.map +1 -0
- package/dist/services/budgets.d.ts +38 -0
- package/dist/services/budgets.d.ts.map +1 -0
- package/dist/services/budgets.js +784 -0
- package/dist/services/budgets.js.map +1 -0
- package/dist/services/companies.d.ts +154 -0
- package/dist/services/companies.d.ts.map +1 -0
- package/dist/services/companies.js +267 -0
- package/dist/services/companies.js.map +1 -0
- package/dist/services/company-export-readme.d.ts +17 -0
- package/dist/services/company-export-readme.d.ts.map +1 -0
- package/dist/services/company-export-readme.js +148 -0
- package/dist/services/company-export-readme.js.map +1 -0
- package/dist/services/company-member-roles.d.ts +9 -0
- package/dist/services/company-member-roles.d.ts.map +1 -0
- package/dist/services/company-member-roles.js +46 -0
- package/dist/services/company-member-roles.js.map +1 -0
- package/dist/services/company-portability.d.ts +24 -0
- package/dist/services/company-portability.d.ts.map +1 -0
- package/dist/services/company-portability.js +4076 -0
- package/dist/services/company-portability.js.map +1 -0
- package/dist/services/company-search-rate-limit.d.ts +22 -0
- package/dist/services/company-search-rate-limit.d.ts.map +1 -0
- package/dist/services/company-search-rate-limit.js +38 -0
- package/dist/services/company-search-rate-limit.js.map +1 -0
- package/dist/services/company-search.d.ts +8 -0
- package/dist/services/company-search.d.ts.map +1 -0
- package/dist/services/company-search.js +626 -0
- package/dist/services/company-search.js.map +1 -0
- package/dist/services/company-skills.d.ts +77 -0
- package/dist/services/company-skills.d.ts.map +1 -0
- package/dist/services/company-skills.js +2120 -0
- package/dist/services/company-skills.js.map +1 -0
- package/dist/services/costs.d.ts +127 -0
- package/dist/services/costs.d.ts.map +1 -0
- package/dist/services/costs.js +409 -0
- package/dist/services/costs.js.map +1 -0
- package/dist/services/cron.d.ts +80 -0
- package/dist/services/cron.d.ts.map +1 -0
- package/dist/services/cron.js +300 -0
- package/dist/services/cron.js.map +1 -0
- package/dist/services/dashboard.d.ts +34 -0
- package/dist/services/dashboard.d.ts.map +1 -0
- package/dist/services/dashboard.js +142 -0
- package/dist/services/dashboard.js.map +1 -0
- package/dist/services/default-agent-instructions.d.ts +9 -0
- package/dist/services/default-agent-instructions.d.ts.map +1 -0
- package/dist/services/default-agent-instructions.js +20 -0
- package/dist/services/default-agent-instructions.js.map +1 -0
- package/dist/services/documents.d.ts +199 -0
- package/dist/services/documents.d.ts.map +1 -0
- package/dist/services/documents.js +411 -0
- package/dist/services/documents.js.map +1 -0
- package/dist/services/environment-config.d.ts +43 -0
- package/dist/services/environment-config.d.ts.map +1 -0
- package/dist/services/environment-config.js +388 -0
- package/dist/services/environment-config.js.map +1 -0
- package/dist/services/environment-execution-target.d.ts +21 -0
- package/dist/services/environment-execution-target.d.ts.map +1 -0
- package/dist/services/environment-execution-target.js +119 -0
- package/dist/services/environment-execution-target.js.map +1 -0
- package/dist/services/environment-probe.d.ts +9 -0
- package/dist/services/environment-probe.d.ts.map +1 -0
- package/dist/services/environment-probe.js +106 -0
- package/dist/services/environment-probe.js.map +1 -0
- package/dist/services/environment-run-orchestrator.d.ts +124 -0
- package/dist/services/environment-run-orchestrator.d.ts.map +1 -0
- package/dist/services/environment-run-orchestrator.js +392 -0
- package/dist/services/environment-run-orchestrator.js.map +1 -0
- package/dist/services/environment-runtime.d.ts +90 -0
- package/dist/services/environment-runtime.d.ts.map +1 -0
- package/dist/services/environment-runtime.js +934 -0
- package/dist/services/environment-runtime.js.map +1 -0
- package/dist/services/environments.d.ts +36 -0
- package/dist/services/environments.d.ts.map +1 -0
- package/dist/services/environments.js +260 -0
- package/dist/services/environments.js.map +1 -0
- package/dist/services/execution-workspace-policy.d.ts +30 -0
- package/dist/services/execution-workspace-policy.d.ts.map +1 -0
- package/dist/services/execution-workspace-policy.js +195 -0
- package/dist/services/execution-workspace-policy.js.map +1 -0
- package/dist/services/execution-workspaces.d.ts +30 -0
- package/dist/services/execution-workspaces.d.ts.map +1 -0
- package/dist/services/execution-workspaces.js +635 -0
- package/dist/services/execution-workspaces.js.map +1 -0
- package/dist/services/feedback-redaction.d.ts +23 -0
- package/dist/services/feedback-redaction.d.ts.map +1 -0
- package/dist/services/feedback-redaction.js +150 -0
- package/dist/services/feedback-redaction.js.map +1 -0
- package/dist/services/feedback-share-client.d.ts +9 -0
- package/dist/services/feedback-share-client.d.ts.map +1 -0
- package/dist/services/feedback-share-client.js +46 -0
- package/dist/services/feedback-share-client.js.map +1 -0
- package/dist/services/feedback.d.ts +93 -0
- package/dist/services/feedback.d.ts.map +1 -0
- package/dist/services/feedback.js +1717 -0
- package/dist/services/feedback.js.map +1 -0
- package/dist/services/finance.d.ts +93 -0
- package/dist/services/finance.d.ts.map +1 -0
- package/dist/services/finance.js +120 -0
- package/dist/services/finance.js.map +1 -0
- package/dist/services/github-fetch.d.ts +4 -0
- package/dist/services/github-fetch.d.ts.map +1 -0
- package/dist/services/github-fetch.js +23 -0
- package/dist/services/github-fetch.js.map +1 -0
- package/dist/services/goals.d.ts +433 -0
- package/dist/services/goals.d.ts.map +1 -0
- package/dist/services/goals.js +54 -0
- package/dist/services/goals.js.map +1 -0
- package/dist/services/heartbeat-run-summary.d.ts +7 -0
- package/dist/services/heartbeat-run-summary.d.ts.map +1 -0
- package/dist/services/heartbeat-run-summary.js +84 -0
- package/dist/services/heartbeat-run-summary.js.map +1 -0
- package/dist/services/heartbeat-stop-metadata.d.ts +28 -0
- package/dist/services/heartbeat-stop-metadata.d.ts.map +1 -0
- package/dist/services/heartbeat-stop-metadata.js +86 -0
- package/dist/services/heartbeat-stop-metadata.js.map +1 -0
- package/dist/services/heartbeat-stop-metadata.test.d.ts +2 -0
- package/dist/services/heartbeat-stop-metadata.test.d.ts.map +1 -0
- package/dist/services/heartbeat-stop-metadata.test.js +93 -0
- package/dist/services/heartbeat-stop-metadata.test.js.map +1 -0
- package/dist/services/heartbeat.d.ts +1484 -0
- package/dist/services/heartbeat.d.ts.map +1 -0
- package/dist/services/heartbeat.js +7557 -0
- package/dist/services/heartbeat.js.map +1 -0
- package/dist/services/hire-hook.d.ts +14 -0
- package/dist/services/hire-hook.d.ts.map +1 -0
- package/dist/services/hire-hook.js +85 -0
- package/dist/services/hire-hook.js.map +1 -0
- package/dist/services/inbox-dismissals.d.ts +22 -0
- package/dist/services/inbox-dismissals.d.ts.map +1 -0
- package/dist/services/inbox-dismissals.js +33 -0
- package/dist/services/inbox-dismissals.js.map +1 -0
- package/dist/services/index.d.ts +44 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +44 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/instance-settings.d.ts +11 -0
- package/dist/services/instance-settings.d.ts.map +1 -0
- package/dist/services/instance-settings.js +138 -0
- package/dist/services/instance-settings.js.map +1 -0
- package/dist/services/invite-grants.d.ts +15 -0
- package/dist/services/invite-grants.d.ts.map +1 -0
- package/dist/services/invite-grants.js +50 -0
- package/dist/services/invite-grants.js.map +1 -0
- package/dist/services/issue-approvals.d.ts +56 -0
- package/dist/services/issue-approvals.d.ts.map +1 -0
- package/dist/services/issue-approvals.js +153 -0
- package/dist/services/issue-approvals.js.map +1 -0
- package/dist/services/issue-assignment-wakeup.d.ts +29 -0
- package/dist/services/issue-assignment-wakeup.d.ts.map +1 -0
- package/dist/services/issue-assignment-wakeup.js +22 -0
- package/dist/services/issue-assignment-wakeup.js.map +1 -0
- package/dist/services/issue-continuation-summary.d.ts +66 -0
- package/dist/services/issue-continuation-summary.d.ts.map +1 -0
- package/dist/services/issue-continuation-summary.js +212 -0
- package/dist/services/issue-continuation-summary.js.map +1 -0
- package/dist/services/issue-execution-policy.d.ts +93 -0
- package/dist/services/issue-execution-policy.d.ts.map +1 -0
- package/dist/services/issue-execution-policy.js +838 -0
- package/dist/services/issue-execution-policy.js.map +1 -0
- package/dist/services/issue-goal-fallback.d.ts +18 -0
- package/dist/services/issue-goal-fallback.d.ts.map +1 -0
- package/dist/services/issue-goal-fallback.js +33 -0
- package/dist/services/issue-goal-fallback.js.map +1 -0
- package/dist/services/issue-liveness.d.ts +3 -0
- package/dist/services/issue-liveness.d.ts.map +1 -0
- package/dist/services/issue-liveness.js +2 -0
- package/dist/services/issue-liveness.js.map +1 -0
- package/dist/services/issue-references.d.ts +21 -0
- package/dist/services/issue-references.d.ts.map +1 -0
- package/dist/services/issue-references.js +318 -0
- package/dist/services/issue-references.js.map +1 -0
- package/dist/services/issue-thread-interactions.d.ts +76 -0
- package/dist/services/issue-thread-interactions.d.ts.map +1 -0
- package/dist/services/issue-thread-interactions.js +923 -0
- package/dist/services/issue-thread-interactions.js.map +1 -0
- package/dist/services/issue-thread-interactions.test.d.ts +2 -0
- package/dist/services/issue-thread-interactions.test.d.ts.map +1 -0
- package/dist/services/issue-thread-interactions.test.js +195 -0
- package/dist/services/issue-thread-interactions.test.js.map +1 -0
- package/dist/services/issue-tree-control.d.ts +89 -0
- package/dist/services/issue-tree-control.d.ts.map +1 -0
- package/dist/services/issue-tree-control.js +933 -0
- package/dist/services/issue-tree-control.js.map +1 -0
- package/dist/services/issues.d.ts +710 -0
- package/dist/services/issues.d.ts.map +1 -0
- package/dist/services/issues.js +3199 -0
- package/dist/services/issues.js.map +1 -0
- package/dist/services/json-schema-secret-refs.d.ts +5 -0
- package/dist/services/json-schema-secret-refs.d.ts.map +1 -0
- package/dist/services/json-schema-secret-refs.js +67 -0
- package/dist/services/json-schema-secret-refs.js.map +1 -0
- package/dist/services/live-events.d.ts +17 -0
- package/dist/services/live-events.d.ts.map +1 -0
- package/dist/services/live-events.js +33 -0
- package/dist/services/live-events.js.map +1 -0
- package/dist/services/local-service-supervisor.d.ts +56 -0
- package/dist/services/local-service-supervisor.d.ts.map +1 -0
- package/dist/services/local-service-supervisor.js +284 -0
- package/dist/services/local-service-supervisor.js.map +1 -0
- package/dist/services/plugin-capability-validator.d.ts +108 -0
- package/dist/services/plugin-capability-validator.d.ts.map +1 -0
- package/dist/services/plugin-capability-validator.js +313 -0
- package/dist/services/plugin-capability-validator.js.map +1 -0
- package/dist/services/plugin-config-validator.d.ts +26 -0
- package/dist/services/plugin-config-validator.d.ts.map +1 -0
- package/dist/services/plugin-config-validator.js +41 -0
- package/dist/services/plugin-config-validator.js.map +1 -0
- package/dist/services/plugin-database.d.ts +49 -0
- package/dist/services/plugin-database.d.ts.map +1 -0
- package/dist/services/plugin-database.js +440 -0
- package/dist/services/plugin-database.js.map +1 -0
- package/dist/services/plugin-dev-watcher.d.ts +30 -0
- package/dist/services/plugin-dev-watcher.d.ts.map +1 -0
- package/dist/services/plugin-dev-watcher.js +241 -0
- package/dist/services/plugin-dev-watcher.js.map +1 -0
- package/dist/services/plugin-environment-driver.d.ts +124 -0
- package/dist/services/plugin-environment-driver.d.ts.map +1 -0
- package/dist/services/plugin-environment-driver.js +224 -0
- package/dist/services/plugin-environment-driver.js.map +1 -0
- package/dist/services/plugin-event-bus.d.ts +149 -0
- package/dist/services/plugin-event-bus.d.ts.map +1 -0
- package/dist/services/plugin-event-bus.js +258 -0
- package/dist/services/plugin-event-bus.js.map +1 -0
- package/dist/services/plugin-host-service-cleanup.d.ts +14 -0
- package/dist/services/plugin-host-service-cleanup.d.ts.map +1 -0
- package/dist/services/plugin-host-service-cleanup.js +37 -0
- package/dist/services/plugin-host-service-cleanup.js.map +1 -0
- package/dist/services/plugin-host-services.d.ts +17 -0
- package/dist/services/plugin-host-services.d.ts.map +1 -0
- package/dist/services/plugin-host-services.js +1861 -0
- package/dist/services/plugin-host-services.js.map +1 -0
- package/dist/services/plugin-job-coordinator.d.ts +81 -0
- package/dist/services/plugin-job-coordinator.d.ts.map +1 -0
- package/dist/services/plugin-job-coordinator.js +172 -0
- package/dist/services/plugin-job-coordinator.js.map +1 -0
- package/dist/services/plugin-job-scheduler.d.ts +163 -0
- package/dist/services/plugin-job-scheduler.d.ts.map +1 -0
- package/dist/services/plugin-job-scheduler.js +454 -0
- package/dist/services/plugin-job-scheduler.js.map +1 -0
- package/dist/services/plugin-job-store.d.ts +208 -0
- package/dist/services/plugin-job-store.d.ts.map +1 -0
- package/dist/services/plugin-job-store.js +350 -0
- package/dist/services/plugin-job-store.js.map +1 -0
- package/dist/services/plugin-lifecycle.d.ts +203 -0
- package/dist/services/plugin-lifecycle.d.ts.map +1 -0
- package/dist/services/plugin-lifecycle.js +476 -0
- package/dist/services/plugin-lifecycle.js.map +1 -0
- package/dist/services/plugin-loader.d.ts +445 -0
- package/dist/services/plugin-loader.d.ts.map +1 -0
- package/dist/services/plugin-loader.js +1273 -0
- package/dist/services/plugin-loader.js.map +1 -0
- package/dist/services/plugin-local-folders.d.ts +48 -0
- package/dist/services/plugin-local-folders.d.ts.map +1 -0
- package/dist/services/plugin-local-folders.js +461 -0
- package/dist/services/plugin-local-folders.js.map +1 -0
- package/dist/services/plugin-log-retention.d.ts +20 -0
- package/dist/services/plugin-log-retention.d.ts.map +1 -0
- package/dist/services/plugin-log-retention.js +63 -0
- package/dist/services/plugin-log-retention.js.map +1 -0
- package/dist/services/plugin-managed-agents.d.ts +15 -0
- package/dist/services/plugin-managed-agents.d.ts.map +1 -0
- package/dist/services/plugin-managed-agents.js +414 -0
- package/dist/services/plugin-managed-agents.js.map +1 -0
- package/dist/services/plugin-managed-routines.d.ts +41 -0
- package/dist/services/plugin-managed-routines.d.ts.map +1 -0
- package/dist/services/plugin-managed-routines.js +416 -0
- package/dist/services/plugin-managed-routines.js.map +1 -0
- package/dist/services/plugin-manifest-validator.d.ts +79 -0
- package/dist/services/plugin-manifest-validator.d.ts.map +1 -0
- package/dist/services/plugin-manifest-validator.js +84 -0
- package/dist/services/plugin-manifest-validator.js.map +1 -0
- package/dist/services/plugin-registry.d.ts +2550 -0
- package/dist/services/plugin-registry.d.ts.map +1 -0
- package/dist/services/plugin-registry.js +581 -0
- package/dist/services/plugin-registry.js.map +1 -0
- package/dist/services/plugin-runtime-sandbox.d.ts +40 -0
- package/dist/services/plugin-runtime-sandbox.d.ts.map +1 -0
- package/dist/services/plugin-runtime-sandbox.js +154 -0
- package/dist/services/plugin-runtime-sandbox.js.map +1 -0
- package/dist/services/plugin-secrets-handler.d.ts +81 -0
- package/dist/services/plugin-secrets-handler.d.ts.map +1 -0
- package/dist/services/plugin-secrets-handler.js +231 -0
- package/dist/services/plugin-secrets-handler.js.map +1 -0
- package/dist/services/plugin-state-store.d.ts +92 -0
- package/dist/services/plugin-state-store.d.ts.map +1 -0
- package/dist/services/plugin-state-store.js +190 -0
- package/dist/services/plugin-state-store.js.map +1 -0
- package/dist/services/plugin-stream-bus.d.ts +29 -0
- package/dist/services/plugin-stream-bus.d.ts.map +1 -0
- package/dist/services/plugin-stream-bus.js +48 -0
- package/dist/services/plugin-stream-bus.js.map +1 -0
- package/dist/services/plugin-tool-dispatcher.d.ts +180 -0
- package/dist/services/plugin-tool-dispatcher.d.ts.map +1 -0
- package/dist/services/plugin-tool-dispatcher.js +224 -0
- package/dist/services/plugin-tool-dispatcher.js.map +1 -0
- package/dist/services/plugin-tool-registry.d.ts +192 -0
- package/dist/services/plugin-tool-registry.d.ts.map +1 -0
- package/dist/services/plugin-tool-registry.js +224 -0
- package/dist/services/plugin-tool-registry.js.map +1 -0
- package/dist/services/plugin-worker-manager.d.ts +262 -0
- package/dist/services/plugin-worker-manager.d.ts.map +1 -0
- package/dist/services/plugin-worker-manager.js +836 -0
- package/dist/services/plugin-worker-manager.js.map +1 -0
- package/dist/services/productivity-review.d.ts +83 -0
- package/dist/services/productivity-review.d.ts.map +1 -0
- package/dist/services/productivity-review.js +652 -0
- package/dist/services/productivity-review.js.map +1 -0
- package/dist/services/project-workspace-runtime-config.d.ts +4 -0
- package/dist/services/project-workspace-runtime-config.d.ts.map +1 -0
- package/dist/services/project-workspace-runtime-config.js +54 -0
- package/dist/services/project-workspace-runtime-config.js.map +1 -0
- package/dist/services/projects.d.ts +99 -0
- package/dist/services/projects.d.ts.map +1 -0
- package/dist/services/projects.js +879 -0
- package/dist/services/projects.js.map +1 -0
- package/dist/services/quota-windows.d.ts +9 -0
- package/dist/services/quota-windows.d.ts.map +1 -0
- package/dist/services/quota-windows.js +56 -0
- package/dist/services/quota-windows.js.map +1 -0
- package/dist/services/recovery/index.d.ts +10 -0
- package/dist/services/recovery/index.d.ts.map +1 -0
- package/dist/services/recovery/index.js +6 -0
- package/dist/services/recovery/index.js.map +1 -0
- package/dist/services/recovery/issue-graph-liveness.d.ts +85 -0
- package/dist/services/recovery/issue-graph-liveness.d.ts.map +1 -0
- package/dist/services/recovery/issue-graph-liveness.js +343 -0
- package/dist/services/recovery/issue-graph-liveness.js.map +1 -0
- package/dist/services/recovery/model-profile-hint.d.ts +8 -0
- package/dist/services/recovery/model-profile-hint.d.ts.map +1 -0
- package/dist/services/recovery/model-profile-hint.js +11 -0
- package/dist/services/recovery/model-profile-hint.js.map +1 -0
- package/dist/services/recovery/origins.d.ts +36 -0
- package/dist/services/recovery/origins.d.ts.map +1 -0
- package/dist/services/recovery/origins.js +45 -0
- package/dist/services/recovery/origins.js.map +1 -0
- package/dist/services/recovery/pause-hold-guard.d.ts +6 -0
- package/dist/services/recovery/pause-hold-guard.d.ts.map +1 -0
- package/dist/services/recovery/pause-hold-guard.js +6 -0
- package/dist/services/recovery/pause-hold-guard.js.map +1 -0
- package/dist/services/recovery/run-liveness-continuations.d.ts +50 -0
- package/dist/services/recovery/run-liveness-continuations.d.ts.map +1 -0
- package/dist/services/recovery/run-liveness-continuations.js +117 -0
- package/dist/services/recovery/run-liveness-continuations.js.map +1 -0
- package/dist/services/recovery/service.d.ts +195 -0
- package/dist/services/recovery/service.d.ts.map +1 -0
- package/dist/services/recovery/service.js +2210 -0
- package/dist/services/recovery/service.js.map +1 -0
- package/dist/services/recovery/successful-run-handoff.d.ts +87 -0
- package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -0
- package/dist/services/recovery/successful-run-handoff.js +297 -0
- package/dist/services/recovery/successful-run-handoff.js.map +1 -0
- package/dist/services/recovery/successful-run-handoff.test.d.ts +2 -0
- package/dist/services/recovery/successful-run-handoff.test.d.ts.map +1 -0
- package/dist/services/recovery/successful-run-handoff.test.js +267 -0
- package/dist/services/recovery/successful-run-handoff.test.js.map +1 -0
- package/dist/services/routines.d.ts +166 -0
- package/dist/services/routines.d.ts.map +1 -0
- package/dist/services/routines.js +1937 -0
- package/dist/services/routines.js.map +1 -0
- package/dist/services/run-continuations.d.ts +3 -0
- package/dist/services/run-continuations.d.ts.map +1 -0
- package/dist/services/run-continuations.js +2 -0
- package/dist/services/run-continuations.js.map +1 -0
- package/dist/services/run-liveness.d.ts +46 -0
- package/dist/services/run-liveness.d.ts.map +1 -0
- package/dist/services/run-liveness.js +275 -0
- package/dist/services/run-liveness.js.map +1 -0
- package/dist/services/run-log-store.d.ts +34 -0
- package/dist/services/run-log-store.d.ts.map +1 -0
- package/dist/services/run-log-store.js +111 -0
- package/dist/services/run-log-store.js.map +1 -0
- package/dist/services/sandbox-provider-runtime.d.ts +132 -0
- package/dist/services/sandbox-provider-runtime.d.ts.map +1 -0
- package/dist/services/sandbox-provider-runtime.js +216 -0
- package/dist/services/sandbox-provider-runtime.js.map +1 -0
- package/dist/services/secrets.d.ts +515 -0
- package/dist/services/secrets.d.ts.map +1 -0
- package/dist/services/secrets.js +290 -0
- package/dist/services/secrets.js.map +1 -0
- package/dist/services/sidebar-badges.d.ts +14 -0
- package/dist/services/sidebar-badges.d.ts.map +1 -0
- package/dist/services/sidebar-badges.js +48 -0
- package/dist/services/sidebar-badges.js.map +1 -0
- package/dist/services/sidebar-preferences.d.ts +9 -0
- package/dist/services/sidebar-preferences.d.ts.map +1 -0
- package/dist/services/sidebar-preferences.js +82 -0
- package/dist/services/sidebar-preferences.js.map +1 -0
- package/dist/services/work-products.d.ts +14 -0
- package/dist/services/work-products.d.ts.map +1 -0
- package/dist/services/work-products.js +100 -0
- package/dist/services/work-products.js.map +1 -0
- package/dist/services/workspace-operation-log-store.d.ts +33 -0
- package/dist/services/workspace-operation-log-store.d.ts.map +1 -0
- package/dist/services/workspace-operation-log-store.js +110 -0
- package/dist/services/workspace-operation-log-store.js.map +1 -0
- package/dist/services/workspace-operations.d.ts +44 -0
- package/dist/services/workspace-operations.d.ts.map +1 -0
- package/dist/services/workspace-operations.js +211 -0
- package/dist/services/workspace-operations.js.map +1 -0
- package/dist/services/workspace-realization.d.ts +33 -0
- package/dist/services/workspace-realization.d.ts.map +1 -0
- package/dist/services/workspace-realization.js +221 -0
- package/dist/services/workspace-realization.js.map +1 -0
- package/dist/services/workspace-runtime-read-model.d.ts +92 -0
- package/dist/services/workspace-runtime-read-model.d.ts.map +1 -0
- package/dist/services/workspace-runtime-read-model.js +67 -0
- package/dist/services/workspace-runtime-read-model.js.map +1 -0
- package/dist/services/workspace-runtime.d.ts +238 -0
- package/dist/services/workspace-runtime.d.ts.map +1 -0
- package/dist/services/workspace-runtime.js +2388 -0
- package/dist/services/workspace-runtime.js.map +1 -0
- package/dist/startup-banner.d.ts +32 -0
- package/dist/startup-banner.d.ts.map +1 -0
- package/dist/startup-banner.js +118 -0
- package/dist/startup-banner.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +29 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local-disk-provider.d.ts +3 -0
- package/dist/storage/local-disk-provider.d.ts.map +1 -0
- package/dist/storage/local-disk-provider.js +79 -0
- package/dist/storage/local-disk-provider.js.map +1 -0
- package/dist/storage/provider-registry.d.ts +4 -0
- package/dist/storage/provider-registry.d.ts.map +1 -0
- package/dist/storage/provider-registry.js +15 -0
- package/dist/storage/provider-registry.js.map +1 -0
- package/dist/storage/s3-provider.d.ts +11 -0
- package/dist/storage/s3-provider.d.ts.map +1 -0
- package/dist/storage/s3-provider.js +123 -0
- package/dist/storage/s3-provider.js.map +1 -0
- package/dist/storage/service.d.ts +3 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +120 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/types.d.ts +55 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/telemetry.d.ts +6 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +20 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/ui-branding.d.ts +13 -0
- package/dist/ui-branding.d.ts.map +1 -0
- package/dist/ui-branding.js +187 -0
- package/dist/ui-branding.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/vite-html-renderer.d.ts +18 -0
- package/dist/vite-html-renderer.d.ts.map +1 -0
- package/dist/vite-html-renderer.js +61 -0
- package/dist/vite-html-renderer.js.map +1 -0
- package/dist/worktree-config.d.ts +19 -0
- package/dist/worktree-config.d.ts.map +1 -0
- package/dist/worktree-config.js +368 -0
- package/dist/worktree-config.js.map +1 -0
- package/package.json +90 -0
- package/skills/diagnose-why-work-stopped/SKILL.md +161 -0
- package/skills/evermore/SKILL.md +366 -0
- package/skills/evermore/references/api-reference.md +899 -0
- package/skills/evermore/references/company-skills.md +193 -0
- package/skills/evermore/references/issue-workspaces.md +80 -0
- package/skills/evermore/references/routines.md +187 -0
- package/skills/evermore/references/workflows.md +141 -0
- package/skills/evermore-converting-plans-to-tasks/SKILL.md +42 -0
- package/skills/evermore-create-agent/SKILL.md +163 -0
- package/skills/evermore-create-agent/references/agent-instruction-templates.md +123 -0
- package/skills/evermore-create-agent/references/agents/coder.md +64 -0
- package/skills/evermore-create-agent/references/agents/qa.md +88 -0
- package/skills/evermore-create-agent/references/agents/securityengineer.md +135 -0
- package/skills/evermore-create-agent/references/agents/uxdesigner.md +115 -0
- package/skills/evermore-create-agent/references/api-reference.md +110 -0
- package/skills/evermore-create-agent/references/baseline-role-guide.md +168 -0
- package/skills/evermore-create-agent/references/draft-review-checklist.md +95 -0
- package/skills/evermore-create-plugin/SKILL.md +101 -0
- package/skills/evermore-dev/SKILL.md +267 -0
- package/skills/para-memory-files/SKILL.md +104 -0
- package/skills/para-memory-files/references/schemas.md +35 -0
- package/skills/terminal-bench-loop/SKILL.md +236 -0
- package/ui-dist/android-chrome-192x192.png +0 -0
- package/ui-dist/android-chrome-512x512.png +0 -0
- package/ui-dist/apple-touch-icon.png +0 -0
- package/ui-dist/assets/_basePickBy-Ds9oHp1_.js +1 -0
- package/ui-dist/assets/_baseUniq-CHYwQyJ_.js +1 -0
- package/ui-dist/assets/apl-B4CMkyY2.js +1 -0
- package/ui-dist/assets/arc-CiUKtBzk.js +1 -0
- package/ui-dist/assets/architectureDiagram-VXUJARFQ-CAW2b6pT.js +36 -0
- package/ui-dist/assets/asciiarmor-Df11BRmG.js +1 -0
- package/ui-dist/assets/asn1-EdZsLKOL.js +1 -0
- package/ui-dist/assets/asterisk-B-8jnY81.js +1 -0
- package/ui-dist/assets/blockDiagram-VD42YOAC-1Rk6YCcn.js +122 -0
- package/ui-dist/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/ui-dist/assets/c4Diagram-YG6GDRKO-DF5RJtyZ.js +10 -0
- package/ui-dist/assets/channel-D7SqxhNi.js +1 -0
- package/ui-dist/assets/chunk-4BX2VUAB-BSZDJAxk.js +1 -0
- package/ui-dist/assets/chunk-55IACEB6-DgVOW-V3.js +1 -0
- package/ui-dist/assets/chunk-B4BG7PRW-C1sGAq6t.js +165 -0
- package/ui-dist/assets/chunk-DI55MBZ5-DZyfq3VK.js +220 -0
- package/ui-dist/assets/chunk-FMBD7UC4-D6K9nYXi.js +15 -0
- package/ui-dist/assets/chunk-QN33PNHL-BJ0Ni2l9.js +1 -0
- package/ui-dist/assets/chunk-QZHKN3VN-Cwjr0vxG.js +1 -0
- package/ui-dist/assets/chunk-TZMSLE5B-C2RGCkyV.js +1 -0
- package/ui-dist/assets/classDiagram-2ON5EDUG-Cx1PlXXb.js +1 -0
- package/ui-dist/assets/classDiagram-v2-WZHVMYZB-Cx1PlXXb.js +1 -0
- package/ui-dist/assets/clike-B9uivgTg.js +1 -0
- package/ui-dist/assets/clojure-BMjYHr_A.js +1 -0
- package/ui-dist/assets/clone-5ZE-SRxk.js +1 -0
- package/ui-dist/assets/cmake-BQqOBYOt.js +1 -0
- package/ui-dist/assets/cobol-CWcv1MsR.js +1 -0
- package/ui-dist/assets/coffeescript-S37ZYGWr.js +1 -0
- package/ui-dist/assets/commonlisp-DBKNyK5s.js +1 -0
- package/ui-dist/assets/cose-bilkent-S5V4N54A-CW0Mh3DC.js +1 -0
- package/ui-dist/assets/crystal-SjHAIU92.js +1 -0
- package/ui-dist/assets/css-BnMrqG3P.js +1 -0
- package/ui-dist/assets/cypher-C_CwsFkJ.js +1 -0
- package/ui-dist/assets/cytoscape.esm-jbPEKk2Y.js +321 -0
- package/ui-dist/assets/d-pRatUO7H.js +1 -0
- package/ui-dist/assets/dagre-6UL2VRFP-BXDbyGyw.js +4 -0
- package/ui-dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/ui-dist/assets/diagram-PSM6KHXK-BHW-b3P9.js +24 -0
- package/ui-dist/assets/diagram-QEK2KX5R-vjGsaMmX.js +43 -0
- package/ui-dist/assets/diagram-S2PKOQOG-BHVhRqBj.js +24 -0
- package/ui-dist/assets/diff-DbItnlRl.js +1 -0
- package/ui-dist/assets/dockerfile-BKs6k2Af.js +1 -0
- package/ui-dist/assets/dtd-DF_7sFjM.js +1 -0
- package/ui-dist/assets/dylan-DwRh75JA.js +1 -0
- package/ui-dist/assets/ebnf-CDyGwa7X.js +1 -0
- package/ui-dist/assets/ecl-Cabwm37j.js +1 -0
- package/ui-dist/assets/eiffel-CnydiIhH.js +1 -0
- package/ui-dist/assets/elm-vLlmbW-K.js +1 -0
- package/ui-dist/assets/erDiagram-Q2GNP2WA-DwsdEDB2.js +60 -0
- package/ui-dist/assets/erlang-BNw1qcRV.js +1 -0
- package/ui-dist/assets/factor-kuTfRLto.js +1 -0
- package/ui-dist/assets/fcl-Kvtd6kyn.js +1 -0
- package/ui-dist/assets/flowDiagram-NV44I4VS-Bv7drkZu.js +162 -0
- package/ui-dist/assets/forth-Ffai-XNe.js +1 -0
- package/ui-dist/assets/fortran-DYz_wnZ1.js +1 -0
- package/ui-dist/assets/ganttDiagram-JELNMOA3-DyYAGyjV.js +267 -0
- package/ui-dist/assets/gas-Bneqetm1.js +1 -0
- package/ui-dist/assets/gherkin-heZmZLOM.js +1 -0
- package/ui-dist/assets/gitGraphDiagram-V2S2FVAM-DmKJJ2X0.js +65 -0
- package/ui-dist/assets/graph-BI12oqz9.js +1 -0
- package/ui-dist/assets/groovy-D9Dt4D0W.js +1 -0
- package/ui-dist/assets/haskell-Cw1EW3IL.js +1 -0
- package/ui-dist/assets/haxe-H-WmDvRZ.js +1 -0
- package/ui-dist/assets/http-DBlCnlav.js +1 -0
- package/ui-dist/assets/idl-BEugSyMb.js +1 -0
- package/ui-dist/assets/index-B1YmgKZD.js +1 -0
- package/ui-dist/assets/index-B5EG1mbW.js +1 -0
- package/ui-dist/assets/index-B63DPkk7.js +2 -0
- package/ui-dist/assets/index-BAalUM2u.js +1 -0
- package/ui-dist/assets/index-BCyQqUEQ.js +1 -0
- package/ui-dist/assets/index-BSRsyahM.js +1 -0
- package/ui-dist/assets/index-B_Iu4zUd.js +1 -0
- package/ui-dist/assets/index-C-BdZUIH.js +1 -0
- package/ui-dist/assets/index-CGgUnSQj.js +6 -0
- package/ui-dist/assets/index-COTIEysQ.js +1 -0
- package/ui-dist/assets/index-ChSqseHR.js +1 -0
- package/ui-dist/assets/index-Ckf1hADU.js +534 -0
- package/ui-dist/assets/index-D4M1TSCO.js +3 -0
- package/ui-dist/assets/index-DCTk2CN-.js +7 -0
- package/ui-dist/assets/index-DI-wyUUr.js +1 -0
- package/ui-dist/assets/index-DNtLqZ-D.js +1 -0
- package/ui-dist/assets/index-DSRR_614.css +1 -0
- package/ui-dist/assets/index-DarmgkJv.js +1 -0
- package/ui-dist/assets/index-DdcxF71a.js +1 -0
- package/ui-dist/assets/index-DfwWaMga.js +1 -0
- package/ui-dist/assets/index-SzUviW57.js +13 -0
- package/ui-dist/assets/index-VjZhELb6.js +1 -0
- package/ui-dist/assets/index-mEcmy8wG.js +1 -0
- package/ui-dist/assets/index-q7ldlDv6.js +1 -0
- package/ui-dist/assets/infoDiagram-HS3SLOUP-CqAxMRbC.js +2 -0
- package/ui-dist/assets/init-Gi6I4Gst.js +1 -0
- package/ui-dist/assets/javascript-iXu5QeM3.js +1 -0
- package/ui-dist/assets/journeyDiagram-XKPGCS4Q-DUJiudtY.js +139 -0
- package/ui-dist/assets/julia-DuME0IfC.js +1 -0
- package/ui-dist/assets/kanban-definition-3W4ZIXB7-CiRgJ4X2.js +89 -0
- package/ui-dist/assets/katex-B95LWT_Q.js +261 -0
- package/ui-dist/assets/layout-DJ8V2pqt.js +1 -0
- package/ui-dist/assets/linear-BwnNUuyZ.js +1 -0
- package/ui-dist/assets/livescript-BwQOo05w.js +1 -0
- package/ui-dist/assets/lua-BgMRiT3U.js +1 -0
- package/ui-dist/assets/mathematica-DTrFuWx2.js +1 -0
- package/ui-dist/assets/mbox-CNhZ1qSd.js +1 -0
- package/ui-dist/assets/mermaid.core-DLCiCWj4.js +250 -0
- package/ui-dist/assets/mindmap-definition-VGOIOE7T-epIiGA01.js +68 -0
- package/ui-dist/assets/mirc-CjQqDB4T.js +1 -0
- package/ui-dist/assets/mllike-CXdrOF99.js +1 -0
- package/ui-dist/assets/modelica-Dc1JOy9r.js +1 -0
- package/ui-dist/assets/mscgen-BA5vi2Kp.js +1 -0
- package/ui-dist/assets/mumps-BT43cFF4.js +1 -0
- package/ui-dist/assets/nginx-DdIZxoE0.js +1 -0
- package/ui-dist/assets/nsis-LdVXkNf5.js +1 -0
- package/ui-dist/assets/ntriples-BfvgReVJ.js +1 -0
- package/ui-dist/assets/octave-Ck1zUtKM.js +1 -0
- package/ui-dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/ui-dist/assets/oz-BzwKVEFT.js +1 -0
- package/ui-dist/assets/pascal--L3eBynH.js +1 -0
- package/ui-dist/assets/perl-CdXCOZ3F.js +1 -0
- package/ui-dist/assets/pieDiagram-ADFJNKIX-CHx4pJ9Y.js +30 -0
- package/ui-dist/assets/pig-CevX1Tat.js +1 -0
- package/ui-dist/assets/powershell-CFHJl5sT.js +1 -0
- package/ui-dist/assets/properties-C78fOPTZ.js +1 -0
- package/ui-dist/assets/protobuf-ChK-085T.js +1 -0
- package/ui-dist/assets/pug-DeIclll2.js +1 -0
- package/ui-dist/assets/puppet-DMA9R1ak.js +1 -0
- package/ui-dist/assets/python-BuPzkPfP.js +1 -0
- package/ui-dist/assets/q-pXgVlZs6.js +1 -0
- package/ui-dist/assets/quadrantDiagram-AYHSOK5B-D0tXmGxE.js +7 -0
- package/ui-dist/assets/r-B6wPVr8A.js +1 -0
- package/ui-dist/assets/requirementDiagram-UZGBJVZJ-BgFc9Xc4.js +64 -0
- package/ui-dist/assets/rpm-CTu-6PCP.js +1 -0
- package/ui-dist/assets/ruby-B2Rjki9n.js +1 -0
- package/ui-dist/assets/sankeyDiagram-TZEHDZUN-DqntX5wV.js +10 -0
- package/ui-dist/assets/sas-B4kiWyti.js +1 -0
- package/ui-dist/assets/scheme-C41bIUwD.js +1 -0
- package/ui-dist/assets/sequenceDiagram-WL72ISMW-BfIJeGAT.js +145 -0
- package/ui-dist/assets/shell-CjFT_Tl9.js +1 -0
- package/ui-dist/assets/sieve-C3Gn_uJK.js +1 -0
- package/ui-dist/assets/simple-mode-GW_nhZxv.js +1 -0
- package/ui-dist/assets/smalltalk-CnHTOXQT.js +1 -0
- package/ui-dist/assets/solr-DehyRSwq.js +1 -0
- package/ui-dist/assets/sparql-DkYu6x3z.js +1 -0
- package/ui-dist/assets/spreadsheet-BCZA_wO0.js +1 -0
- package/ui-dist/assets/sql-D0XecflT.js +1 -0
- package/ui-dist/assets/stateDiagram-FKZM4ZOC-CBlsmcoW.js +1 -0
- package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-DJtH2tCS.js +1 -0
- package/ui-dist/assets/stex-C3f8Ysf7.js +1 -0
- package/ui-dist/assets/stylus-B533Al4x.js +1 -0
- package/ui-dist/assets/swift-BzpIVaGY.js +1 -0
- package/ui-dist/assets/tcl-DVfN8rqt.js +1 -0
- package/ui-dist/assets/textile-CnDTJFAw.js +1 -0
- package/ui-dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
- package/ui-dist/assets/tiki-DGYXhP31.js +1 -0
- package/ui-dist/assets/timeline-definition-IT6M3QCI-C2XyT4Lo.js +61 -0
- package/ui-dist/assets/toml-Bm5Em-hy.js +1 -0
- package/ui-dist/assets/treemap-GDKQZRPO-DvTrxs0e.js +154 -0
- package/ui-dist/assets/troff-wAsdV37c.js +1 -0
- package/ui-dist/assets/ttcn-CfJYG6tj.js +1 -0
- package/ui-dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- package/ui-dist/assets/turtle-B1tBg_DP.js +1 -0
- package/ui-dist/assets/vb-CmGdzxic.js +1 -0
- package/ui-dist/assets/vbscript-BuJXcnF6.js +1 -0
- package/ui-dist/assets/velocity-D8B20fx6.js +1 -0
- package/ui-dist/assets/verilog-C6RDOZhf.js +1 -0
- package/ui-dist/assets/vhdl-lSbBsy5d.js +1 -0
- package/ui-dist/assets/webidl-ZXfAyPTL.js +1 -0
- package/ui-dist/assets/xquery-DzFWVndE.js +1 -0
- package/ui-dist/assets/xychartDiagram-PRI3JC2R-pTizTbG0.js +7 -0
- package/ui-dist/assets/yacas-BJ4BC0dw.js +1 -0
- package/ui-dist/assets/z80-Hz9HOZM7.js +1 -0
- package/ui-dist/brands/opencode-logo-dark-square.svg +18 -0
- package/ui-dist/brands/opencode-logo-light-square.svg +18 -0
- package/ui-dist/favicon-16x16.png +0 -0
- package/ui-dist/favicon-32x32.png +0 -0
- package/ui-dist/favicon.ico +0 -0
- package/ui-dist/favicon.svg +9 -0
- package/ui-dist/index.html +49 -0
- package/ui-dist/site.webmanifest +30 -0
- package/ui-dist/sw.js +42 -0
- package/ui-dist/worktree-favicon-16x16.png +0 -0
- package/ui-dist/worktree-favicon-32x32.png +0 -0
- package/ui-dist/worktree-favicon.ico +0 -0
- package/ui-dist/worktree-favicon.svg +9 -0
|
@@ -0,0 +1,3886 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Router } from "express";
|
|
3
|
+
import multer from "multer";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
6
|
+
import { activityLog, executionWorkspaces, issueExecutionDecisions, projectWorkspaces } from "@evermore.work/db";
|
|
7
|
+
import { addIssueCommentSchema, acceptIssueThreadInteractionSchema, cancelIssueThreadInteractionSchema, companySearchQuerySchema, createIssueAttachmentMetadataSchema, createIssueThreadInteractionSchema, createIssueWorkProductSchema, createIssueLabelSchema, checkoutIssueSchema, createChildIssueSchema, createIssueSchema, feedbackTargetTypeSchema, feedbackTraceStatusSchema, feedbackVoteValueSchema, upsertIssueFeedbackVoteSchema, linkIssueApprovalSchema, issueDocumentKeySchema, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, rejectIssueThreadInteractionSchema, restoreIssueDocumentRevisionSchema, respondIssueThreadInteractionSchema, updateIssueWorkProductSchema, upsertIssueDocumentSchema, updateIssueSchema, getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@evermore.work/shared";
|
|
8
|
+
import { trackAgentTaskCompleted } from "@evermore.work/shared/telemetry";
|
|
9
|
+
import { getTelemetryClient } from "../telemetry.js";
|
|
10
|
+
import { validate } from "../middleware/validate.js";
|
|
11
|
+
import * as serviceIndex from "../services/index.js";
|
|
12
|
+
import { accessService, agentService, companyService, companySearchService, goalService, heartbeatService, issueApprovalService, issueThreadInteractionService, ISSUE_LIST_DEFAULT_LIMIT, ISSUE_LIST_MAX_LIMIT, issueReferenceService, issueService, clampIssueListLimit, documentService, logActivity, projectService, routineService, workProductService, } from "../services/index.js";
|
|
13
|
+
import { logger } from "../middleware/logger.js";
|
|
14
|
+
import { conflict, forbidden, HttpError, notFound, unauthorized, unprocessable } from "../errors.js";
|
|
15
|
+
import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js";
|
|
16
|
+
import { assertNoAgentHostWorkspaceCommandMutation, collectIssueWorkspaceCommandPaths, } from "./workspace-command-authz.js";
|
|
17
|
+
import { shouldWakeAssigneeOnCheckout } from "./issues-checkout-wakeup.js";
|
|
18
|
+
import { isInlineAttachmentContentType, normalizeIssueAttachmentMaxBytes, normalizeContentType, SVG_CONTENT_TYPE, } from "../attachment-types.js";
|
|
19
|
+
import { queueIssueAssignmentWakeup } from "../services/issue-assignment-wakeup.js";
|
|
20
|
+
import { assertEnvironmentSelectionForCompany } from "./environment-selection.js";
|
|
21
|
+
import { executionWorkspaceService as executionWorkspaceServiceDirect } from "../services/execution-workspaces.js";
|
|
22
|
+
import { feedbackService } from "../services/feedback.js";
|
|
23
|
+
import { instanceSettingsService } from "../services/instance-settings.js";
|
|
24
|
+
import { environmentService } from "../services/environments.js";
|
|
25
|
+
import { redactSensitiveText } from "../redaction.js";
|
|
26
|
+
import { createCompanySearchRateLimiter, } from "../services/company-search-rate-limit.js";
|
|
27
|
+
import { applyIssueExecutionPolicyTransition, normalizeIssueExecutionPolicy, parseIssueExecutionState, redactIssueMonitorExternalRef, setIssueExecutionPolicyMonitorScheduledBy, } from "../services/issue-execution-policy.js";
|
|
28
|
+
import { parseIssueExecutionWorkspaceSettings } from "../services/execution-workspace-policy.js";
|
|
29
|
+
const MAX_ISSUE_COMMENT_LIMIT = 500;
|
|
30
|
+
const updateIssueRouteSchema = updateIssueSchema.extend({
|
|
31
|
+
interrupt: z.boolean().optional(),
|
|
32
|
+
});
|
|
33
|
+
const SUCCESSFUL_RUN_HANDOFF_ACTIONS = [
|
|
34
|
+
"issue.successful_run_handoff_required",
|
|
35
|
+
"issue.successful_run_handoff_resolved",
|
|
36
|
+
"issue.successful_run_handoff_escalated",
|
|
37
|
+
];
|
|
38
|
+
const ISSUE_WORKSPACE_AUDIT_FIELDS = new Set([
|
|
39
|
+
"projectWorkspaceId",
|
|
40
|
+
"executionWorkspaceId",
|
|
41
|
+
"executionWorkspacePreference",
|
|
42
|
+
"executionWorkspaceSettings",
|
|
43
|
+
]);
|
|
44
|
+
function readNonEmptyString(value) {
|
|
45
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
46
|
+
}
|
|
47
|
+
function hasIssueWorkspaceAuditChange(previous) {
|
|
48
|
+
return Object.keys(previous).some((key) => ISSUE_WORKSPACE_AUDIT_FIELDS.has(key));
|
|
49
|
+
}
|
|
50
|
+
function labelIssueWorkspaceMode(mode) {
|
|
51
|
+
switch (mode) {
|
|
52
|
+
case "shared_workspace":
|
|
53
|
+
return "Project default";
|
|
54
|
+
case "isolated_workspace":
|
|
55
|
+
return "New isolated workspace";
|
|
56
|
+
case "operator_branch":
|
|
57
|
+
return "Operator branch";
|
|
58
|
+
case "reuse_existing":
|
|
59
|
+
return "Reuse existing workspace";
|
|
60
|
+
case "agent_default":
|
|
61
|
+
return "Agent default";
|
|
62
|
+
case "inherit":
|
|
63
|
+
return "Inherited workspace";
|
|
64
|
+
default:
|
|
65
|
+
return "No workspace";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function emptyWorkspaceNameMaps() {
|
|
69
|
+
return {
|
|
70
|
+
projectWorkspaceNames: new Map(),
|
|
71
|
+
executionWorkspaceNames: new Map(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function summarizeIssueWorkspaceForActivity(issue, names) {
|
|
75
|
+
const settings = parseIssueExecutionWorkspaceSettings(issue.executionWorkspaceSettings);
|
|
76
|
+
const mode = settings?.mode ?? issue.executionWorkspacePreference ?? null;
|
|
77
|
+
const executionWorkspaceId = issue.executionWorkspaceId ?? null;
|
|
78
|
+
const projectWorkspaceId = issue.projectWorkspaceId ?? null;
|
|
79
|
+
const label = (() => {
|
|
80
|
+
if (executionWorkspaceId) {
|
|
81
|
+
return names.executionWorkspaceNames.get(executionWorkspaceId) ?? `Workspace ${executionWorkspaceId.slice(0, 8)}`;
|
|
82
|
+
}
|
|
83
|
+
if (projectWorkspaceId) {
|
|
84
|
+
return names.projectWorkspaceNames.get(projectWorkspaceId) ?? `Workspace ${projectWorkspaceId.slice(0, 8)}`;
|
|
85
|
+
}
|
|
86
|
+
return labelIssueWorkspaceMode(mode);
|
|
87
|
+
})();
|
|
88
|
+
return {
|
|
89
|
+
label,
|
|
90
|
+
projectWorkspaceId,
|
|
91
|
+
executionWorkspaceId,
|
|
92
|
+
mode,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function buildIssueWorkspaceChangeActivityDetails(db, companyId, previousIssue, nextIssue) {
|
|
96
|
+
const projectWorkspaceIds = [
|
|
97
|
+
previousIssue.projectWorkspaceId,
|
|
98
|
+
nextIssue.projectWorkspaceId,
|
|
99
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
100
|
+
const executionWorkspaceIds = [
|
|
101
|
+
previousIssue.executionWorkspaceId,
|
|
102
|
+
nextIssue.executionWorkspaceId,
|
|
103
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
104
|
+
const [projectRows, executionRows] = await Promise.all([
|
|
105
|
+
projectWorkspaceIds.length > 0
|
|
106
|
+
? db
|
|
107
|
+
.select({ id: projectWorkspaces.id, name: projectWorkspaces.name })
|
|
108
|
+
.from(projectWorkspaces)
|
|
109
|
+
.where(and(eq(projectWorkspaces.companyId, companyId), inArray(projectWorkspaces.id, projectWorkspaceIds)))
|
|
110
|
+
: Promise.resolve([]),
|
|
111
|
+
executionWorkspaceIds.length > 0
|
|
112
|
+
? db
|
|
113
|
+
.select({ id: executionWorkspaces.id, name: executionWorkspaces.name })
|
|
114
|
+
.from(executionWorkspaces)
|
|
115
|
+
.where(and(eq(executionWorkspaces.companyId, companyId), inArray(executionWorkspaces.id, executionWorkspaceIds)))
|
|
116
|
+
: Promise.resolve([]),
|
|
117
|
+
]);
|
|
118
|
+
const names = {
|
|
119
|
+
projectWorkspaceNames: new Map(projectRows.map((row) => [row.id, row.name])),
|
|
120
|
+
executionWorkspaceNames: new Map(executionRows.map((row) => [row.id, row.name])),
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
from: summarizeIssueWorkspaceForActivity(previousIssue, names),
|
|
124
|
+
to: summarizeIssueWorkspaceForActivity(nextIssue, names),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function hasExecutionParticipant(value) {
|
|
128
|
+
const state = parseIssueExecutionState(value);
|
|
129
|
+
if (!state || state.status !== "pending")
|
|
130
|
+
return false;
|
|
131
|
+
const participant = state.currentParticipant;
|
|
132
|
+
if (!participant)
|
|
133
|
+
return false;
|
|
134
|
+
if (participant.type === "agent")
|
|
135
|
+
return Boolean(participant.agentId);
|
|
136
|
+
if (participant.type === "user")
|
|
137
|
+
return Boolean(participant.userId);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
function hasScheduledMonitor(input) {
|
|
141
|
+
if (input.patchMonitorNextCheckAt instanceof Date && !Number.isNaN(input.patchMonitorNextCheckAt.getTime()))
|
|
142
|
+
return true;
|
|
143
|
+
if (input.patchMonitorNextCheckAt === undefined && input.existingMonitorNextCheckAt)
|
|
144
|
+
return true;
|
|
145
|
+
const policy = normalizeIssueExecutionPolicy(input.executionPolicy ?? null);
|
|
146
|
+
return Boolean(policy?.monitor?.nextCheckAt);
|
|
147
|
+
}
|
|
148
|
+
function successfulRunHandoffStateFromActivity(row) {
|
|
149
|
+
const details = row.details ?? {};
|
|
150
|
+
const state = row.action === "issue.successful_run_handoff_required"
|
|
151
|
+
? "required"
|
|
152
|
+
: row.action === "issue.successful_run_handoff_resolved"
|
|
153
|
+
? "resolved"
|
|
154
|
+
: row.action === "issue.successful_run_handoff_escalated"
|
|
155
|
+
? "escalated"
|
|
156
|
+
: null;
|
|
157
|
+
if (!state)
|
|
158
|
+
return null;
|
|
159
|
+
const detectedProgressSummary = readNonEmptyString(details.detectedProgressSummary)
|
|
160
|
+
?? readNonEmptyString(details.detected_progress_summary)
|
|
161
|
+
?? null;
|
|
162
|
+
return {
|
|
163
|
+
state,
|
|
164
|
+
required: state === "required",
|
|
165
|
+
sourceRunId: readNonEmptyString(details.sourceRunId)
|
|
166
|
+
?? readNonEmptyString(details.source_run_id)
|
|
167
|
+
?? readNonEmptyString(details.resumeFromRunId)
|
|
168
|
+
?? row.runId
|
|
169
|
+
?? null,
|
|
170
|
+
correctiveRunId: readNonEmptyString(details.correctiveRunId)
|
|
171
|
+
?? readNonEmptyString(details.corrective_run_id)
|
|
172
|
+
?? (state !== "required" ? row.runId : null),
|
|
173
|
+
assigneeAgentId: readNonEmptyString(details.assigneeAgentId)
|
|
174
|
+
?? readNonEmptyString(details.agentId)
|
|
175
|
+
?? row.agentId
|
|
176
|
+
?? null,
|
|
177
|
+
detectedProgressSummary: detectedProgressSummary
|
|
178
|
+
? redactSensitiveText(detectedProgressSummary)
|
|
179
|
+
: null,
|
|
180
|
+
createdAt: row.createdAt,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function listSuccessfulRunHandoffStates(db, companyId, issueIds) {
|
|
184
|
+
if (issueIds.length === 0)
|
|
185
|
+
return new Map();
|
|
186
|
+
const rows = await db
|
|
187
|
+
.select({
|
|
188
|
+
entityId: activityLog.entityId,
|
|
189
|
+
action: activityLog.action,
|
|
190
|
+
agentId: activityLog.agentId,
|
|
191
|
+
runId: activityLog.runId,
|
|
192
|
+
details: activityLog.details,
|
|
193
|
+
createdAt: activityLog.createdAt,
|
|
194
|
+
})
|
|
195
|
+
.from(activityLog)
|
|
196
|
+
.where(and(eq(activityLog.companyId, companyId), eq(activityLog.entityType, "issue"), inArray(activityLog.entityId, issueIds), inArray(activityLog.action, [...SUCCESSFUL_RUN_HANDOFF_ACTIONS])))
|
|
197
|
+
.orderBy(activityLog.entityId, desc(activityLog.createdAt), desc(activityLog.id));
|
|
198
|
+
const states = new Map();
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
if (states.has(row.entityId))
|
|
201
|
+
continue;
|
|
202
|
+
const state = successfulRunHandoffStateFromActivity(row);
|
|
203
|
+
if (state)
|
|
204
|
+
states.set(row.entityId, state);
|
|
205
|
+
}
|
|
206
|
+
return states;
|
|
207
|
+
}
|
|
208
|
+
const ACTIVE_REVIEW_APPROVAL_STATUSES = new Set(["pending", "revision_requested"]);
|
|
209
|
+
const INVALID_AGENT_IN_REVIEW_DISPOSITION_MESSAGE = "invalid_issue_disposition: Agent-authored updates that move an issue to in_review must include a real review path. " +
|
|
210
|
+
"This request would leave the issue in_review without anyone or anything owning the next action. " +
|
|
211
|
+
"Keep working instead of moving to review, create a request_confirmation or ask_user_questions interaction, " +
|
|
212
|
+
"link or request a pending approval, assign a human reviewer with assigneeUserId, set a typed executionState.currentParticipant through an execution policy, " +
|
|
213
|
+
"or schedule an issue monitor for an external review/check. After creating one of those review paths, retry the status update.";
|
|
214
|
+
function executionPrincipalsEqual(left, right) {
|
|
215
|
+
if (!left || !right || left.type !== right.type)
|
|
216
|
+
return false;
|
|
217
|
+
return left.type === "agent" ? left.agentId === right.agentId : left.userId === right.userId;
|
|
218
|
+
}
|
|
219
|
+
function buildExecutionStageWakeContext(input) {
|
|
220
|
+
return {
|
|
221
|
+
wakeRole: input.wakeRole,
|
|
222
|
+
stageId: input.state.currentStageId,
|
|
223
|
+
stageType: input.state.currentStageType,
|
|
224
|
+
currentParticipant: input.state.currentParticipant,
|
|
225
|
+
returnAssignee: input.state.returnAssignee,
|
|
226
|
+
reviewRequest: input.state.reviewRequest ?? null,
|
|
227
|
+
lastDecisionOutcome: input.state.lastDecisionOutcome,
|
|
228
|
+
allowedActions: input.allowedActions,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function summarizeIssueRelationForActivity(relation) {
|
|
232
|
+
return {
|
|
233
|
+
id: relation.id,
|
|
234
|
+
identifier: relation.identifier,
|
|
235
|
+
title: relation.title,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const defaultCompanySearchRateLimiter = createCompanySearchRateLimiter();
|
|
239
|
+
function companySearchRateLimitActor(req, companyId) {
|
|
240
|
+
if (req.actor.type === "agent") {
|
|
241
|
+
return {
|
|
242
|
+
companyId,
|
|
243
|
+
actorType: "agent",
|
|
244
|
+
actorId: req.actor.agentId ?? req.actor.keyId ?? "unknown-agent",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
companyId,
|
|
249
|
+
actorType: "board",
|
|
250
|
+
actorId: req.actor.userId ?? req.actor.source ?? "board",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function summarizeIssueReferenceActivityDetails(input) {
|
|
254
|
+
if (!input)
|
|
255
|
+
return {};
|
|
256
|
+
return {
|
|
257
|
+
...(input.addedReferencedIssues.length > 0 ? { addedReferencedIssues: input.addedReferencedIssues } : {}),
|
|
258
|
+
...(input.removedReferencedIssues.length > 0 ? { removedReferencedIssues: input.removedReferencedIssues } : {}),
|
|
259
|
+
...(input.currentReferencedIssues.length > 0 ? { currentReferencedIssues: input.currentReferencedIssues } : {}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function monitorPoliciesEqual(left, right) {
|
|
263
|
+
return JSON.stringify(left?.monitor ?? null) === JSON.stringify(right?.monitor ?? null);
|
|
264
|
+
}
|
|
265
|
+
function applyActorMonitorScheduledBy(policy, actorType) {
|
|
266
|
+
return setIssueExecutionPolicyMonitorScheduledBy(policy, actorType === "user" ? "board" : "assignee");
|
|
267
|
+
}
|
|
268
|
+
function assertCanManageIssueMonitor(req, assigneeAgentId, monitorChanged) {
|
|
269
|
+
if (!monitorChanged)
|
|
270
|
+
return;
|
|
271
|
+
if (req.actor.type === "board")
|
|
272
|
+
return;
|
|
273
|
+
if (req.actor.type === "agent" && req.actor.agentId && req.actor.agentId === assigneeAgentId)
|
|
274
|
+
return;
|
|
275
|
+
throw forbidden("Only the assignee agent or a board user can manage issue monitors");
|
|
276
|
+
}
|
|
277
|
+
function summarizeIssueMonitor(issue, policy) {
|
|
278
|
+
const state = parseIssueExecutionState(issue.executionState);
|
|
279
|
+
return {
|
|
280
|
+
nextCheckAt: issue.monitorNextCheckAt?.toISOString() ?? policy?.monitor?.nextCheckAt ?? null,
|
|
281
|
+
lastTriggeredAt: issue.monitorLastTriggeredAt?.toISOString() ?? state?.monitor?.lastTriggeredAt ?? null,
|
|
282
|
+
attemptCount: issue.monitorAttemptCount ?? state?.monitor?.attemptCount ?? 0,
|
|
283
|
+
notes: policy?.monitor?.notes ?? issue.monitorNotes ?? state?.monitor?.notes ?? null,
|
|
284
|
+
scheduledBy: issue.monitorScheduledBy ?? policy?.monitor?.scheduledBy ?? state?.monitor?.scheduledBy ?? null,
|
|
285
|
+
kind: policy?.monitor?.kind ?? state?.monitor?.kind ?? null,
|
|
286
|
+
serviceName: policy?.monitor?.serviceName ?? state?.monitor?.serviceName ?? null,
|
|
287
|
+
externalRef: redactIssueMonitorExternalRef(policy?.monitor?.externalRef ?? state?.monitor?.externalRef ?? null),
|
|
288
|
+
timeoutAt: policy?.monitor?.timeoutAt ?? state?.monitor?.timeoutAt ?? null,
|
|
289
|
+
maxAttempts: policy?.monitor?.maxAttempts ?? state?.monitor?.maxAttempts ?? null,
|
|
290
|
+
recoveryPolicy: policy?.monitor?.recoveryPolicy ?? state?.monitor?.recoveryPolicy ?? null,
|
|
291
|
+
status: state?.monitor?.status ?? (policy?.monitor ? "scheduled" : null),
|
|
292
|
+
clearReason: state?.monitor?.clearReason ?? null,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function activityExecutionParticipantKey(participant) {
|
|
296
|
+
return participant.type === "agent" ? `agent:${participant.agentId}` : `user:${participant.userId}`;
|
|
297
|
+
}
|
|
298
|
+
function summarizeExecutionParticipants(policy, stageType) {
|
|
299
|
+
const stage = policy?.stages.find((candidate) => candidate.type === stageType);
|
|
300
|
+
return (stage?.participants.map((participant) => ({
|
|
301
|
+
type: participant.type,
|
|
302
|
+
agentId: participant.agentId ?? null,
|
|
303
|
+
userId: participant.userId ?? null,
|
|
304
|
+
})) ?? []);
|
|
305
|
+
}
|
|
306
|
+
function isClosedIssueStatus(status) {
|
|
307
|
+
return status === "done" || status === "cancelled";
|
|
308
|
+
}
|
|
309
|
+
function shouldImplicitlyMoveCommentedIssueToTodo(input) {
|
|
310
|
+
// Only human comments should implicitly reopen finished work.
|
|
311
|
+
// Agent-authored comments remain communicative unless reopen was explicit.
|
|
312
|
+
if (input.actorType !== "user")
|
|
313
|
+
return false;
|
|
314
|
+
if (!isClosedIssueStatus(input.issueStatus) && input.issueStatus !== "blocked")
|
|
315
|
+
return false;
|
|
316
|
+
if (typeof input.assigneeAgentId !== "string" || input.assigneeAgentId.length === 0)
|
|
317
|
+
return false;
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
function isExplicitResumeCapableStatus(status) {
|
|
321
|
+
return status === "done" || status === "blocked" || status === "todo" || status === "in_progress";
|
|
322
|
+
}
|
|
323
|
+
function queueResolvedInteractionContinuationWakeup(input) {
|
|
324
|
+
if (input.interaction.continuationPolicy !== "wake_assignee"
|
|
325
|
+
&& input.interaction.continuationPolicy !== "wake_assignee_on_accept")
|
|
326
|
+
return;
|
|
327
|
+
if (input.interaction.continuationPolicy === "wake_assignee_on_accept"
|
|
328
|
+
&& input.interaction.status !== "accepted")
|
|
329
|
+
return;
|
|
330
|
+
if (input.interaction.status === "expired")
|
|
331
|
+
return;
|
|
332
|
+
if (!input.issue.assigneeAgentId || isClosedIssueStatus(input.issue.status))
|
|
333
|
+
return;
|
|
334
|
+
void input.heartbeat.wakeup(input.issue.assigneeAgentId, {
|
|
335
|
+
source: "automation",
|
|
336
|
+
triggerDetail: "system",
|
|
337
|
+
reason: "issue_commented",
|
|
338
|
+
payload: {
|
|
339
|
+
issueId: input.issue.id,
|
|
340
|
+
interactionId: input.interaction.id,
|
|
341
|
+
interactionKind: input.interaction.kind,
|
|
342
|
+
interactionStatus: input.interaction.status,
|
|
343
|
+
sourceCommentId: input.interaction.sourceCommentId ?? null,
|
|
344
|
+
sourceRunId: input.interaction.sourceRunId ?? null,
|
|
345
|
+
mutation: "interaction",
|
|
346
|
+
},
|
|
347
|
+
requestedByActorType: input.actor.actorType,
|
|
348
|
+
requestedByActorId: input.actor.actorId,
|
|
349
|
+
contextSnapshot: {
|
|
350
|
+
issueId: input.issue.id,
|
|
351
|
+
taskId: input.issue.id,
|
|
352
|
+
interactionId: input.interaction.id,
|
|
353
|
+
interactionKind: input.interaction.kind,
|
|
354
|
+
interactionStatus: input.interaction.status,
|
|
355
|
+
sourceCommentId: input.interaction.sourceCommentId ?? null,
|
|
356
|
+
sourceRunId: input.interaction.sourceRunId ?? null,
|
|
357
|
+
wakeReason: "issue_commented",
|
|
358
|
+
source: input.source,
|
|
359
|
+
},
|
|
360
|
+
}).catch((err) => logger.warn({
|
|
361
|
+
err,
|
|
362
|
+
issueId: input.issue.id,
|
|
363
|
+
interactionId: input.interaction.id,
|
|
364
|
+
agentId: input.issue.assigneeAgentId,
|
|
365
|
+
}, "failed to wake assignee on issue interaction resolution"));
|
|
366
|
+
}
|
|
367
|
+
function diffExecutionParticipants(previousPolicy, nextPolicy, stageType) {
|
|
368
|
+
const previousParticipants = summarizeExecutionParticipants(previousPolicy, stageType);
|
|
369
|
+
const nextParticipants = summarizeExecutionParticipants(nextPolicy, stageType);
|
|
370
|
+
const previousByKey = new Map(previousParticipants.map((participant) => [
|
|
371
|
+
activityExecutionParticipantKey(participant),
|
|
372
|
+
participant,
|
|
373
|
+
]));
|
|
374
|
+
const nextByKey = new Map(nextParticipants.map((participant) => [
|
|
375
|
+
activityExecutionParticipantKey(participant),
|
|
376
|
+
participant,
|
|
377
|
+
]));
|
|
378
|
+
return {
|
|
379
|
+
participants: nextParticipants,
|
|
380
|
+
addedParticipants: nextParticipants.filter((participant) => !previousByKey.has(activityExecutionParticipantKey(participant))),
|
|
381
|
+
removedParticipants: previousParticipants.filter((participant) => !nextByKey.has(activityExecutionParticipantKey(participant))),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function buildExecutionStageWakeup(input) {
|
|
385
|
+
const { issueId, previousState, nextState, interruptedRunId } = input;
|
|
386
|
+
if (!nextState)
|
|
387
|
+
return null;
|
|
388
|
+
if (nextState.status === "pending") {
|
|
389
|
+
const agentId = nextState.currentParticipant?.type === "agent" ? (nextState.currentParticipant.agentId ?? null) : null;
|
|
390
|
+
const stageChanged = previousState?.status !== "pending" ||
|
|
391
|
+
previousState?.currentStageId !== nextState.currentStageId ||
|
|
392
|
+
!executionPrincipalsEqual(previousState?.currentParticipant ?? null, nextState.currentParticipant ?? null);
|
|
393
|
+
if (!agentId || !stageChanged)
|
|
394
|
+
return null;
|
|
395
|
+
const reason = nextState.currentStageType === "approval" ? "execution_approval_requested" : "execution_review_requested";
|
|
396
|
+
const executionStage = buildExecutionStageWakeContext({
|
|
397
|
+
state: nextState,
|
|
398
|
+
wakeRole: nextState.currentStageType === "approval" ? "approver" : "reviewer",
|
|
399
|
+
allowedActions: ["approve", "request_changes"],
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
agentId,
|
|
403
|
+
wakeup: {
|
|
404
|
+
source: "assignment",
|
|
405
|
+
triggerDetail: "system",
|
|
406
|
+
reason,
|
|
407
|
+
payload: {
|
|
408
|
+
issueId,
|
|
409
|
+
mutation: "update",
|
|
410
|
+
executionStage,
|
|
411
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
412
|
+
},
|
|
413
|
+
requestedByActorType: input.requestedByActorType,
|
|
414
|
+
requestedByActorId: input.requestedByActorId,
|
|
415
|
+
contextSnapshot: {
|
|
416
|
+
issueId,
|
|
417
|
+
taskId: issueId,
|
|
418
|
+
wakeReason: reason,
|
|
419
|
+
source: "issue.execution_stage",
|
|
420
|
+
executionStage,
|
|
421
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (nextState.status === "changes_requested") {
|
|
427
|
+
const agentId = nextState.returnAssignee?.type === "agent" ? (nextState.returnAssignee.agentId ?? null) : null;
|
|
428
|
+
const becameChangesRequested = previousState?.status !== "changes_requested" ||
|
|
429
|
+
previousState?.lastDecisionId !== nextState.lastDecisionId ||
|
|
430
|
+
!executionPrincipalsEqual(previousState?.returnAssignee ?? null, nextState.returnAssignee ?? null);
|
|
431
|
+
if (!agentId || !becameChangesRequested)
|
|
432
|
+
return null;
|
|
433
|
+
const executionStage = buildExecutionStageWakeContext({
|
|
434
|
+
state: nextState,
|
|
435
|
+
wakeRole: "executor",
|
|
436
|
+
allowedActions: ["address_changes", "resubmit"],
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
agentId,
|
|
440
|
+
wakeup: {
|
|
441
|
+
source: "assignment",
|
|
442
|
+
triggerDetail: "system",
|
|
443
|
+
reason: "execution_changes_requested",
|
|
444
|
+
payload: {
|
|
445
|
+
issueId,
|
|
446
|
+
mutation: "update",
|
|
447
|
+
executionStage,
|
|
448
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
449
|
+
},
|
|
450
|
+
requestedByActorType: input.requestedByActorType,
|
|
451
|
+
requestedByActorId: input.requestedByActorId,
|
|
452
|
+
contextSnapshot: {
|
|
453
|
+
issueId,
|
|
454
|
+
taskId: issueId,
|
|
455
|
+
wakeReason: "execution_changes_requested",
|
|
456
|
+
source: "issue.execution_stage",
|
|
457
|
+
executionStage,
|
|
458
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
export function issueRoutes(db, storage, opts = {}) {
|
|
466
|
+
const router = Router();
|
|
467
|
+
const svc = issueService(db);
|
|
468
|
+
const access = accessService(db);
|
|
469
|
+
const heartbeat = heartbeatService(db, {
|
|
470
|
+
pluginWorkerManager: opts.pluginWorkerManager,
|
|
471
|
+
});
|
|
472
|
+
const feedback = feedbackService(db);
|
|
473
|
+
const companiesSvc = companyService(db);
|
|
474
|
+
let searchSvc = opts.searchService ?? null;
|
|
475
|
+
const getSearchService = () => {
|
|
476
|
+
searchSvc ??= companySearchService(db);
|
|
477
|
+
return searchSvc;
|
|
478
|
+
};
|
|
479
|
+
const searchRateLimiter = opts.searchRateLimiter ?? defaultCompanySearchRateLimiter;
|
|
480
|
+
const instanceSettings = instanceSettingsService(db);
|
|
481
|
+
const agentsSvc = agentService(db);
|
|
482
|
+
const projectsSvc = projectService(db);
|
|
483
|
+
const goalsSvc = goalService(db);
|
|
484
|
+
const issueApprovalsSvc = issueApprovalService(db);
|
|
485
|
+
const executionWorkspacesSvc = executionWorkspaceServiceDirect(db);
|
|
486
|
+
const workProductsSvc = workProductService(db);
|
|
487
|
+
const documentsSvc = documentService(db);
|
|
488
|
+
const issueReferencesSvc = issueReferenceService(db);
|
|
489
|
+
const routinesSvc = routineService(db, {
|
|
490
|
+
pluginWorkerManager: opts.pluginWorkerManager,
|
|
491
|
+
});
|
|
492
|
+
const issueTreeControlFactory = Object.prototype.hasOwnProperty.call(serviceIndex, "issueTreeControlService")
|
|
493
|
+
? serviceIndex.issueTreeControlService
|
|
494
|
+
: undefined;
|
|
495
|
+
const treeControlSvc = issueTreeControlFactory?.(db) ?? {
|
|
496
|
+
getActivePauseHoldGate: async () => null,
|
|
497
|
+
};
|
|
498
|
+
const feedbackExportService = opts?.feedbackExportService;
|
|
499
|
+
const environmentsSvc = environmentService(db);
|
|
500
|
+
function withContentPath(attachment) {
|
|
501
|
+
return {
|
|
502
|
+
...attachment,
|
|
503
|
+
contentPath: `/api/attachments/${attachment.id}/content`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function parseBooleanQuery(value) {
|
|
507
|
+
return value === true || value === "true" || value === "1";
|
|
508
|
+
}
|
|
509
|
+
async function assertIssueEnvironmentSelection(companyId, environmentId) {
|
|
510
|
+
if (environmentId === undefined || environmentId === null)
|
|
511
|
+
return;
|
|
512
|
+
await assertEnvironmentSelectionForCompany(environmentsSvc, companyId, environmentId, { allowedDrivers: ["local", "ssh", "sandbox"] });
|
|
513
|
+
}
|
|
514
|
+
async function assertAgentInReviewReviewPath(input) {
|
|
515
|
+
const nextStatus = typeof input.updateFields.status === "string"
|
|
516
|
+
? input.updateFields.status
|
|
517
|
+
: input.existing.status;
|
|
518
|
+
if (input.actorType !== "agent" || input.existing.status === "in_review" || nextStatus !== "in_review")
|
|
519
|
+
return;
|
|
520
|
+
const nextAssigneeUserId = input.updateFields.assigneeUserId === undefined
|
|
521
|
+
? input.existing.assigneeUserId
|
|
522
|
+
: input.updateFields.assigneeUserId;
|
|
523
|
+
if (typeof nextAssigneeUserId === "string" && nextAssigneeUserId.trim().length > 0)
|
|
524
|
+
return;
|
|
525
|
+
const nextExecutionState = input.updateFields.executionState === undefined
|
|
526
|
+
? input.existing.executionState
|
|
527
|
+
: input.updateFields.executionState;
|
|
528
|
+
if (hasExecutionParticipant(nextExecutionState))
|
|
529
|
+
return;
|
|
530
|
+
const nextExecutionPolicy = input.updateFields.executionPolicy;
|
|
531
|
+
if (hasScheduledMonitor({
|
|
532
|
+
existingMonitorNextCheckAt: input.existing.monitorNextCheckAt ?? null,
|
|
533
|
+
patchMonitorNextCheckAt: input.updateFields.monitorNextCheckAt,
|
|
534
|
+
executionPolicy: nextExecutionPolicy,
|
|
535
|
+
}))
|
|
536
|
+
return;
|
|
537
|
+
const interactions = await issueThreadInteractionService(db).listForIssue(input.existing.id);
|
|
538
|
+
if (interactions.some((interaction) => interaction.status === "pending"))
|
|
539
|
+
return;
|
|
540
|
+
const approvals = await issueApprovalsSvc.listApprovalsForIssue(input.existing.id);
|
|
541
|
+
if (approvals.some((approval) => ACTIVE_REVIEW_APPROVAL_STATUSES.has(String(approval.status))))
|
|
542
|
+
return;
|
|
543
|
+
throw unprocessable(INVALID_AGENT_IN_REVIEW_DISPOSITION_MESSAGE, {
|
|
544
|
+
code: "invalid_issue_disposition",
|
|
545
|
+
missing: "review_path",
|
|
546
|
+
validReviewPaths: [
|
|
547
|
+
"pending_issue_thread_interaction",
|
|
548
|
+
"linked_pending_approval",
|
|
549
|
+
"human_assignee_user_id",
|
|
550
|
+
"typed_execution_state_current_participant",
|
|
551
|
+
"scheduled_issue_monitor",
|
|
552
|
+
],
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
async function logExpiredRequestConfirmations(input) {
|
|
556
|
+
for (const interaction of input.interactions) {
|
|
557
|
+
await logActivity(db, {
|
|
558
|
+
companyId: input.issue.companyId,
|
|
559
|
+
actorType: input.actor.actorType,
|
|
560
|
+
actorId: input.actor.actorId,
|
|
561
|
+
agentId: input.actor.agentId,
|
|
562
|
+
runId: input.actor.runId,
|
|
563
|
+
action: "issue.thread_interaction_expired",
|
|
564
|
+
entityType: "issue",
|
|
565
|
+
entityId: input.issue.id,
|
|
566
|
+
details: {
|
|
567
|
+
identifier: input.issue.identifier ?? null,
|
|
568
|
+
interactionId: interaction.id,
|
|
569
|
+
interactionKind: interaction.kind,
|
|
570
|
+
interactionStatus: interaction.status,
|
|
571
|
+
source: input.source,
|
|
572
|
+
result: interaction.result ?? null,
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function parseDateQuery(value, field) {
|
|
578
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
579
|
+
return undefined;
|
|
580
|
+
const parsed = new Date(value);
|
|
581
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
582
|
+
throw new HttpError(400, `Invalid ${field} query value`);
|
|
583
|
+
}
|
|
584
|
+
return parsed;
|
|
585
|
+
}
|
|
586
|
+
async function runSingleFileUpload(req, res, fileSizeLimit) {
|
|
587
|
+
const upload = multer({
|
|
588
|
+
storage: multer.memoryStorage(),
|
|
589
|
+
limits: { fileSize: fileSizeLimit, files: 1 },
|
|
590
|
+
});
|
|
591
|
+
await new Promise((resolve, reject) => {
|
|
592
|
+
upload.single("file")(req, res, (err) => {
|
|
593
|
+
if (err)
|
|
594
|
+
reject(err);
|
|
595
|
+
else
|
|
596
|
+
resolve();
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
async function assertCanManageIssueApprovalLinks(req, res, companyId) {
|
|
601
|
+
assertCompanyAccess(req, companyId);
|
|
602
|
+
if (req.actor.type === "board")
|
|
603
|
+
return true;
|
|
604
|
+
if (!req.actor.agentId) {
|
|
605
|
+
res.status(403).json({ error: "Agent authentication required" });
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
const actorAgent = await agentsSvc.getById(req.actor.agentId);
|
|
609
|
+
if (!actorAgent || actorAgent.companyId !== companyId) {
|
|
610
|
+
res.status(403).json({ error: "Forbidden" });
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
if (actorAgent.role === "ceo" || Boolean(actorAgent.permissions?.canCreateAgents))
|
|
614
|
+
return true;
|
|
615
|
+
res.status(403).json({ error: "Missing permission to link approvals" });
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
function actorCanAccessCompany(req, companyId) {
|
|
619
|
+
if (req.actor.type === "none")
|
|
620
|
+
return false;
|
|
621
|
+
if (req.actor.type === "agent")
|
|
622
|
+
return req.actor.companyId === companyId;
|
|
623
|
+
if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin)
|
|
624
|
+
return true;
|
|
625
|
+
return (req.actor.companyIds ?? []).includes(companyId);
|
|
626
|
+
}
|
|
627
|
+
function canCreateAgentsLegacy(agent) {
|
|
628
|
+
if (agent.role === "ceo")
|
|
629
|
+
return true;
|
|
630
|
+
if (!agent.permissions || typeof agent.permissions !== "object")
|
|
631
|
+
return false;
|
|
632
|
+
return Boolean(agent.permissions.canCreateAgents);
|
|
633
|
+
}
|
|
634
|
+
async function assertCanAssignTasks(req, companyId) {
|
|
635
|
+
assertCompanyAccess(req, companyId);
|
|
636
|
+
if (req.actor.type === "board") {
|
|
637
|
+
if (req.actor.source === "local_implicit" || req.actor.isInstanceAdmin)
|
|
638
|
+
return;
|
|
639
|
+
const allowed = await access.canUser(companyId, req.actor.userId, "tasks:assign");
|
|
640
|
+
if (!allowed)
|
|
641
|
+
throw forbidden("Missing permission: tasks:assign");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (req.actor.type === "agent") {
|
|
645
|
+
if (!req.actor.agentId)
|
|
646
|
+
throw forbidden("Agent authentication required");
|
|
647
|
+
const allowedByGrant = await access.hasPermission(companyId, "agent", req.actor.agentId, "tasks:assign");
|
|
648
|
+
if (allowedByGrant)
|
|
649
|
+
return;
|
|
650
|
+
const actorAgent = await agentsSvc.getById(req.actor.agentId);
|
|
651
|
+
if (actorAgent && actorAgent.companyId === companyId && canCreateAgentsLegacy(actorAgent))
|
|
652
|
+
return;
|
|
653
|
+
throw forbidden("Missing permission: tasks:assign");
|
|
654
|
+
}
|
|
655
|
+
throw unauthorized();
|
|
656
|
+
}
|
|
657
|
+
function requireAgentRunId(req, res) {
|
|
658
|
+
if (req.actor.type !== "agent")
|
|
659
|
+
return null;
|
|
660
|
+
const runId = req.actor.runId?.trim();
|
|
661
|
+
if (runId)
|
|
662
|
+
return runId;
|
|
663
|
+
res.status(401).json({ error: "Agent run id required" });
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
async function hasActiveCheckoutManagementOverride(actorAgentId, companyId, assigneeAgentId) {
|
|
667
|
+
const allowedByGrant = await access.hasPermission(companyId, "agent", actorAgentId, "tasks:manage_active_checkouts");
|
|
668
|
+
if (allowedByGrant)
|
|
669
|
+
return true;
|
|
670
|
+
const companyAgents = await agentsSvc.list(companyId);
|
|
671
|
+
const agentsById = new Map(companyAgents.map((agent) => [agent.id, agent]));
|
|
672
|
+
const actorAgent = agentsById.get(actorAgentId);
|
|
673
|
+
if (!actorAgent)
|
|
674
|
+
return false;
|
|
675
|
+
if (canCreateAgentsLegacy(actorAgent))
|
|
676
|
+
return true;
|
|
677
|
+
// Reporting-chain managers may intervene in an agent's active checkout
|
|
678
|
+
// without taking the task over. Peers must own the checkout/run first.
|
|
679
|
+
let cursor = assigneeAgentId;
|
|
680
|
+
for (let depth = 0; cursor && depth < 50; depth += 1) {
|
|
681
|
+
const assignee = agentsById.get(cursor);
|
|
682
|
+
if (!assignee)
|
|
683
|
+
return false;
|
|
684
|
+
if (assignee.reportsTo === actorAgentId)
|
|
685
|
+
return true;
|
|
686
|
+
cursor = assignee.reportsTo;
|
|
687
|
+
}
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
async function assertAgentIssueMutationAllowed(req, res, issue) {
|
|
691
|
+
if (req.actor.type !== "agent")
|
|
692
|
+
return true;
|
|
693
|
+
const actorAgentId = req.actor.agentId;
|
|
694
|
+
if (!actorAgentId) {
|
|
695
|
+
res.status(403).json({ error: "Agent authentication required" });
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
if (issue.assigneeAgentId === null) {
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
if (issue.assigneeAgentId !== actorAgentId) {
|
|
702
|
+
if (await hasActiveCheckoutManagementOverride(actorAgentId, issue.companyId, issue.assigneeAgentId)) {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
if (issue.status === "in_progress") {
|
|
706
|
+
res.status(409).json({
|
|
707
|
+
error: "Issue is checked out by another agent",
|
|
708
|
+
details: {
|
|
709
|
+
issueId: issue.id,
|
|
710
|
+
assigneeAgentId: issue.assigneeAgentId,
|
|
711
|
+
actorAgentId,
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
res.status(403).json({
|
|
717
|
+
error: "Agent cannot mutate another agent's issue",
|
|
718
|
+
details: {
|
|
719
|
+
issueId: issue.id,
|
|
720
|
+
assigneeAgentId: issue.assigneeAgentId,
|
|
721
|
+
actorAgentId,
|
|
722
|
+
status: issue.status,
|
|
723
|
+
securityPrinciples: ["Least Privilege", "Complete Mediation", "Fail Securely"],
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
if (issue.status !== "in_progress") {
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
const runId = requireAgentRunId(req, res);
|
|
733
|
+
if (!runId)
|
|
734
|
+
return false;
|
|
735
|
+
const ownership = await svc.assertCheckoutOwner(issue.id, actorAgentId, runId);
|
|
736
|
+
if (ownership.adoptedFromRunId) {
|
|
737
|
+
const actor = getActorInfo(req);
|
|
738
|
+
await logActivity(db, {
|
|
739
|
+
companyId: issue.companyId,
|
|
740
|
+
actorType: actor.actorType,
|
|
741
|
+
actorId: actor.actorId,
|
|
742
|
+
agentId: actor.agentId,
|
|
743
|
+
runId: actor.runId,
|
|
744
|
+
action: "issue.checkout_lock_adopted",
|
|
745
|
+
entityType: "issue",
|
|
746
|
+
entityId: issue.id,
|
|
747
|
+
details: {
|
|
748
|
+
previousCheckoutRunId: ownership.adoptedFromRunId,
|
|
749
|
+
checkoutRunId: runId,
|
|
750
|
+
reason: "stale_checkout_run",
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
function assertStructuredCommentFieldsAllowed(req, res, input) {
|
|
757
|
+
const hasStructuredFields = input.presentation !== undefined || input.metadata !== undefined;
|
|
758
|
+
if (!hasStructuredFields)
|
|
759
|
+
return true;
|
|
760
|
+
if (req.actor.type === "board")
|
|
761
|
+
return true;
|
|
762
|
+
res.status(403).json({
|
|
763
|
+
error: "Only board users may set structured comment presentation or metadata",
|
|
764
|
+
details: {
|
|
765
|
+
securityPrinciples: ["Least Privilege", "Secure Defaults", "Complete Mediation"],
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
async function assertExplicitResumeIntentAllowed(req, res, issue) {
|
|
771
|
+
if (issue.status === "cancelled") {
|
|
772
|
+
res.status(409).json({
|
|
773
|
+
error: "Cancelled issues must be restored through the dedicated restore flow",
|
|
774
|
+
details: {
|
|
775
|
+
issueId: issue.id,
|
|
776
|
+
status: issue.status,
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
if (!isExplicitResumeCapableStatus(issue.status)) {
|
|
782
|
+
res.status(409).json({
|
|
783
|
+
error: "Issue is not resumable through comment follow-up intent",
|
|
784
|
+
details: { issueId: issue.id, status: issue.status },
|
|
785
|
+
});
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
const activePauseHold = await treeControlSvc.getActivePauseHoldGate(issue.companyId, issue.id);
|
|
789
|
+
if (activePauseHold) {
|
|
790
|
+
res.status(409).json({
|
|
791
|
+
error: "Issue follow-up blocked by active subtree pause hold",
|
|
792
|
+
details: {
|
|
793
|
+
issueId: issue.id,
|
|
794
|
+
holdId: activePauseHold.holdId,
|
|
795
|
+
rootIssueId: activePauseHold.rootIssueId,
|
|
796
|
+
mode: activePauseHold.mode,
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
if (issue.status === "blocked") {
|
|
802
|
+
const readiness = await svc.getDependencyReadiness(issue.id);
|
|
803
|
+
if (readiness.unresolvedBlockerCount > 0) {
|
|
804
|
+
res.status(409).json({
|
|
805
|
+
error: "Issue follow-up blocked by unresolved blockers",
|
|
806
|
+
details: {
|
|
807
|
+
issueId: issue.id,
|
|
808
|
+
unresolvedBlockerIssueIds: readiness.unresolvedBlockerIssueIds,
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (req.actor.type !== "agent")
|
|
815
|
+
return true;
|
|
816
|
+
const actorAgentId = req.actor.agentId;
|
|
817
|
+
if (!actorAgentId) {
|
|
818
|
+
res.status(403).json({ error: "Agent authentication required" });
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
if (!issue.assigneeAgentId) {
|
|
822
|
+
res.status(409).json({
|
|
823
|
+
error: "Issue follow-up requires an assigned agent",
|
|
824
|
+
details: { issueId: issue.id, actorAgentId },
|
|
825
|
+
});
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
if (issue.assigneeAgentId === actorAgentId)
|
|
829
|
+
return true;
|
|
830
|
+
if (await hasActiveCheckoutManagementOverride(actorAgentId, issue.companyId, issue.assigneeAgentId)) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
res.status(403).json({
|
|
834
|
+
error: "Agent cannot request follow-up for another agent's issue",
|
|
835
|
+
details: {
|
|
836
|
+
issueId: issue.id,
|
|
837
|
+
assigneeAgentId: issue.assigneeAgentId,
|
|
838
|
+
actorAgentId,
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
async function resolveActiveIssueRun(issue) {
|
|
844
|
+
let runToInterrupt = issue.executionRunId ? await heartbeat.getRun(issue.executionRunId) : null;
|
|
845
|
+
if ((!runToInterrupt || runToInterrupt.status !== "running") && issue.assigneeAgentId) {
|
|
846
|
+
const activeRun = await heartbeat.getActiveRunForAgent(issue.assigneeAgentId);
|
|
847
|
+
const activeIssueId = activeRun &&
|
|
848
|
+
activeRun.contextSnapshot &&
|
|
849
|
+
typeof activeRun.contextSnapshot === "object" &&
|
|
850
|
+
typeof activeRun.contextSnapshot.issueId === "string"
|
|
851
|
+
? activeRun.contextSnapshot.issueId
|
|
852
|
+
: null;
|
|
853
|
+
if (activeRun && activeRun.status === "running" && activeIssueId === issue.id) {
|
|
854
|
+
runToInterrupt = activeRun;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return runToInterrupt?.status === "running" ? runToInterrupt : null;
|
|
858
|
+
}
|
|
859
|
+
async function normalizeIssueAssigneeAgentReference(companyId, rawAssigneeAgentId) {
|
|
860
|
+
if (rawAssigneeAgentId === undefined || rawAssigneeAgentId === null) {
|
|
861
|
+
return rawAssigneeAgentId;
|
|
862
|
+
}
|
|
863
|
+
const raw = rawAssigneeAgentId.trim();
|
|
864
|
+
if (raw.length === 0) {
|
|
865
|
+
return rawAssigneeAgentId;
|
|
866
|
+
}
|
|
867
|
+
const resolved = await agentsSvc.resolveByReference(companyId, raw);
|
|
868
|
+
if (resolved.ambiguous) {
|
|
869
|
+
throw conflict("Agent shortname is ambiguous in this company. Use the agent ID.");
|
|
870
|
+
}
|
|
871
|
+
if (!resolved.agent) {
|
|
872
|
+
throw notFound("Agent not found");
|
|
873
|
+
}
|
|
874
|
+
return resolved.agent.id;
|
|
875
|
+
}
|
|
876
|
+
function toValidTimestamp(value) {
|
|
877
|
+
if (!value)
|
|
878
|
+
return null;
|
|
879
|
+
const timestamp = value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
880
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
881
|
+
}
|
|
882
|
+
function isQueuedIssueCommentForActiveRun(params) {
|
|
883
|
+
const activeRunStartedAtMs = toValidTimestamp(params.activeRun.startedAt) ?? toValidTimestamp(params.activeRun.createdAt);
|
|
884
|
+
const commentCreatedAtMs = toValidTimestamp(params.comment.createdAt);
|
|
885
|
+
if (activeRunStartedAtMs === null || commentCreatedAtMs === null)
|
|
886
|
+
return false;
|
|
887
|
+
if (params.comment.authorAgentId && params.comment.authorAgentId === params.activeRun.agentId)
|
|
888
|
+
return false;
|
|
889
|
+
return commentCreatedAtMs >= activeRunStartedAtMs;
|
|
890
|
+
}
|
|
891
|
+
async function getClosedIssueExecutionWorkspace(issue) {
|
|
892
|
+
if (!issue.executionWorkspaceId)
|
|
893
|
+
return null;
|
|
894
|
+
const workspace = await executionWorkspacesSvc.getById(issue.executionWorkspaceId);
|
|
895
|
+
if (!workspace || !isClosedIsolatedExecutionWorkspace(workspace))
|
|
896
|
+
return null;
|
|
897
|
+
return workspace;
|
|
898
|
+
}
|
|
899
|
+
function respondClosedIssueExecutionWorkspace(res, workspace) {
|
|
900
|
+
res.status(409).json({
|
|
901
|
+
error: getClosedIsolatedExecutionWorkspaceMessage(workspace),
|
|
902
|
+
executionWorkspace: workspace,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
async function resolveIssueRouteId(rawId) {
|
|
906
|
+
const identifier = normalizeIssueReferenceIdentifier(rawId);
|
|
907
|
+
if (identifier) {
|
|
908
|
+
const issue = await svc.getByIdentifier(identifier);
|
|
909
|
+
if (issue) {
|
|
910
|
+
return issue.id;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return rawId;
|
|
914
|
+
}
|
|
915
|
+
async function resolveIssueProjectAndGoal(issue) {
|
|
916
|
+
const projectPromise = issue.projectId ? projectsSvc.getById(issue.projectId) : Promise.resolve(null);
|
|
917
|
+
const directGoalPromise = issue.goalId ? goalsSvc.getById(issue.goalId) : Promise.resolve(null);
|
|
918
|
+
const [project, directGoal] = await Promise.all([projectPromise, directGoalPromise]);
|
|
919
|
+
if (directGoal) {
|
|
920
|
+
return { project, goal: directGoal };
|
|
921
|
+
}
|
|
922
|
+
const projectGoalId = project?.goalId ?? project?.goalIds[0] ?? null;
|
|
923
|
+
if (projectGoalId) {
|
|
924
|
+
const projectGoal = await goalsSvc.getById(projectGoalId);
|
|
925
|
+
return { project, goal: projectGoal };
|
|
926
|
+
}
|
|
927
|
+
if (!issue.projectId) {
|
|
928
|
+
const defaultGoal = await goalsSvc.getDefaultCompanyGoal(issue.companyId);
|
|
929
|
+
return { project, goal: defaultGoal };
|
|
930
|
+
}
|
|
931
|
+
return { project, goal: null };
|
|
932
|
+
}
|
|
933
|
+
// Resolve issue identifiers (e.g. "EVR-39") to UUIDs for all /issues/:id routes
|
|
934
|
+
router.param("id", async (req, res, next, rawId) => {
|
|
935
|
+
try {
|
|
936
|
+
req.params.id = await resolveIssueRouteId(rawId);
|
|
937
|
+
next();
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
next(err);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
// Resolve issue identifiers (e.g. "EVR-39") to UUIDs for company-scoped attachment routes.
|
|
944
|
+
router.param("issueId", async (req, res, next, rawId) => {
|
|
945
|
+
try {
|
|
946
|
+
req.params.issueId = await resolveIssueRouteId(rawId);
|
|
947
|
+
next();
|
|
948
|
+
}
|
|
949
|
+
catch (err) {
|
|
950
|
+
next(err);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
// Common malformed path when companyId is empty in "/api/companies/{companyId}/issues".
|
|
954
|
+
router.get("/issues", (_req, res) => {
|
|
955
|
+
res.status(400).json({
|
|
956
|
+
error: "Missing companyId in path. Use /api/companies/{companyId}/issues.",
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
router.get("/companies/:companyId/search", async (req, res) => {
|
|
960
|
+
const companyId = req.params.companyId;
|
|
961
|
+
assertCompanyAccess(req, companyId);
|
|
962
|
+
const query = companySearchQuerySchema.parse(req.query);
|
|
963
|
+
const rateLimit = searchRateLimiter.consume(companySearchRateLimitActor(req, companyId));
|
|
964
|
+
res.setHeader("X-RateLimit-Limit", String(rateLimit.limit));
|
|
965
|
+
res.setHeader("X-RateLimit-Remaining", String(rateLimit.remaining));
|
|
966
|
+
if (!rateLimit.allowed) {
|
|
967
|
+
res.setHeader("Retry-After", String(rateLimit.retryAfterSeconds));
|
|
968
|
+
res.status(429).json({
|
|
969
|
+
error: "Search rate limit exceeded",
|
|
970
|
+
retryAfterSeconds: rateLimit.retryAfterSeconds,
|
|
971
|
+
});
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const result = await getSearchService().search(companyId, query);
|
|
975
|
+
res.json(result);
|
|
976
|
+
});
|
|
977
|
+
router.get("/companies/:companyId/issues", async (req, res) => {
|
|
978
|
+
const companyId = req.params.companyId;
|
|
979
|
+
assertCompanyAccess(req, companyId);
|
|
980
|
+
const assigneeUserFilterRaw = req.query.assigneeUserId;
|
|
981
|
+
const touchedByUserFilterRaw = req.query.touchedByUserId;
|
|
982
|
+
const inboxArchivedByUserFilterRaw = req.query.inboxArchivedByUserId;
|
|
983
|
+
const unreadForUserFilterRaw = req.query.unreadForUserId;
|
|
984
|
+
const assigneeUserId = assigneeUserFilterRaw === "me" && req.actor.type === "board"
|
|
985
|
+
? req.actor.userId
|
|
986
|
+
: assigneeUserFilterRaw;
|
|
987
|
+
const touchedByUserId = touchedByUserFilterRaw === "me" && req.actor.type === "board"
|
|
988
|
+
? req.actor.userId
|
|
989
|
+
: touchedByUserFilterRaw;
|
|
990
|
+
const inboxArchivedByUserId = inboxArchivedByUserFilterRaw === "me" && req.actor.type === "board"
|
|
991
|
+
? req.actor.userId
|
|
992
|
+
: inboxArchivedByUserFilterRaw;
|
|
993
|
+
const unreadForUserId = unreadForUserFilterRaw === "me" && req.actor.type === "board"
|
|
994
|
+
? req.actor.userId
|
|
995
|
+
: unreadForUserFilterRaw;
|
|
996
|
+
const rawLimit = req.query.limit;
|
|
997
|
+
const parsedLimit = rawLimit !== undefined && /^\d+$/.test(rawLimit)
|
|
998
|
+
? Number.parseInt(rawLimit, 10)
|
|
999
|
+
: null;
|
|
1000
|
+
const limit = parsedLimit === null ? ISSUE_LIST_DEFAULT_LIMIT : clampIssueListLimit(parsedLimit);
|
|
1001
|
+
const rawOffset = req.query.offset;
|
|
1002
|
+
const parsedOffset = rawOffset !== undefined && /^\d+$/.test(rawOffset)
|
|
1003
|
+
? Number.parseInt(rawOffset, 10)
|
|
1004
|
+
: null;
|
|
1005
|
+
if (assigneeUserFilterRaw === "me" && (!assigneeUserId || req.actor.type !== "board")) {
|
|
1006
|
+
res.status(403).json({ error: "assigneeUserId=me requires board authentication" });
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (touchedByUserFilterRaw === "me" && (!touchedByUserId || req.actor.type !== "board")) {
|
|
1010
|
+
res.status(403).json({ error: "touchedByUserId=me requires board authentication" });
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (inboxArchivedByUserFilterRaw === "me" && (!inboxArchivedByUserId || req.actor.type !== "board")) {
|
|
1014
|
+
res.status(403).json({ error: "inboxArchivedByUserId=me requires board authentication" });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (unreadForUserFilterRaw === "me" && (!unreadForUserId || req.actor.type !== "board")) {
|
|
1018
|
+
res.status(403).json({ error: "unreadForUserId=me requires board authentication" });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (rawLimit !== undefined && (parsedLimit === null || !Number.isInteger(parsedLimit) || parsedLimit <= 0)) {
|
|
1022
|
+
res.status(400).json({ error: `limit must be a positive integer up to ${ISSUE_LIST_MAX_LIMIT}` });
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (rawOffset !== undefined && (parsedOffset === null || !Number.isInteger(parsedOffset) || parsedOffset < 0)) {
|
|
1026
|
+
res.status(400).json({ error: "offset must be a non-negative integer" });
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const offset = parsedOffset ?? 0;
|
|
1030
|
+
const result = await svc.list(companyId, {
|
|
1031
|
+
status: req.query.status,
|
|
1032
|
+
assigneeAgentId: req.query.assigneeAgentId,
|
|
1033
|
+
participantAgentId: req.query.participantAgentId,
|
|
1034
|
+
assigneeUserId,
|
|
1035
|
+
touchedByUserId,
|
|
1036
|
+
inboxArchivedByUserId,
|
|
1037
|
+
unreadForUserId,
|
|
1038
|
+
projectId: req.query.projectId,
|
|
1039
|
+
workspaceId: req.query.workspaceId,
|
|
1040
|
+
executionWorkspaceId: req.query.executionWorkspaceId,
|
|
1041
|
+
parentId: req.query.parentId,
|
|
1042
|
+
descendantOf: req.query.descendantOf,
|
|
1043
|
+
labelId: req.query.labelId,
|
|
1044
|
+
originKind: req.query.originKind,
|
|
1045
|
+
originKindPrefix: req.query.originKindPrefix,
|
|
1046
|
+
originId: req.query.originId,
|
|
1047
|
+
includeRoutineExecutions: req.query.includeRoutineExecutions === "true" || req.query.includeRoutineExecutions === "1",
|
|
1048
|
+
excludeRoutineExecutions: req.query.excludeRoutineExecutions === "true" || req.query.excludeRoutineExecutions === "1",
|
|
1049
|
+
includePluginOperations: req.query.includePluginOperations === "true" || req.query.includePluginOperations === "1",
|
|
1050
|
+
includeBlockedBy: req.query.includeBlockedBy === "true" || req.query.includeBlockedBy === "1",
|
|
1051
|
+
q: req.query.q,
|
|
1052
|
+
limit,
|
|
1053
|
+
offset,
|
|
1054
|
+
});
|
|
1055
|
+
const handoffStates = await listSuccessfulRunHandoffStates(db, companyId, result.map((issue) => issue.id));
|
|
1056
|
+
res.json(result.map((issue) => ({
|
|
1057
|
+
...issue,
|
|
1058
|
+
successfulRunHandoff: handoffStates.get(issue.id) ?? null,
|
|
1059
|
+
})));
|
|
1060
|
+
});
|
|
1061
|
+
router.get("/companies/:companyId/labels", async (req, res) => {
|
|
1062
|
+
const companyId = req.params.companyId;
|
|
1063
|
+
assertCompanyAccess(req, companyId);
|
|
1064
|
+
const result = await svc.listLabels(companyId);
|
|
1065
|
+
res.json(result);
|
|
1066
|
+
});
|
|
1067
|
+
router.post("/companies/:companyId/labels", validate(createIssueLabelSchema), async (req, res) => {
|
|
1068
|
+
const companyId = req.params.companyId;
|
|
1069
|
+
assertCompanyAccess(req, companyId);
|
|
1070
|
+
const label = await svc.createLabel(companyId, req.body);
|
|
1071
|
+
const actor = getActorInfo(req);
|
|
1072
|
+
await logActivity(db, {
|
|
1073
|
+
companyId,
|
|
1074
|
+
actorType: actor.actorType,
|
|
1075
|
+
actorId: actor.actorId,
|
|
1076
|
+
agentId: actor.agentId,
|
|
1077
|
+
runId: actor.runId,
|
|
1078
|
+
action: "label.created",
|
|
1079
|
+
entityType: "label",
|
|
1080
|
+
entityId: label.id,
|
|
1081
|
+
details: { name: label.name, color: label.color },
|
|
1082
|
+
});
|
|
1083
|
+
res.status(201).json(label);
|
|
1084
|
+
});
|
|
1085
|
+
router.delete("/labels/:labelId", async (req, res) => {
|
|
1086
|
+
const labelId = req.params.labelId;
|
|
1087
|
+
const existing = await svc.getLabelById(labelId);
|
|
1088
|
+
if (!existing) {
|
|
1089
|
+
res.status(404).json({ error: "Label not found" });
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
assertCompanyAccess(req, existing.companyId);
|
|
1093
|
+
const removed = await svc.deleteLabel(labelId);
|
|
1094
|
+
if (!removed) {
|
|
1095
|
+
res.status(404).json({ error: "Label not found" });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const actor = getActorInfo(req);
|
|
1099
|
+
await logActivity(db, {
|
|
1100
|
+
companyId: removed.companyId,
|
|
1101
|
+
actorType: actor.actorType,
|
|
1102
|
+
actorId: actor.actorId,
|
|
1103
|
+
agentId: actor.agentId,
|
|
1104
|
+
runId: actor.runId,
|
|
1105
|
+
action: "label.deleted",
|
|
1106
|
+
entityType: "label",
|
|
1107
|
+
entityId: removed.id,
|
|
1108
|
+
details: { name: removed.name, color: removed.color },
|
|
1109
|
+
});
|
|
1110
|
+
res.json(removed);
|
|
1111
|
+
});
|
|
1112
|
+
router.get("/issues/:id/heartbeat-context", async (req, res) => {
|
|
1113
|
+
const id = req.params.id;
|
|
1114
|
+
const issue = await svc.getById(id);
|
|
1115
|
+
if (!issue) {
|
|
1116
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1120
|
+
const wakeCommentId = typeof req.query.wakeCommentId === "string" && req.query.wakeCommentId.trim().length > 0
|
|
1121
|
+
? req.query.wakeCommentId.trim()
|
|
1122
|
+
: null;
|
|
1123
|
+
const currentExecutionWorkspacePromise = issue.executionWorkspaceId
|
|
1124
|
+
? executionWorkspacesSvc.getById(issue.executionWorkspaceId)
|
|
1125
|
+
: Promise.resolve(null);
|
|
1126
|
+
const [{ project, goal }, ancestors, commentCursor, wakeComment, relations, blockerAttention, productivityReview, attachments, continuationSummary, currentExecutionWorkspace,] = await Promise.all([
|
|
1127
|
+
resolveIssueProjectAndGoal(issue),
|
|
1128
|
+
svc.getAncestors(issue.id),
|
|
1129
|
+
svc.getCommentCursor(issue.id),
|
|
1130
|
+
wakeCommentId ? svc.getComment(wakeCommentId) : null,
|
|
1131
|
+
svc.getRelationSummaries(issue.id),
|
|
1132
|
+
svc.listBlockerAttention(issue.companyId, [issue]).then((map) => map.get(issue.id) ?? null),
|
|
1133
|
+
svc.listProductivityReviews(issue.companyId, [issue.id]).then((map) => map.get(issue.id) ?? null),
|
|
1134
|
+
svc.listAttachments(issue.id),
|
|
1135
|
+
documentsSvc.getIssueDocumentByKey(issue.id, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY),
|
|
1136
|
+
currentExecutionWorkspacePromise,
|
|
1137
|
+
]);
|
|
1138
|
+
res.json({
|
|
1139
|
+
issue: {
|
|
1140
|
+
id: issue.id,
|
|
1141
|
+
identifier: issue.identifier,
|
|
1142
|
+
title: issue.title,
|
|
1143
|
+
description: issue.description,
|
|
1144
|
+
status: issue.status,
|
|
1145
|
+
workMode: issue.workMode,
|
|
1146
|
+
...(blockerAttention ? { blockerAttention } : {}),
|
|
1147
|
+
productivityReview,
|
|
1148
|
+
priority: issue.priority,
|
|
1149
|
+
projectId: issue.projectId,
|
|
1150
|
+
goalId: goal?.id ?? issue.goalId,
|
|
1151
|
+
parentId: issue.parentId,
|
|
1152
|
+
blockedBy: relations.blockedBy,
|
|
1153
|
+
blocks: relations.blocks,
|
|
1154
|
+
assigneeAgentId: issue.assigneeAgentId,
|
|
1155
|
+
assigneeUserId: issue.assigneeUserId,
|
|
1156
|
+
originKind: issue.originKind,
|
|
1157
|
+
originId: issue.originId,
|
|
1158
|
+
updatedAt: issue.updatedAt,
|
|
1159
|
+
},
|
|
1160
|
+
ancestors: ancestors.map((ancestor) => ({
|
|
1161
|
+
id: ancestor.id,
|
|
1162
|
+
identifier: ancestor.identifier,
|
|
1163
|
+
title: ancestor.title,
|
|
1164
|
+
status: ancestor.status,
|
|
1165
|
+
priority: ancestor.priority,
|
|
1166
|
+
})),
|
|
1167
|
+
project: project
|
|
1168
|
+
? {
|
|
1169
|
+
id: project.id,
|
|
1170
|
+
name: project.name,
|
|
1171
|
+
status: project.status,
|
|
1172
|
+
targetDate: project.targetDate,
|
|
1173
|
+
}
|
|
1174
|
+
: null,
|
|
1175
|
+
goal: goal
|
|
1176
|
+
? {
|
|
1177
|
+
id: goal.id,
|
|
1178
|
+
title: goal.title,
|
|
1179
|
+
status: goal.status,
|
|
1180
|
+
level: goal.level,
|
|
1181
|
+
parentId: goal.parentId,
|
|
1182
|
+
}
|
|
1183
|
+
: null,
|
|
1184
|
+
commentCursor,
|
|
1185
|
+
wakeComment: wakeComment && wakeComment.issueId === issue.id
|
|
1186
|
+
? wakeComment
|
|
1187
|
+
: null,
|
|
1188
|
+
attachments: attachments.map((a) => ({
|
|
1189
|
+
id: a.id,
|
|
1190
|
+
filename: a.originalFilename,
|
|
1191
|
+
contentType: a.contentType,
|
|
1192
|
+
byteSize: a.byteSize,
|
|
1193
|
+
contentPath: withContentPath(a).contentPath,
|
|
1194
|
+
createdAt: a.createdAt,
|
|
1195
|
+
})),
|
|
1196
|
+
continuationSummary: continuationSummary
|
|
1197
|
+
? {
|
|
1198
|
+
key: continuationSummary.key,
|
|
1199
|
+
title: continuationSummary.title,
|
|
1200
|
+
body: continuationSummary.body,
|
|
1201
|
+
latestRevisionId: continuationSummary.latestRevisionId,
|
|
1202
|
+
latestRevisionNumber: continuationSummary.latestRevisionNumber,
|
|
1203
|
+
updatedAt: continuationSummary.updatedAt,
|
|
1204
|
+
}
|
|
1205
|
+
: null,
|
|
1206
|
+
currentExecutionWorkspace,
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
router.get("/issues/:id", async (req, res) => {
|
|
1210
|
+
const id = req.params.id;
|
|
1211
|
+
const issue = await svc.getById(id);
|
|
1212
|
+
if (!issue) {
|
|
1213
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1217
|
+
const [{ project, goal }, ancestors, mentionedProjectIds, documentPayload, relations, blockerAttention, productivityReview, referenceSummary, successfulRunHandoffStates,] = await Promise.all([
|
|
1218
|
+
resolveIssueProjectAndGoal(issue),
|
|
1219
|
+
svc.getAncestors(issue.id),
|
|
1220
|
+
svc.findMentionedProjectIds(issue.id, { includeCommentBodies: false }),
|
|
1221
|
+
documentsSvc.getIssueDocumentPayload(issue),
|
|
1222
|
+
svc.getRelationSummaries(issue.id),
|
|
1223
|
+
svc.listBlockerAttention(issue.companyId, [issue]).then((map) => map.get(issue.id) ?? null),
|
|
1224
|
+
svc.listProductivityReviews(issue.companyId, [issue.id]).then((map) => map.get(issue.id) ?? null),
|
|
1225
|
+
issueReferencesSvc.listIssueReferenceSummary(issue.id),
|
|
1226
|
+
listSuccessfulRunHandoffStates(db, issue.companyId, [issue.id]),
|
|
1227
|
+
]);
|
|
1228
|
+
const mentionedProjects = mentionedProjectIds.length > 0
|
|
1229
|
+
? await projectsSvc.listByIds(issue.companyId, mentionedProjectIds)
|
|
1230
|
+
: [];
|
|
1231
|
+
const currentExecutionWorkspace = issue.executionWorkspaceId
|
|
1232
|
+
? await executionWorkspacesSvc.getById(issue.executionWorkspaceId)
|
|
1233
|
+
: null;
|
|
1234
|
+
const workProducts = await workProductsSvc.listForIssue(issue.id);
|
|
1235
|
+
res.json({
|
|
1236
|
+
...issue,
|
|
1237
|
+
goalId: goal?.id ?? issue.goalId,
|
|
1238
|
+
ancestors,
|
|
1239
|
+
...(blockerAttention ? { blockerAttention } : {}),
|
|
1240
|
+
productivityReview,
|
|
1241
|
+
successfulRunHandoff: successfulRunHandoffStates.get(issue.id) ?? null,
|
|
1242
|
+
blockedBy: relations.blockedBy,
|
|
1243
|
+
blocks: relations.blocks,
|
|
1244
|
+
relatedWork: referenceSummary,
|
|
1245
|
+
referencedIssueIdentifiers: referenceSummary.outbound.map((item) => item.issue.identifier ?? item.issue.id),
|
|
1246
|
+
...documentPayload,
|
|
1247
|
+
project: project ?? null,
|
|
1248
|
+
goal: goal ?? null,
|
|
1249
|
+
mentionedProjects,
|
|
1250
|
+
currentExecutionWorkspace,
|
|
1251
|
+
workProducts,
|
|
1252
|
+
});
|
|
1253
|
+
});
|
|
1254
|
+
router.get("/issues/:id/work-products", async (req, res) => {
|
|
1255
|
+
const id = req.params.id;
|
|
1256
|
+
const issue = await svc.getById(id);
|
|
1257
|
+
if (!issue) {
|
|
1258
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1262
|
+
const workProducts = await workProductsSvc.listForIssue(issue.id);
|
|
1263
|
+
res.json(workProducts);
|
|
1264
|
+
});
|
|
1265
|
+
router.get("/issues/:id/documents", async (req, res) => {
|
|
1266
|
+
const id = req.params.id;
|
|
1267
|
+
const issue = await svc.getById(id);
|
|
1268
|
+
if (!issue) {
|
|
1269
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1273
|
+
const docs = await documentsSvc.listIssueDocuments(issue.id, {
|
|
1274
|
+
includeSystem: req.query.includeSystem === "true",
|
|
1275
|
+
});
|
|
1276
|
+
res.json(docs);
|
|
1277
|
+
});
|
|
1278
|
+
router.get("/issues/:id/documents/:key", async (req, res) => {
|
|
1279
|
+
const id = req.params.id;
|
|
1280
|
+
const issue = await svc.getById(id);
|
|
1281
|
+
if (!issue) {
|
|
1282
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1286
|
+
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
|
|
1287
|
+
if (!keyParsed.success) {
|
|
1288
|
+
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const doc = await documentsSvc.getIssueDocumentByKey(issue.id, keyParsed.data);
|
|
1292
|
+
if (!doc) {
|
|
1293
|
+
res.status(404).json({ error: "Document not found" });
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
res.json(doc);
|
|
1297
|
+
});
|
|
1298
|
+
router.put("/issues/:id/documents/:key", validate(upsertIssueDocumentSchema), async (req, res) => {
|
|
1299
|
+
const id = req.params.id;
|
|
1300
|
+
const issue = await svc.getById(id);
|
|
1301
|
+
if (!issue) {
|
|
1302
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1306
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
1307
|
+
return;
|
|
1308
|
+
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
|
|
1309
|
+
if (!keyParsed.success) {
|
|
1310
|
+
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const actor = getActorInfo(req);
|
|
1314
|
+
const referenceSummaryBefore = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
1315
|
+
const result = await documentsSvc.upsertIssueDocument({
|
|
1316
|
+
issueId: issue.id,
|
|
1317
|
+
key: keyParsed.data,
|
|
1318
|
+
title: req.body.title ?? null,
|
|
1319
|
+
format: req.body.format,
|
|
1320
|
+
body: req.body.body,
|
|
1321
|
+
changeSummary: req.body.changeSummary ?? null,
|
|
1322
|
+
baseRevisionId: req.body.baseRevisionId ?? null,
|
|
1323
|
+
createdByAgentId: actor.agentId ?? null,
|
|
1324
|
+
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
1325
|
+
createdByRunId: actor.runId ?? null,
|
|
1326
|
+
});
|
|
1327
|
+
const doc = result.document;
|
|
1328
|
+
await issueReferencesSvc.syncDocument(doc.id);
|
|
1329
|
+
const referenceSummaryAfter = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
1330
|
+
const referenceDiff = issueReferencesSvc.diffIssueReferenceSummary(referenceSummaryBefore, referenceSummaryAfter);
|
|
1331
|
+
await logActivity(db, {
|
|
1332
|
+
companyId: issue.companyId,
|
|
1333
|
+
actorType: actor.actorType,
|
|
1334
|
+
actorId: actor.actorId,
|
|
1335
|
+
agentId: actor.agentId,
|
|
1336
|
+
runId: actor.runId,
|
|
1337
|
+
action: result.created ? "issue.document_created" : "issue.document_updated",
|
|
1338
|
+
entityType: "issue",
|
|
1339
|
+
entityId: issue.id,
|
|
1340
|
+
details: {
|
|
1341
|
+
key: doc.key,
|
|
1342
|
+
documentId: doc.id,
|
|
1343
|
+
title: doc.title,
|
|
1344
|
+
format: doc.format,
|
|
1345
|
+
revisionNumber: doc.latestRevisionNumber,
|
|
1346
|
+
...summarizeIssueReferenceActivityDetails({
|
|
1347
|
+
addedReferencedIssues: referenceDiff.addedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1348
|
+
removedReferencedIssues: referenceDiff.removedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1349
|
+
currentReferencedIssues: referenceDiff.currentReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1350
|
+
}),
|
|
1351
|
+
},
|
|
1352
|
+
});
|
|
1353
|
+
if (!result.created) {
|
|
1354
|
+
const expiredInteractions = await issueThreadInteractionService(db).expireStaleRequestConfirmationsForIssueDocument(issue, {
|
|
1355
|
+
id: doc.id,
|
|
1356
|
+
key: doc.key,
|
|
1357
|
+
latestRevisionId: doc.latestRevisionId,
|
|
1358
|
+
latestRevisionNumber: doc.latestRevisionNumber,
|
|
1359
|
+
}, {
|
|
1360
|
+
agentId: actor.agentId,
|
|
1361
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
1362
|
+
});
|
|
1363
|
+
await logExpiredRequestConfirmations({
|
|
1364
|
+
issue,
|
|
1365
|
+
interactions: expiredInteractions,
|
|
1366
|
+
actor,
|
|
1367
|
+
source: "issue.document_updated",
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
res.status(result.created ? 201 : 200).json(doc);
|
|
1371
|
+
});
|
|
1372
|
+
router.get("/issues/:id/documents/:key/revisions", async (req, res) => {
|
|
1373
|
+
const id = req.params.id;
|
|
1374
|
+
const issue = await svc.getById(id);
|
|
1375
|
+
if (!issue) {
|
|
1376
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1380
|
+
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
|
|
1381
|
+
if (!keyParsed.success) {
|
|
1382
|
+
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const revisions = await documentsSvc.listIssueDocumentRevisions(issue.id, keyParsed.data);
|
|
1386
|
+
res.json(revisions);
|
|
1387
|
+
});
|
|
1388
|
+
router.post("/issues/:id/documents/:key/revisions/:revisionId/restore", validate(restoreIssueDocumentRevisionSchema), async (req, res) => {
|
|
1389
|
+
const id = req.params.id;
|
|
1390
|
+
const revisionId = req.params.revisionId;
|
|
1391
|
+
const issue = await svc.getById(id);
|
|
1392
|
+
if (!issue) {
|
|
1393
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1397
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
1398
|
+
return;
|
|
1399
|
+
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
|
|
1400
|
+
if (!keyParsed.success) {
|
|
1401
|
+
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const actor = getActorInfo(req);
|
|
1405
|
+
const referenceSummaryBefore = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
1406
|
+
const result = await documentsSvc.restoreIssueDocumentRevision({
|
|
1407
|
+
issueId: issue.id,
|
|
1408
|
+
key: keyParsed.data,
|
|
1409
|
+
revisionId,
|
|
1410
|
+
createdByAgentId: actor.agentId ?? null,
|
|
1411
|
+
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
1412
|
+
});
|
|
1413
|
+
await issueReferencesSvc.syncDocument(result.document.id);
|
|
1414
|
+
const referenceSummaryAfter = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
1415
|
+
const referenceDiff = issueReferencesSvc.diffIssueReferenceSummary(referenceSummaryBefore, referenceSummaryAfter);
|
|
1416
|
+
await logActivity(db, {
|
|
1417
|
+
companyId: issue.companyId,
|
|
1418
|
+
actorType: actor.actorType,
|
|
1419
|
+
actorId: actor.actorId,
|
|
1420
|
+
agentId: actor.agentId,
|
|
1421
|
+
runId: actor.runId,
|
|
1422
|
+
action: "issue.document_restored",
|
|
1423
|
+
entityType: "issue",
|
|
1424
|
+
entityId: issue.id,
|
|
1425
|
+
details: {
|
|
1426
|
+
key: result.document.key,
|
|
1427
|
+
documentId: result.document.id,
|
|
1428
|
+
title: result.document.title,
|
|
1429
|
+
format: result.document.format,
|
|
1430
|
+
revisionNumber: result.document.latestRevisionNumber,
|
|
1431
|
+
restoredFromRevisionId: result.restoredFromRevisionId,
|
|
1432
|
+
restoredFromRevisionNumber: result.restoredFromRevisionNumber,
|
|
1433
|
+
...summarizeIssueReferenceActivityDetails({
|
|
1434
|
+
addedReferencedIssues: referenceDiff.addedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1435
|
+
removedReferencedIssues: referenceDiff.removedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1436
|
+
currentReferencedIssues: referenceDiff.currentReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1437
|
+
}),
|
|
1438
|
+
},
|
|
1439
|
+
});
|
|
1440
|
+
const expiredInteractions = await issueThreadInteractionService(db).expireStaleRequestConfirmationsForIssueDocument(issue, {
|
|
1441
|
+
id: result.document.id,
|
|
1442
|
+
key: result.document.key,
|
|
1443
|
+
latestRevisionId: result.document.latestRevisionId,
|
|
1444
|
+
latestRevisionNumber: result.document.latestRevisionNumber,
|
|
1445
|
+
}, {
|
|
1446
|
+
agentId: actor.agentId,
|
|
1447
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
1448
|
+
});
|
|
1449
|
+
await logExpiredRequestConfirmations({
|
|
1450
|
+
issue,
|
|
1451
|
+
interactions: expiredInteractions,
|
|
1452
|
+
actor,
|
|
1453
|
+
source: "issue.document_restored",
|
|
1454
|
+
});
|
|
1455
|
+
res.json(result.document);
|
|
1456
|
+
});
|
|
1457
|
+
router.delete("/issues/:id/documents/:key", async (req, res) => {
|
|
1458
|
+
const id = req.params.id;
|
|
1459
|
+
const issue = await svc.getById(id);
|
|
1460
|
+
if (!issue) {
|
|
1461
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1465
|
+
if (req.actor.type !== "board") {
|
|
1466
|
+
res.status(403).json({ error: "Board authentication required" });
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
|
|
1470
|
+
if (!keyParsed.success) {
|
|
1471
|
+
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
const referenceSummaryBefore = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
1475
|
+
const removed = await documentsSvc.deleteIssueDocument(issue.id, keyParsed.data);
|
|
1476
|
+
if (!removed) {
|
|
1477
|
+
res.status(404).json({ error: "Document not found" });
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
await issueReferencesSvc.deleteDocumentSource(removed.id);
|
|
1481
|
+
const referenceSummaryAfter = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
1482
|
+
const referenceDiff = issueReferencesSvc.diffIssueReferenceSummary(referenceSummaryBefore, referenceSummaryAfter);
|
|
1483
|
+
const actor = getActorInfo(req);
|
|
1484
|
+
await logActivity(db, {
|
|
1485
|
+
companyId: issue.companyId,
|
|
1486
|
+
actorType: actor.actorType,
|
|
1487
|
+
actorId: actor.actorId,
|
|
1488
|
+
agentId: actor.agentId,
|
|
1489
|
+
runId: actor.runId,
|
|
1490
|
+
action: "issue.document_deleted",
|
|
1491
|
+
entityType: "issue",
|
|
1492
|
+
entityId: issue.id,
|
|
1493
|
+
details: {
|
|
1494
|
+
key: removed.key,
|
|
1495
|
+
documentId: removed.id,
|
|
1496
|
+
title: removed.title,
|
|
1497
|
+
...summarizeIssueReferenceActivityDetails({
|
|
1498
|
+
addedReferencedIssues: referenceDiff.addedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1499
|
+
removedReferencedIssues: referenceDiff.removedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1500
|
+
currentReferencedIssues: referenceDiff.currentReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1501
|
+
}),
|
|
1502
|
+
},
|
|
1503
|
+
});
|
|
1504
|
+
const expiredInteractions = await issueThreadInteractionService(db).expireStaleRequestConfirmationsForIssueDocument(issue, {
|
|
1505
|
+
id: removed.id,
|
|
1506
|
+
key: removed.key,
|
|
1507
|
+
latestRevisionId: null,
|
|
1508
|
+
latestRevisionNumber: null,
|
|
1509
|
+
}, {
|
|
1510
|
+
agentId: actor.agentId,
|
|
1511
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
1512
|
+
});
|
|
1513
|
+
await logExpiredRequestConfirmations({
|
|
1514
|
+
issue,
|
|
1515
|
+
interactions: expiredInteractions,
|
|
1516
|
+
actor,
|
|
1517
|
+
source: "issue.document_deleted",
|
|
1518
|
+
});
|
|
1519
|
+
res.json({ ok: true });
|
|
1520
|
+
});
|
|
1521
|
+
router.post("/issues/:id/work-products", validate(createIssueWorkProductSchema), async (req, res) => {
|
|
1522
|
+
const id = req.params.id;
|
|
1523
|
+
const issue = await svc.getById(id);
|
|
1524
|
+
if (!issue) {
|
|
1525
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1529
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
1530
|
+
return;
|
|
1531
|
+
const product = await workProductsSvc.createForIssue(issue.id, issue.companyId, {
|
|
1532
|
+
...req.body,
|
|
1533
|
+
projectId: req.body.projectId ?? issue.projectId ?? null,
|
|
1534
|
+
});
|
|
1535
|
+
if (!product) {
|
|
1536
|
+
res.status(422).json({ error: "Invalid work product payload" });
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const actor = getActorInfo(req);
|
|
1540
|
+
await logActivity(db, {
|
|
1541
|
+
companyId: issue.companyId,
|
|
1542
|
+
actorType: actor.actorType,
|
|
1543
|
+
actorId: actor.actorId,
|
|
1544
|
+
agentId: actor.agentId,
|
|
1545
|
+
runId: actor.runId,
|
|
1546
|
+
action: "issue.work_product_created",
|
|
1547
|
+
entityType: "issue",
|
|
1548
|
+
entityId: issue.id,
|
|
1549
|
+
details: { workProductId: product.id, type: product.type, provider: product.provider },
|
|
1550
|
+
});
|
|
1551
|
+
res.status(201).json(product);
|
|
1552
|
+
});
|
|
1553
|
+
router.patch("/work-products/:id", validate(updateIssueWorkProductSchema), async (req, res) => {
|
|
1554
|
+
const id = req.params.id;
|
|
1555
|
+
const existing = await workProductsSvc.getById(id);
|
|
1556
|
+
if (!existing) {
|
|
1557
|
+
res.status(404).json({ error: "Work product not found" });
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
assertCompanyAccess(req, existing.companyId);
|
|
1561
|
+
const issue = await svc.getById(existing.issueId);
|
|
1562
|
+
if (!issue) {
|
|
1563
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
1567
|
+
return;
|
|
1568
|
+
const product = await workProductsSvc.update(id, req.body);
|
|
1569
|
+
if (!product) {
|
|
1570
|
+
res.status(404).json({ error: "Work product not found" });
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const actor = getActorInfo(req);
|
|
1574
|
+
await logActivity(db, {
|
|
1575
|
+
companyId: existing.companyId,
|
|
1576
|
+
actorType: actor.actorType,
|
|
1577
|
+
actorId: actor.actorId,
|
|
1578
|
+
agentId: actor.agentId,
|
|
1579
|
+
runId: actor.runId,
|
|
1580
|
+
action: "issue.work_product_updated",
|
|
1581
|
+
entityType: "issue",
|
|
1582
|
+
entityId: existing.issueId,
|
|
1583
|
+
details: { workProductId: product.id, changedKeys: Object.keys(req.body).sort() },
|
|
1584
|
+
});
|
|
1585
|
+
res.json(product);
|
|
1586
|
+
});
|
|
1587
|
+
router.delete("/work-products/:id", async (req, res) => {
|
|
1588
|
+
const id = req.params.id;
|
|
1589
|
+
const existing = await workProductsSvc.getById(id);
|
|
1590
|
+
if (!existing) {
|
|
1591
|
+
res.status(404).json({ error: "Work product not found" });
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
assertCompanyAccess(req, existing.companyId);
|
|
1595
|
+
const issue = await svc.getById(existing.issueId);
|
|
1596
|
+
if (!issue) {
|
|
1597
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
1601
|
+
return;
|
|
1602
|
+
const removed = await workProductsSvc.remove(id);
|
|
1603
|
+
if (!removed) {
|
|
1604
|
+
res.status(404).json({ error: "Work product not found" });
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const actor = getActorInfo(req);
|
|
1608
|
+
await logActivity(db, {
|
|
1609
|
+
companyId: existing.companyId,
|
|
1610
|
+
actorType: actor.actorType,
|
|
1611
|
+
actorId: actor.actorId,
|
|
1612
|
+
agentId: actor.agentId,
|
|
1613
|
+
runId: actor.runId,
|
|
1614
|
+
action: "issue.work_product_deleted",
|
|
1615
|
+
entityType: "issue",
|
|
1616
|
+
entityId: existing.issueId,
|
|
1617
|
+
details: { workProductId: removed.id, type: removed.type },
|
|
1618
|
+
});
|
|
1619
|
+
res.json(removed);
|
|
1620
|
+
});
|
|
1621
|
+
router.post("/issues/:id/read", async (req, res) => {
|
|
1622
|
+
const id = req.params.id;
|
|
1623
|
+
const issue = await svc.getById(id);
|
|
1624
|
+
if (!issue) {
|
|
1625
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1629
|
+
if (req.actor.type !== "board") {
|
|
1630
|
+
res.status(403).json({ error: "Board authentication required" });
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (!req.actor.userId) {
|
|
1634
|
+
res.status(403).json({ error: "Board user context required" });
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const readState = await svc.markRead(issue.companyId, issue.id, req.actor.userId, new Date());
|
|
1638
|
+
const actor = getActorInfo(req);
|
|
1639
|
+
await logActivity(db, {
|
|
1640
|
+
companyId: issue.companyId,
|
|
1641
|
+
actorType: actor.actorType,
|
|
1642
|
+
actorId: actor.actorId,
|
|
1643
|
+
agentId: actor.agentId,
|
|
1644
|
+
runId: actor.runId,
|
|
1645
|
+
action: "issue.read_marked",
|
|
1646
|
+
entityType: "issue",
|
|
1647
|
+
entityId: issue.id,
|
|
1648
|
+
details: { userId: req.actor.userId, lastReadAt: readState.lastReadAt },
|
|
1649
|
+
});
|
|
1650
|
+
res.json(readState);
|
|
1651
|
+
});
|
|
1652
|
+
router.delete("/issues/:id/read", async (req, res) => {
|
|
1653
|
+
const id = req.params.id;
|
|
1654
|
+
const issue = await svc.getById(id);
|
|
1655
|
+
if (!issue) {
|
|
1656
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1660
|
+
if (req.actor.type !== "board") {
|
|
1661
|
+
res.status(403).json({ error: "Board authentication required" });
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
if (!req.actor.userId) {
|
|
1665
|
+
res.status(403).json({ error: "Board user context required" });
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const removed = await svc.markUnread(issue.companyId, issue.id, req.actor.userId);
|
|
1669
|
+
const actor = getActorInfo(req);
|
|
1670
|
+
await logActivity(db, {
|
|
1671
|
+
companyId: issue.companyId,
|
|
1672
|
+
actorType: actor.actorType,
|
|
1673
|
+
actorId: actor.actorId,
|
|
1674
|
+
agentId: actor.agentId,
|
|
1675
|
+
runId: actor.runId,
|
|
1676
|
+
action: "issue.read_unmarked",
|
|
1677
|
+
entityType: "issue",
|
|
1678
|
+
entityId: issue.id,
|
|
1679
|
+
details: { userId: req.actor.userId },
|
|
1680
|
+
});
|
|
1681
|
+
res.json({ id: issue.id, removed });
|
|
1682
|
+
});
|
|
1683
|
+
router.post("/issues/:id/inbox-archive", async (req, res) => {
|
|
1684
|
+
const id = req.params.id;
|
|
1685
|
+
const issue = await svc.getById(id);
|
|
1686
|
+
if (!issue) {
|
|
1687
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1691
|
+
if (req.actor.type !== "board") {
|
|
1692
|
+
res.status(403).json({ error: "Board authentication required" });
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (!req.actor.userId) {
|
|
1696
|
+
res.status(403).json({ error: "Board user context required" });
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const archiveState = await svc.archiveInbox(issue.companyId, issue.id, req.actor.userId, new Date());
|
|
1700
|
+
const actor = getActorInfo(req);
|
|
1701
|
+
await logActivity(db, {
|
|
1702
|
+
companyId: issue.companyId,
|
|
1703
|
+
actorType: actor.actorType,
|
|
1704
|
+
actorId: actor.actorId,
|
|
1705
|
+
agentId: actor.agentId,
|
|
1706
|
+
runId: actor.runId,
|
|
1707
|
+
action: "issue.inbox_archived",
|
|
1708
|
+
entityType: "issue",
|
|
1709
|
+
entityId: issue.id,
|
|
1710
|
+
details: { userId: req.actor.userId, archivedAt: archiveState.archivedAt },
|
|
1711
|
+
});
|
|
1712
|
+
res.json(archiveState);
|
|
1713
|
+
});
|
|
1714
|
+
router.delete("/issues/:id/inbox-archive", async (req, res) => {
|
|
1715
|
+
const id = req.params.id;
|
|
1716
|
+
const issue = await svc.getById(id);
|
|
1717
|
+
if (!issue) {
|
|
1718
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1722
|
+
if (req.actor.type !== "board") {
|
|
1723
|
+
res.status(403).json({ error: "Board authentication required" });
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (!req.actor.userId) {
|
|
1727
|
+
res.status(403).json({ error: "Board user context required" });
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const removed = await svc.unarchiveInbox(issue.companyId, issue.id, req.actor.userId);
|
|
1731
|
+
const actor = getActorInfo(req);
|
|
1732
|
+
await logActivity(db, {
|
|
1733
|
+
companyId: issue.companyId,
|
|
1734
|
+
actorType: actor.actorType,
|
|
1735
|
+
actorId: actor.actorId,
|
|
1736
|
+
agentId: actor.agentId,
|
|
1737
|
+
runId: actor.runId,
|
|
1738
|
+
action: "issue.inbox_unarchived",
|
|
1739
|
+
entityType: "issue",
|
|
1740
|
+
entityId: issue.id,
|
|
1741
|
+
details: { userId: req.actor.userId },
|
|
1742
|
+
});
|
|
1743
|
+
res.json(removed ?? { ok: true });
|
|
1744
|
+
});
|
|
1745
|
+
router.get("/issues/:id/approvals", async (req, res) => {
|
|
1746
|
+
const id = req.params.id;
|
|
1747
|
+
const issue = await svc.getById(id);
|
|
1748
|
+
if (!issue) {
|
|
1749
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1753
|
+
const approvals = await issueApprovalsSvc.listApprovalsForIssue(id);
|
|
1754
|
+
res.json(approvals);
|
|
1755
|
+
});
|
|
1756
|
+
router.post("/issues/:id/approvals", validate(linkIssueApprovalSchema), async (req, res) => {
|
|
1757
|
+
const id = req.params.id;
|
|
1758
|
+
const issue = await svc.getById(id);
|
|
1759
|
+
if (!issue) {
|
|
1760
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1764
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
1765
|
+
return;
|
|
1766
|
+
if (!(await assertCanManageIssueApprovalLinks(req, res, issue.companyId)))
|
|
1767
|
+
return;
|
|
1768
|
+
const actor = getActorInfo(req);
|
|
1769
|
+
await issueApprovalsSvc.link(id, req.body.approvalId, {
|
|
1770
|
+
agentId: actor.agentId,
|
|
1771
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
1772
|
+
});
|
|
1773
|
+
await logActivity(db, {
|
|
1774
|
+
companyId: issue.companyId,
|
|
1775
|
+
actorType: actor.actorType,
|
|
1776
|
+
actorId: actor.actorId,
|
|
1777
|
+
agentId: actor.agentId,
|
|
1778
|
+
runId: actor.runId,
|
|
1779
|
+
action: "issue.approval_linked",
|
|
1780
|
+
entityType: "issue",
|
|
1781
|
+
entityId: issue.id,
|
|
1782
|
+
details: { approvalId: req.body.approvalId },
|
|
1783
|
+
});
|
|
1784
|
+
const approvals = await issueApprovalsSvc.listApprovalsForIssue(id);
|
|
1785
|
+
res.status(201).json(approvals);
|
|
1786
|
+
});
|
|
1787
|
+
router.delete("/issues/:id/approvals/:approvalId", async (req, res) => {
|
|
1788
|
+
const id = req.params.id;
|
|
1789
|
+
const approvalId = req.params.approvalId;
|
|
1790
|
+
const issue = await svc.getById(id);
|
|
1791
|
+
if (!issue) {
|
|
1792
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1796
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
1797
|
+
return;
|
|
1798
|
+
if (!(await assertCanManageIssueApprovalLinks(req, res, issue.companyId)))
|
|
1799
|
+
return;
|
|
1800
|
+
await issueApprovalsSvc.unlink(id, approvalId);
|
|
1801
|
+
const actor = getActorInfo(req);
|
|
1802
|
+
await logActivity(db, {
|
|
1803
|
+
companyId: issue.companyId,
|
|
1804
|
+
actorType: actor.actorType,
|
|
1805
|
+
actorId: actor.actorId,
|
|
1806
|
+
agentId: actor.agentId,
|
|
1807
|
+
runId: actor.runId,
|
|
1808
|
+
action: "issue.approval_unlinked",
|
|
1809
|
+
entityType: "issue",
|
|
1810
|
+
entityId: issue.id,
|
|
1811
|
+
details: { approvalId },
|
|
1812
|
+
});
|
|
1813
|
+
res.json({ ok: true });
|
|
1814
|
+
});
|
|
1815
|
+
router.post("/companies/:companyId/issues", validate(createIssueSchema), async (req, res) => {
|
|
1816
|
+
const companyId = req.params.companyId;
|
|
1817
|
+
assertCompanyAccess(req, companyId);
|
|
1818
|
+
assertNoAgentHostWorkspaceCommandMutation(req, collectIssueWorkspaceCommandPaths(req.body));
|
|
1819
|
+
if (req.body.assigneeAgentId || req.body.assigneeUserId) {
|
|
1820
|
+
await assertCanAssignTasks(req, companyId);
|
|
1821
|
+
}
|
|
1822
|
+
await assertIssueEnvironmentSelection(companyId, req.body.executionWorkspaceSettings?.environmentId);
|
|
1823
|
+
const actor = getActorInfo(req);
|
|
1824
|
+
const executionPolicy = applyActorMonitorScheduledBy(normalizeIssueExecutionPolicy(req.body.executionPolicy), actor.actorType);
|
|
1825
|
+
assertCanManageIssueMonitor(req, req.body.assigneeAgentId ?? null, Boolean(executionPolicy?.monitor));
|
|
1826
|
+
const issue = await svc.create(companyId, {
|
|
1827
|
+
...req.body,
|
|
1828
|
+
executionPolicy,
|
|
1829
|
+
createdByAgentId: actor.agentId,
|
|
1830
|
+
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
1831
|
+
});
|
|
1832
|
+
await issueReferencesSvc.syncIssue(issue.id);
|
|
1833
|
+
const referenceSummary = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
1834
|
+
const referenceDiff = issueReferencesSvc.diffIssueReferenceSummary(issueReferencesSvc.emptySummary(), referenceSummary);
|
|
1835
|
+
await logActivity(db, {
|
|
1836
|
+
companyId,
|
|
1837
|
+
actorType: actor.actorType,
|
|
1838
|
+
actorId: actor.actorId,
|
|
1839
|
+
agentId: actor.agentId,
|
|
1840
|
+
runId: actor.runId,
|
|
1841
|
+
action: "issue.created",
|
|
1842
|
+
entityType: "issue",
|
|
1843
|
+
entityId: issue.id,
|
|
1844
|
+
details: {
|
|
1845
|
+
title: issue.title,
|
|
1846
|
+
identifier: issue.identifier,
|
|
1847
|
+
...(Array.isArray(req.body.blockedByIssueIds) ? { blockedByIssueIds: req.body.blockedByIssueIds } : {}),
|
|
1848
|
+
...summarizeIssueReferenceActivityDetails({
|
|
1849
|
+
addedReferencedIssues: referenceDiff.addedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1850
|
+
removedReferencedIssues: referenceDiff.removedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1851
|
+
currentReferencedIssues: referenceDiff.currentReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
1852
|
+
}),
|
|
1853
|
+
},
|
|
1854
|
+
});
|
|
1855
|
+
if (executionPolicy?.monitor) {
|
|
1856
|
+
await logActivity(db, {
|
|
1857
|
+
companyId,
|
|
1858
|
+
actorType: actor.actorType,
|
|
1859
|
+
actorId: actor.actorId,
|
|
1860
|
+
agentId: actor.agentId,
|
|
1861
|
+
runId: actor.runId,
|
|
1862
|
+
action: "issue.monitor_scheduled",
|
|
1863
|
+
entityType: "issue",
|
|
1864
|
+
entityId: issue.id,
|
|
1865
|
+
details: {
|
|
1866
|
+
identifier: issue.identifier,
|
|
1867
|
+
nextCheckAt: executionPolicy.monitor.nextCheckAt,
|
|
1868
|
+
notes: executionPolicy.monitor.notes,
|
|
1869
|
+
scheduledBy: executionPolicy.monitor.scheduledBy,
|
|
1870
|
+
serviceName: executionPolicy.monitor.serviceName ?? null,
|
|
1871
|
+
timeoutAt: executionPolicy.monitor.timeoutAt ?? null,
|
|
1872
|
+
maxAttempts: executionPolicy.monitor.maxAttempts ?? null,
|
|
1873
|
+
recoveryPolicy: executionPolicy.monitor.recoveryPolicy ?? null,
|
|
1874
|
+
},
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
void queueIssueAssignmentWakeup({
|
|
1878
|
+
heartbeat,
|
|
1879
|
+
issue,
|
|
1880
|
+
reason: "issue_assigned",
|
|
1881
|
+
mutation: "create",
|
|
1882
|
+
contextSource: "issue.create",
|
|
1883
|
+
requestedByActorType: actor.actorType,
|
|
1884
|
+
requestedByActorId: actor.actorId,
|
|
1885
|
+
});
|
|
1886
|
+
res.status(201).json({
|
|
1887
|
+
...issue,
|
|
1888
|
+
relatedWork: referenceSummary,
|
|
1889
|
+
referencedIssueIdentifiers: referenceSummary.outbound.map((item) => item.issue.identifier ?? item.issue.id),
|
|
1890
|
+
});
|
|
1891
|
+
});
|
|
1892
|
+
router.post("/issues/:id/children", validate(createChildIssueSchema), async (req, res) => {
|
|
1893
|
+
const parentId = req.params.id;
|
|
1894
|
+
const parent = await svc.getById(parentId);
|
|
1895
|
+
if (!parent) {
|
|
1896
|
+
res.status(404).json({ error: "Parent issue not found" });
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
assertCompanyAccess(req, parent.companyId);
|
|
1900
|
+
assertNoAgentHostWorkspaceCommandMutation(req, collectIssueWorkspaceCommandPaths(req.body));
|
|
1901
|
+
if (req.body.assigneeAgentId || req.body.assigneeUserId) {
|
|
1902
|
+
await assertCanAssignTasks(req, parent.companyId);
|
|
1903
|
+
}
|
|
1904
|
+
await assertIssueEnvironmentSelection(parent.companyId, req.body.executionWorkspaceSettings?.environmentId);
|
|
1905
|
+
const actor = getActorInfo(req);
|
|
1906
|
+
const executionPolicy = applyActorMonitorScheduledBy(normalizeIssueExecutionPolicy(req.body.executionPolicy), actor.actorType);
|
|
1907
|
+
assertCanManageIssueMonitor(req, req.body.assigneeAgentId ?? null, Boolean(executionPolicy?.monitor));
|
|
1908
|
+
const { issue, parentBlockerAdded } = await svc.createChild(parent.id, {
|
|
1909
|
+
...req.body,
|
|
1910
|
+
executionPolicy,
|
|
1911
|
+
createdByAgentId: actor.agentId,
|
|
1912
|
+
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
1913
|
+
actorAgentId: actor.agentId,
|
|
1914
|
+
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
1915
|
+
});
|
|
1916
|
+
await logActivity(db, {
|
|
1917
|
+
companyId: parent.companyId,
|
|
1918
|
+
actorType: actor.actorType,
|
|
1919
|
+
actorId: actor.actorId,
|
|
1920
|
+
agentId: actor.agentId,
|
|
1921
|
+
runId: actor.runId,
|
|
1922
|
+
action: "issue.child_created",
|
|
1923
|
+
entityType: "issue",
|
|
1924
|
+
entityId: issue.id,
|
|
1925
|
+
details: {
|
|
1926
|
+
parentId: parent.id,
|
|
1927
|
+
identifier: issue.identifier,
|
|
1928
|
+
title: issue.title,
|
|
1929
|
+
inheritedExecutionWorkspaceFromIssueId: parent.id,
|
|
1930
|
+
...(Array.isArray(req.body.blockedByIssueIds) ? { blockedByIssueIds: req.body.blockedByIssueIds } : {}),
|
|
1931
|
+
...(parentBlockerAdded ? { parentBlockerAdded: true } : {}),
|
|
1932
|
+
},
|
|
1933
|
+
});
|
|
1934
|
+
if (executionPolicy?.monitor) {
|
|
1935
|
+
await logActivity(db, {
|
|
1936
|
+
companyId: parent.companyId,
|
|
1937
|
+
actorType: actor.actorType,
|
|
1938
|
+
actorId: actor.actorId,
|
|
1939
|
+
agentId: actor.agentId,
|
|
1940
|
+
runId: actor.runId,
|
|
1941
|
+
action: "issue.monitor_scheduled",
|
|
1942
|
+
entityType: "issue",
|
|
1943
|
+
entityId: issue.id,
|
|
1944
|
+
details: {
|
|
1945
|
+
identifier: issue.identifier,
|
|
1946
|
+
parentId: parent.id,
|
|
1947
|
+
nextCheckAt: executionPolicy.monitor.nextCheckAt,
|
|
1948
|
+
notes: executionPolicy.monitor.notes,
|
|
1949
|
+
scheduledBy: executionPolicy.monitor.scheduledBy,
|
|
1950
|
+
serviceName: executionPolicy.monitor.serviceName ?? null,
|
|
1951
|
+
timeoutAt: executionPolicy.monitor.timeoutAt ?? null,
|
|
1952
|
+
maxAttempts: executionPolicy.monitor.maxAttempts ?? null,
|
|
1953
|
+
recoveryPolicy: executionPolicy.monitor.recoveryPolicy ?? null,
|
|
1954
|
+
},
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
void queueIssueAssignmentWakeup({
|
|
1958
|
+
heartbeat,
|
|
1959
|
+
issue,
|
|
1960
|
+
reason: "issue_assigned",
|
|
1961
|
+
mutation: "create",
|
|
1962
|
+
contextSource: "issue.child_create",
|
|
1963
|
+
requestedByActorType: actor.actorType,
|
|
1964
|
+
requestedByActorId: actor.actorId,
|
|
1965
|
+
});
|
|
1966
|
+
res.status(201).json(issue);
|
|
1967
|
+
});
|
|
1968
|
+
router.post("/issues/:id/monitor/check-now", async (req, res) => {
|
|
1969
|
+
const id = req.params.id;
|
|
1970
|
+
const issue = await svc.getById(id);
|
|
1971
|
+
if (!issue) {
|
|
1972
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
assertCompanyAccess(req, issue.companyId);
|
|
1976
|
+
assertCanManageIssueMonitor(req, issue.assigneeAgentId, true);
|
|
1977
|
+
const actor = getActorInfo(req);
|
|
1978
|
+
await heartbeat.triggerIssueMonitor(issue.id, {
|
|
1979
|
+
actorType: actor.actorType,
|
|
1980
|
+
actorId: actor.actorId,
|
|
1981
|
+
agentId: actor.agentId ?? null,
|
|
1982
|
+
runId: actor.runId ?? null,
|
|
1983
|
+
});
|
|
1984
|
+
res.json({ ok: true });
|
|
1985
|
+
});
|
|
1986
|
+
router.patch("/issues/:id", validate(updateIssueRouteSchema), async (req, res) => {
|
|
1987
|
+
const id = req.params.id;
|
|
1988
|
+
const existing = await svc.getById(id);
|
|
1989
|
+
if (!existing) {
|
|
1990
|
+
res.status(404).json({ error: "Issue not found" });
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
assertCompanyAccess(req, existing.companyId);
|
|
1994
|
+
assertNoAgentHostWorkspaceCommandMutation(req, collectIssueWorkspaceCommandPaths(req.body));
|
|
1995
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, existing)))
|
|
1996
|
+
return;
|
|
1997
|
+
const actor = getActorInfo(req);
|
|
1998
|
+
const isClosed = isClosedIssueStatus(existing.status);
|
|
1999
|
+
const isBlocked = existing.status === "blocked";
|
|
2000
|
+
const normalizedAssigneeAgentId = await normalizeIssueAssigneeAgentReference(existing.companyId, req.body.assigneeAgentId);
|
|
2001
|
+
const titleOrDescriptionChanged = req.body.title !== undefined || req.body.description !== undefined;
|
|
2002
|
+
const existingRelations = Array.isArray(req.body.blockedByIssueIds)
|
|
2003
|
+
? await svc.getRelationSummaries(existing.id)
|
|
2004
|
+
: null;
|
|
2005
|
+
const { comment: commentBody, reviewRequest, reopen: reopenRequested, resume: resumeRequested, interrupt: interruptRequested, hiddenAt: hiddenAtRaw, ...updateFields } = req.body;
|
|
2006
|
+
const shouldCancelActiveRunForCancelledStatus = existing.status !== "cancelled" && updateFields.status === "cancelled";
|
|
2007
|
+
if (resumeRequested === true && !commentBody) {
|
|
2008
|
+
res.status(400).json({ error: "Follow-up intent requires a comment" });
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (resumeRequested === true && !(await assertExplicitResumeIntentAllowed(req, res, existing)))
|
|
2012
|
+
return;
|
|
2013
|
+
if (resumeRequested !== true && reopenRequested === true && req.actor.type === "agent") {
|
|
2014
|
+
if (!(await assertExplicitResumeIntentAllowed(req, res, existing)))
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
await assertIssueEnvironmentSelection(existing.companyId, updateFields.executionWorkspaceSettings?.environmentId);
|
|
2018
|
+
const requestedAssigneeAgentId = normalizedAssigneeAgentId === undefined ? existing.assigneeAgentId : normalizedAssigneeAgentId;
|
|
2019
|
+
const explicitMoveToTodoRequested = reopenRequested || resumeRequested === true;
|
|
2020
|
+
const effectiveMoveToTodoRequested = explicitMoveToTodoRequested ||
|
|
2021
|
+
(!!commentBody &&
|
|
2022
|
+
shouldImplicitlyMoveCommentedIssueToTodo({
|
|
2023
|
+
issueStatus: existing.status,
|
|
2024
|
+
assigneeAgentId: requestedAssigneeAgentId,
|
|
2025
|
+
actorType: actor.actorType,
|
|
2026
|
+
actorId: actor.actorId,
|
|
2027
|
+
}));
|
|
2028
|
+
const updateReferenceSummaryBefore = titleOrDescriptionChanged
|
|
2029
|
+
? await issueReferencesSvc.listIssueReferenceSummary(existing.id)
|
|
2030
|
+
: null;
|
|
2031
|
+
const hasUnresolvedFirstClassBlockers = isBlocked && effectiveMoveToTodoRequested
|
|
2032
|
+
? (await svc.getDependencyReadiness(existing.id)).unresolvedBlockerCount > 0
|
|
2033
|
+
: false;
|
|
2034
|
+
if (resumeRequested === true && isBlocked && hasUnresolvedFirstClassBlockers) {
|
|
2035
|
+
res.status(409).json({ error: "Issue follow-up blocked by unresolved blockers" });
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
let interruptedRunId = null;
|
|
2039
|
+
const closedExecutionWorkspace = await getClosedIssueExecutionWorkspace(existing);
|
|
2040
|
+
const isAgentWorkUpdate = req.actor.type === "agent" && (Object.keys(updateFields).length > 0 || reviewRequest !== undefined);
|
|
2041
|
+
if (closedExecutionWorkspace && (commentBody || isAgentWorkUpdate)) {
|
|
2042
|
+
respondClosedIssueExecutionWorkspace(res, closedExecutionWorkspace);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
if (interruptRequested) {
|
|
2046
|
+
if (!commentBody) {
|
|
2047
|
+
res.status(400).json({ error: "Interrupt is only supported when posting a comment" });
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (req.actor.type !== "board") {
|
|
2051
|
+
res.status(403).json({ error: "Only board users can interrupt active runs from issue comments" });
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
const runToInterrupt = await resolveActiveIssueRun(existing);
|
|
2055
|
+
if (runToInterrupt) {
|
|
2056
|
+
const cancelled = await heartbeat.cancelRun(runToInterrupt.id);
|
|
2057
|
+
if (cancelled) {
|
|
2058
|
+
interruptedRunId = cancelled.id;
|
|
2059
|
+
await logActivity(db, {
|
|
2060
|
+
companyId: cancelled.companyId,
|
|
2061
|
+
actorType: actor.actorType,
|
|
2062
|
+
actorId: actor.actorId,
|
|
2063
|
+
agentId: actor.agentId,
|
|
2064
|
+
runId: actor.runId,
|
|
2065
|
+
action: "heartbeat.cancelled",
|
|
2066
|
+
entityType: "heartbeat_run",
|
|
2067
|
+
entityId: cancelled.id,
|
|
2068
|
+
details: { agentId: cancelled.agentId, source: "issue_comment_interrupt", issueId: existing.id },
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
const runToCancelForCancelledStatus = shouldCancelActiveRunForCancelledStatus
|
|
2074
|
+
? await resolveActiveIssueRun(existing)
|
|
2075
|
+
: null;
|
|
2076
|
+
if (hiddenAtRaw !== undefined) {
|
|
2077
|
+
updateFields.hiddenAt = hiddenAtRaw ? new Date(hiddenAtRaw) : null;
|
|
2078
|
+
}
|
|
2079
|
+
if (commentBody &&
|
|
2080
|
+
effectiveMoveToTodoRequested &&
|
|
2081
|
+
(isClosed || (isBlocked && !hasUnresolvedFirstClassBlockers)) &&
|
|
2082
|
+
updateFields.status === undefined) {
|
|
2083
|
+
updateFields.status = "todo";
|
|
2084
|
+
}
|
|
2085
|
+
if (req.body.executionPolicy !== undefined) {
|
|
2086
|
+
updateFields.executionPolicy = applyActorMonitorScheduledBy(normalizeIssueExecutionPolicy(req.body.executionPolicy), actor.actorType);
|
|
2087
|
+
}
|
|
2088
|
+
const previousExecutionPolicy = normalizeIssueExecutionPolicy(existing.executionPolicy ?? null);
|
|
2089
|
+
const nextExecutionPolicy = updateFields.executionPolicy !== undefined
|
|
2090
|
+
? updateFields.executionPolicy
|
|
2091
|
+
: previousExecutionPolicy;
|
|
2092
|
+
if (normalizedAssigneeAgentId !== undefined) {
|
|
2093
|
+
updateFields.assigneeAgentId = normalizedAssigneeAgentId;
|
|
2094
|
+
}
|
|
2095
|
+
const monitorChanged = monitorPoliciesEqual(previousExecutionPolicy, nextExecutionPolicy) === false;
|
|
2096
|
+
assertCanManageIssueMonitor(req, existing.assigneeAgentId, req.body.executionPolicy !== undefined && monitorChanged);
|
|
2097
|
+
const transition = applyIssueExecutionPolicyTransition({
|
|
2098
|
+
issue: existing,
|
|
2099
|
+
policy: nextExecutionPolicy,
|
|
2100
|
+
previousPolicy: previousExecutionPolicy,
|
|
2101
|
+
requestedStatus: typeof updateFields.status === "string" ? updateFields.status : undefined,
|
|
2102
|
+
requestedAssigneePatch: {
|
|
2103
|
+
assigneeAgentId: normalizedAssigneeAgentId,
|
|
2104
|
+
assigneeUserId: req.body.assigneeUserId === undefined ? undefined : req.body.assigneeUserId,
|
|
2105
|
+
},
|
|
2106
|
+
actor: {
|
|
2107
|
+
agentId: actor.agentId ?? null,
|
|
2108
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
2109
|
+
},
|
|
2110
|
+
commentBody,
|
|
2111
|
+
reviewRequest: reviewRequest === undefined ? undefined : reviewRequest,
|
|
2112
|
+
monitorExplicitlyUpdated: req.body.executionPolicy !== undefined && monitorChanged,
|
|
2113
|
+
});
|
|
2114
|
+
const decisionId = transition.decision ? randomUUID() : null;
|
|
2115
|
+
if (decisionId) {
|
|
2116
|
+
const nextExecutionState = transition.patch.executionState;
|
|
2117
|
+
if (!nextExecutionState || typeof nextExecutionState !== "object") {
|
|
2118
|
+
throw new Error("Execution policy decision patch is missing executionState");
|
|
2119
|
+
}
|
|
2120
|
+
transition.patch.executionState = {
|
|
2121
|
+
...nextExecutionState,
|
|
2122
|
+
lastDecisionId: decisionId,
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
Object.assign(updateFields, transition.patch);
|
|
2126
|
+
if (reviewRequest !== undefined && transition.patch.executionState === undefined) {
|
|
2127
|
+
const existingExecutionState = parseIssueExecutionState(existing.executionState);
|
|
2128
|
+
if (!existingExecutionState || existingExecutionState.status !== "pending") {
|
|
2129
|
+
if (reviewRequest !== null) {
|
|
2130
|
+
res.status(422).json({ error: "reviewRequest requires an active review or approval stage" });
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
updateFields.executionState = {
|
|
2136
|
+
...existingExecutionState,
|
|
2137
|
+
reviewRequest,
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
await assertAgentInReviewReviewPath({
|
|
2142
|
+
existing,
|
|
2143
|
+
updateFields,
|
|
2144
|
+
actorType: req.actor.type,
|
|
2145
|
+
});
|
|
2146
|
+
const nextAssigneeAgentId = updateFields.assigneeAgentId === undefined ? existing.assigneeAgentId : updateFields.assigneeAgentId;
|
|
2147
|
+
const nextAssigneeUserId = updateFields.assigneeUserId === undefined ? existing.assigneeUserId : updateFields.assigneeUserId;
|
|
2148
|
+
const assigneeWillChange = nextAssigneeAgentId !== existing.assigneeAgentId || nextAssigneeUserId !== existing.assigneeUserId;
|
|
2149
|
+
const isAgentReturningIssueToCreator = req.actor.type === "agent" &&
|
|
2150
|
+
!!req.actor.agentId &&
|
|
2151
|
+
existing.assigneeAgentId === req.actor.agentId &&
|
|
2152
|
+
nextAssigneeAgentId === null &&
|
|
2153
|
+
typeof nextAssigneeUserId === "string" &&
|
|
2154
|
+
!!existing.createdByUserId &&
|
|
2155
|
+
nextAssigneeUserId === existing.createdByUserId;
|
|
2156
|
+
if (assigneeWillChange && !transition.workflowControlledAssignment) {
|
|
2157
|
+
if (!isAgentReturningIssueToCreator) {
|
|
2158
|
+
await assertCanAssignTasks(req, existing.companyId);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
let issue;
|
|
2162
|
+
try {
|
|
2163
|
+
if (transition.decision && decisionId) {
|
|
2164
|
+
const decision = transition.decision;
|
|
2165
|
+
issue = await db.transaction(async (tx) => {
|
|
2166
|
+
const updated = await svc.update(id, {
|
|
2167
|
+
...updateFields,
|
|
2168
|
+
actorAgentId: actor.agentId ?? null,
|
|
2169
|
+
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
2170
|
+
}, tx);
|
|
2171
|
+
if (!updated)
|
|
2172
|
+
return null;
|
|
2173
|
+
await tx.insert(issueExecutionDecisions).values({
|
|
2174
|
+
id: decisionId,
|
|
2175
|
+
companyId: updated.companyId,
|
|
2176
|
+
issueId: updated.id,
|
|
2177
|
+
stageId: decision.stageId,
|
|
2178
|
+
stageType: decision.stageType,
|
|
2179
|
+
actorAgentId: actor.agentId ?? null,
|
|
2180
|
+
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
2181
|
+
outcome: decision.outcome,
|
|
2182
|
+
body: decision.body,
|
|
2183
|
+
createdByRunId: actor.runId ?? null,
|
|
2184
|
+
});
|
|
2185
|
+
return updated;
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
else {
|
|
2189
|
+
issue = await svc.update(id, {
|
|
2190
|
+
...updateFields,
|
|
2191
|
+
actorAgentId: actor.agentId ?? null,
|
|
2192
|
+
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
catch (err) {
|
|
2197
|
+
if (err instanceof HttpError && err.status === 422) {
|
|
2198
|
+
logger.warn({
|
|
2199
|
+
issueId: id,
|
|
2200
|
+
companyId: existing.companyId,
|
|
2201
|
+
assigneePatch: {
|
|
2202
|
+
assigneeAgentId: normalizedAssigneeAgentId === undefined ? "__omitted__" : normalizedAssigneeAgentId,
|
|
2203
|
+
assigneeUserId: req.body.assigneeUserId === undefined ? "__omitted__" : req.body.assigneeUserId,
|
|
2204
|
+
},
|
|
2205
|
+
currentAssignee: {
|
|
2206
|
+
assigneeAgentId: existing.assigneeAgentId,
|
|
2207
|
+
assigneeUserId: existing.assigneeUserId,
|
|
2208
|
+
},
|
|
2209
|
+
error: err.message,
|
|
2210
|
+
details: err.details,
|
|
2211
|
+
}, "issue update rejected with 422");
|
|
2212
|
+
}
|
|
2213
|
+
throw err;
|
|
2214
|
+
}
|
|
2215
|
+
if (!issue) {
|
|
2216
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
let cancelledStatusRunId = null;
|
|
2220
|
+
if (runToCancelForCancelledStatus) {
|
|
2221
|
+
try {
|
|
2222
|
+
const cancelled = await heartbeat.cancelRun(runToCancelForCancelledStatus.id);
|
|
2223
|
+
if (cancelled) {
|
|
2224
|
+
cancelledStatusRunId = cancelled.id;
|
|
2225
|
+
await logActivity(db, {
|
|
2226
|
+
companyId: cancelled.companyId,
|
|
2227
|
+
actorType: actor.actorType,
|
|
2228
|
+
actorId: actor.actorId,
|
|
2229
|
+
agentId: actor.agentId,
|
|
2230
|
+
runId: actor.runId,
|
|
2231
|
+
action: "heartbeat.cancelled",
|
|
2232
|
+
entityType: "heartbeat_run",
|
|
2233
|
+
entityId: cancelled.id,
|
|
2234
|
+
details: { agentId: cancelled.agentId, source: "issue_status_cancelled", issueId: existing.id },
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
catch (err) {
|
|
2239
|
+
logger.warn({ err, issueId: existing.id, runId: runToCancelForCancelledStatus.id }, "failed to cancel run for cancelled issue");
|
|
2240
|
+
await logActivity(db, {
|
|
2241
|
+
companyId: existing.companyId,
|
|
2242
|
+
actorType: actor.actorType,
|
|
2243
|
+
actorId: actor.actorId,
|
|
2244
|
+
agentId: actor.agentId,
|
|
2245
|
+
runId: actor.runId,
|
|
2246
|
+
action: "heartbeat.cancel_failed",
|
|
2247
|
+
entityType: "heartbeat_run",
|
|
2248
|
+
entityId: runToCancelForCancelledStatus.id,
|
|
2249
|
+
details: { source: "issue_status_cancelled", issueId: existing.id },
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (titleOrDescriptionChanged) {
|
|
2254
|
+
await issueReferencesSvc.syncIssue(issue.id);
|
|
2255
|
+
}
|
|
2256
|
+
const updateReferenceSummaryAfter = titleOrDescriptionChanged
|
|
2257
|
+
? await issueReferencesSvc.listIssueReferenceSummary(issue.id)
|
|
2258
|
+
: null;
|
|
2259
|
+
const updateReferenceDiff = updateReferenceSummaryBefore && updateReferenceSummaryAfter
|
|
2260
|
+
? issueReferencesSvc.diffIssueReferenceSummary(updateReferenceSummaryBefore, updateReferenceSummaryAfter)
|
|
2261
|
+
: null;
|
|
2262
|
+
let issueResponse = issue;
|
|
2263
|
+
let updatedRelations = null;
|
|
2264
|
+
if (issue && Array.isArray(req.body.blockedByIssueIds)) {
|
|
2265
|
+
updatedRelations = await svc.getRelationSummaries(issue.id);
|
|
2266
|
+
issueResponse = {
|
|
2267
|
+
...issue,
|
|
2268
|
+
blockedBy: updatedRelations.blockedBy,
|
|
2269
|
+
blocks: updatedRelations.blocks,
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
await routinesSvc.syncRunStatusForIssue(issue.id);
|
|
2273
|
+
if (actor.runId) {
|
|
2274
|
+
await heartbeat.reportRunActivity(actor.runId).catch((err) => logger.warn({ err, runId: actor.runId }, "failed to clear detached run warning after issue activity"));
|
|
2275
|
+
}
|
|
2276
|
+
// Build activity details with previous values for changed fields
|
|
2277
|
+
const previous = {};
|
|
2278
|
+
for (const key of Object.keys(updateFields)) {
|
|
2279
|
+
if (key in existing && existing[key] !== updateFields[key]) {
|
|
2280
|
+
previous[key] = existing[key];
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
if (Array.isArray(req.body.blockedByIssueIds)) {
|
|
2284
|
+
previous.blockedByIssueIds = existingRelations?.blockedBy.map((relation) => relation.id) ?? [];
|
|
2285
|
+
}
|
|
2286
|
+
const hasFieldChanges = Object.keys(previous).length > 0;
|
|
2287
|
+
let workspaceChange = null;
|
|
2288
|
+
if (hasIssueWorkspaceAuditChange(previous)) {
|
|
2289
|
+
try {
|
|
2290
|
+
workspaceChange = await buildIssueWorkspaceChangeActivityDetails(db, issue.companyId, existing, issue);
|
|
2291
|
+
}
|
|
2292
|
+
catch (err) {
|
|
2293
|
+
logger.warn({ err, issueId: issue.id }, "failed to enrich issue workspace change activity details");
|
|
2294
|
+
const fallbackNames = emptyWorkspaceNameMaps();
|
|
2295
|
+
workspaceChange = {
|
|
2296
|
+
from: summarizeIssueWorkspaceForActivity(existing, fallbackNames),
|
|
2297
|
+
to: summarizeIssueWorkspaceForActivity(issue, fallbackNames),
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
const reopened = commentBody &&
|
|
2302
|
+
effectiveMoveToTodoRequested &&
|
|
2303
|
+
(isClosed || (isBlocked && !hasUnresolvedFirstClassBlockers)) &&
|
|
2304
|
+
previous.status !== undefined &&
|
|
2305
|
+
issue.status === "todo";
|
|
2306
|
+
const reopenFromStatus = reopened ? existing.status : null;
|
|
2307
|
+
await logActivity(db, {
|
|
2308
|
+
companyId: issue.companyId,
|
|
2309
|
+
actorType: actor.actorType,
|
|
2310
|
+
actorId: actor.actorId,
|
|
2311
|
+
agentId: actor.agentId,
|
|
2312
|
+
runId: actor.runId,
|
|
2313
|
+
action: "issue.updated",
|
|
2314
|
+
entityType: "issue",
|
|
2315
|
+
entityId: issue.id,
|
|
2316
|
+
details: {
|
|
2317
|
+
...updateFields,
|
|
2318
|
+
identifier: issue.identifier,
|
|
2319
|
+
...(commentBody ? { source: "comment" } : {}),
|
|
2320
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
2321
|
+
...(reopened ? { reopened: true, reopenedFrom: reopenFromStatus } : {}),
|
|
2322
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
2323
|
+
...(cancelledStatusRunId ? { cancelledStatusRunId } : {}),
|
|
2324
|
+
...(workspaceChange ? { workspaceChange } : {}),
|
|
2325
|
+
_previous: hasFieldChanges ? previous : undefined,
|
|
2326
|
+
...summarizeIssueReferenceActivityDetails(updateReferenceDiff
|
|
2327
|
+
? {
|
|
2328
|
+
addedReferencedIssues: updateReferenceDiff.addedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
2329
|
+
removedReferencedIssues: updateReferenceDiff.removedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
2330
|
+
currentReferencedIssues: updateReferenceDiff.currentReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
2331
|
+
}
|
|
2332
|
+
: null),
|
|
2333
|
+
},
|
|
2334
|
+
});
|
|
2335
|
+
if (existing.status === "in_progress" && issue.status !== existing.status && issue.status !== "in_progress") {
|
|
2336
|
+
await listSuccessfulRunHandoffStates(db, issue.companyId, [issue.id])
|
|
2337
|
+
.then(async (handoffStates) => {
|
|
2338
|
+
const handoff = handoffStates.get(issue.id);
|
|
2339
|
+
if (handoff?.state !== "required")
|
|
2340
|
+
return;
|
|
2341
|
+
await logActivity(db, {
|
|
2342
|
+
companyId: issue.companyId,
|
|
2343
|
+
actorType: actor.actorType,
|
|
2344
|
+
actorId: actor.actorId,
|
|
2345
|
+
agentId: actor.agentId,
|
|
2346
|
+
runId: actor.runId,
|
|
2347
|
+
action: "issue.successful_run_handoff_resolved",
|
|
2348
|
+
entityType: "issue",
|
|
2349
|
+
entityId: issue.id,
|
|
2350
|
+
details: {
|
|
2351
|
+
identifier: issue.identifier,
|
|
2352
|
+
sourceRunId: handoff.sourceRunId,
|
|
2353
|
+
correctiveRunId: handoff.correctiveRunId,
|
|
2354
|
+
resolvedByStatus: issue.status,
|
|
2355
|
+
},
|
|
2356
|
+
});
|
|
2357
|
+
})
|
|
2358
|
+
.catch((err) => {
|
|
2359
|
+
logger.warn({ err, issueId: issue.id }, "failed to log successful run handoff resolution");
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
if (Array.isArray(req.body.blockedByIssueIds)) {
|
|
2363
|
+
const previousBlockedByIds = new Set((existingRelations?.blockedBy ?? []).map((relation) => relation.id));
|
|
2364
|
+
const nextBlockedByIds = new Set(req.body.blockedByIssueIds);
|
|
2365
|
+
const addedBlockedByIssueIds = [...nextBlockedByIds].filter((candidate) => !previousBlockedByIds.has(candidate));
|
|
2366
|
+
const removedBlockedByIssueIds = [...previousBlockedByIds].filter((candidate) => !nextBlockedByIds.has(candidate));
|
|
2367
|
+
const nextBlockedByRelations = updatedRelations?.blockedBy ?? [];
|
|
2368
|
+
const previousBlockedByRelations = existingRelations?.blockedBy ?? [];
|
|
2369
|
+
if (addedBlockedByIssueIds.length > 0 || removedBlockedByIssueIds.length > 0) {
|
|
2370
|
+
await logActivity(db, {
|
|
2371
|
+
companyId: issue.companyId,
|
|
2372
|
+
actorType: actor.actorType,
|
|
2373
|
+
actorId: actor.actorId,
|
|
2374
|
+
agentId: actor.agentId,
|
|
2375
|
+
runId: actor.runId,
|
|
2376
|
+
action: "issue.blockers_updated",
|
|
2377
|
+
entityType: "issue",
|
|
2378
|
+
entityId: issue.id,
|
|
2379
|
+
details: {
|
|
2380
|
+
identifier: issue.identifier,
|
|
2381
|
+
blockedByIssueIds: req.body.blockedByIssueIds,
|
|
2382
|
+
addedBlockedByIssueIds,
|
|
2383
|
+
removedBlockedByIssueIds,
|
|
2384
|
+
blockedByIssues: nextBlockedByRelations.map(summarizeIssueRelationForActivity),
|
|
2385
|
+
addedBlockedByIssues: nextBlockedByRelations
|
|
2386
|
+
.filter((relation) => addedBlockedByIssueIds.includes(relation.id))
|
|
2387
|
+
.map(summarizeIssueRelationForActivity),
|
|
2388
|
+
removedBlockedByIssues: previousBlockedByRelations
|
|
2389
|
+
.filter((relation) => removedBlockedByIssueIds.includes(relation.id))
|
|
2390
|
+
.map(summarizeIssueRelationForActivity),
|
|
2391
|
+
},
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
const reviewerChanges = diffExecutionParticipants(previousExecutionPolicy, nextExecutionPolicy, "review");
|
|
2396
|
+
if (reviewerChanges.addedParticipants.length > 0 || reviewerChanges.removedParticipants.length > 0) {
|
|
2397
|
+
await logActivity(db, {
|
|
2398
|
+
companyId: issue.companyId,
|
|
2399
|
+
actorType: actor.actorType,
|
|
2400
|
+
actorId: actor.actorId,
|
|
2401
|
+
agentId: actor.agentId,
|
|
2402
|
+
runId: actor.runId,
|
|
2403
|
+
action: "issue.reviewers_updated",
|
|
2404
|
+
entityType: "issue",
|
|
2405
|
+
entityId: issue.id,
|
|
2406
|
+
details: {
|
|
2407
|
+
identifier: issue.identifier,
|
|
2408
|
+
participants: reviewerChanges.participants,
|
|
2409
|
+
addedParticipants: reviewerChanges.addedParticipants,
|
|
2410
|
+
removedParticipants: reviewerChanges.removedParticipants,
|
|
2411
|
+
},
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
const approverChanges = diffExecutionParticipants(previousExecutionPolicy, nextExecutionPolicy, "approval");
|
|
2415
|
+
if (approverChanges.addedParticipants.length > 0 || approverChanges.removedParticipants.length > 0) {
|
|
2416
|
+
await logActivity(db, {
|
|
2417
|
+
companyId: issue.companyId,
|
|
2418
|
+
actorType: actor.actorType,
|
|
2419
|
+
actorId: actor.actorId,
|
|
2420
|
+
agentId: actor.agentId,
|
|
2421
|
+
runId: actor.runId,
|
|
2422
|
+
action: "issue.approvers_updated",
|
|
2423
|
+
entityType: "issue",
|
|
2424
|
+
entityId: issue.id,
|
|
2425
|
+
details: {
|
|
2426
|
+
identifier: issue.identifier,
|
|
2427
|
+
participants: approverChanges.participants,
|
|
2428
|
+
addedParticipants: approverChanges.addedParticipants,
|
|
2429
|
+
removedParticipants: approverChanges.removedParticipants,
|
|
2430
|
+
},
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
const nextStoredExecutionPolicy = normalizeIssueExecutionPolicy(issue.executionPolicy ?? null);
|
|
2434
|
+
const previousMonitor = summarizeIssueMonitor(existing, previousExecutionPolicy);
|
|
2435
|
+
const nextMonitor = summarizeIssueMonitor(issue, nextStoredExecutionPolicy);
|
|
2436
|
+
const monitorScheduledChanged = previousMonitor.nextCheckAt !== nextMonitor.nextCheckAt;
|
|
2437
|
+
if (nextMonitor.nextCheckAt && (monitorScheduledChanged || previousMonitor.notes !== nextMonitor.notes)) {
|
|
2438
|
+
await logActivity(db, {
|
|
2439
|
+
companyId: issue.companyId,
|
|
2440
|
+
actorType: actor.actorType,
|
|
2441
|
+
actorId: actor.actorId,
|
|
2442
|
+
agentId: actor.agentId,
|
|
2443
|
+
runId: actor.runId,
|
|
2444
|
+
action: "issue.monitor_scheduled",
|
|
2445
|
+
entityType: "issue",
|
|
2446
|
+
entityId: issue.id,
|
|
2447
|
+
details: {
|
|
2448
|
+
identifier: issue.identifier,
|
|
2449
|
+
nextCheckAt: nextMonitor.nextCheckAt,
|
|
2450
|
+
previousNextCheckAt: previousMonitor.nextCheckAt,
|
|
2451
|
+
notes: nextMonitor.notes,
|
|
2452
|
+
scheduledBy: nextMonitor.scheduledBy,
|
|
2453
|
+
serviceName: nextMonitor.serviceName,
|
|
2454
|
+
timeoutAt: nextMonitor.timeoutAt,
|
|
2455
|
+
maxAttempts: nextMonitor.maxAttempts,
|
|
2456
|
+
recoveryPolicy: nextMonitor.recoveryPolicy,
|
|
2457
|
+
},
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
else if (!nextMonitor.nextCheckAt && previousMonitor.nextCheckAt) {
|
|
2461
|
+
await logActivity(db, {
|
|
2462
|
+
companyId: issue.companyId,
|
|
2463
|
+
actorType: actor.actorType,
|
|
2464
|
+
actorId: actor.actorId,
|
|
2465
|
+
agentId: actor.agentId,
|
|
2466
|
+
runId: actor.runId,
|
|
2467
|
+
action: "issue.monitor_cleared",
|
|
2468
|
+
entityType: "issue",
|
|
2469
|
+
entityId: issue.id,
|
|
2470
|
+
details: {
|
|
2471
|
+
identifier: issue.identifier,
|
|
2472
|
+
previousNextCheckAt: previousMonitor.nextCheckAt,
|
|
2473
|
+
reason: nextMonitor.clearReason ?? "manual",
|
|
2474
|
+
notes: previousMonitor.notes,
|
|
2475
|
+
},
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
if (issue.status === "done" && existing.status !== "done") {
|
|
2479
|
+
const tc = getTelemetryClient();
|
|
2480
|
+
if (tc && actor.agentId) {
|
|
2481
|
+
const actorAgent = await agentsSvc.getById(actor.agentId);
|
|
2482
|
+
if (actorAgent) {
|
|
2483
|
+
const model = typeof actorAgent.adapterConfig?.model === "string" ? actorAgent.adapterConfig.model : undefined;
|
|
2484
|
+
trackAgentTaskCompleted(tc, {
|
|
2485
|
+
agentRole: actorAgent.role,
|
|
2486
|
+
agentId: actorAgent.id,
|
|
2487
|
+
adapterType: actorAgent.adapterType,
|
|
2488
|
+
model,
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
let comment = null;
|
|
2494
|
+
if (commentBody) {
|
|
2495
|
+
const commentReferenceSummaryBefore = updateReferenceSummaryAfter
|
|
2496
|
+
?? await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
2497
|
+
comment = await svc.addComment(id, commentBody, {
|
|
2498
|
+
agentId: actor.agentId ?? undefined,
|
|
2499
|
+
userId: actor.actorType === "user" ? actor.actorId : undefined,
|
|
2500
|
+
runId: actor.runId,
|
|
2501
|
+
});
|
|
2502
|
+
await issueReferencesSvc.syncComment(comment.id);
|
|
2503
|
+
const commentReferenceSummaryAfter = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
2504
|
+
const commentReferenceDiff = issueReferencesSvc.diffIssueReferenceSummary(commentReferenceSummaryBefore, commentReferenceSummaryAfter);
|
|
2505
|
+
issueResponse = {
|
|
2506
|
+
...issueResponse,
|
|
2507
|
+
relatedWork: commentReferenceSummaryAfter,
|
|
2508
|
+
referencedIssueIdentifiers: commentReferenceSummaryAfter.outbound.map((item) => item.issue.identifier ?? item.issue.id),
|
|
2509
|
+
};
|
|
2510
|
+
await logActivity(db, {
|
|
2511
|
+
companyId: issue.companyId,
|
|
2512
|
+
actorType: actor.actorType,
|
|
2513
|
+
actorId: actor.actorId,
|
|
2514
|
+
agentId: actor.agentId,
|
|
2515
|
+
runId: actor.runId,
|
|
2516
|
+
action: "issue.comment_added",
|
|
2517
|
+
entityType: "issue",
|
|
2518
|
+
entityId: issue.id,
|
|
2519
|
+
details: {
|
|
2520
|
+
commentId: comment.id,
|
|
2521
|
+
bodySnippet: comment.body.slice(0, 120),
|
|
2522
|
+
identifier: issue.identifier,
|
|
2523
|
+
issueTitle: issue.title,
|
|
2524
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
2525
|
+
...(reopened ? { reopened: true, reopenedFrom: reopenFromStatus, source: "comment" } : {}),
|
|
2526
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
2527
|
+
...(hasFieldChanges ? { updated: true } : {}),
|
|
2528
|
+
...summarizeIssueReferenceActivityDetails({
|
|
2529
|
+
addedReferencedIssues: commentReferenceDiff.addedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
2530
|
+
removedReferencedIssues: commentReferenceDiff.removedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
2531
|
+
currentReferencedIssues: commentReferenceDiff.currentReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
2532
|
+
}),
|
|
2533
|
+
},
|
|
2534
|
+
});
|
|
2535
|
+
const expiredInteractions = await issueThreadInteractionService(db).expireRequestConfirmationsSupersededByComment(issue, comment, {
|
|
2536
|
+
agentId: actor.agentId,
|
|
2537
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
2538
|
+
});
|
|
2539
|
+
await logExpiredRequestConfirmations({
|
|
2540
|
+
issue,
|
|
2541
|
+
interactions: expiredInteractions,
|
|
2542
|
+
actor,
|
|
2543
|
+
source: "issue.comment",
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
else if (updateReferenceSummaryAfter) {
|
|
2547
|
+
issueResponse = {
|
|
2548
|
+
...issueResponse,
|
|
2549
|
+
relatedWork: updateReferenceSummaryAfter,
|
|
2550
|
+
referencedIssueIdentifiers: updateReferenceSummaryAfter.outbound.map((item) => item.issue.identifier ?? item.issue.id),
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
const assigneeChanged = issue.assigneeAgentId !== existing.assigneeAgentId || issue.assigneeUserId !== existing.assigneeUserId;
|
|
2554
|
+
const statusChangedFromBacklog = existing.status === "backlog" &&
|
|
2555
|
+
issue.status !== "backlog" &&
|
|
2556
|
+
req.body.status !== undefined;
|
|
2557
|
+
const statusChangedFromBlockedToTodo = existing.status === "blocked" &&
|
|
2558
|
+
issue.status === "todo" &&
|
|
2559
|
+
(req.body.status !== undefined || reopened);
|
|
2560
|
+
const statusChangedFromClosedToTodo = isClosedIssueStatus(existing.status) &&
|
|
2561
|
+
issue.status === "todo" &&
|
|
2562
|
+
req.body.status !== undefined;
|
|
2563
|
+
const previousExecutionState = parseIssueExecutionState(existing.executionState);
|
|
2564
|
+
const nextExecutionState = parseIssueExecutionState(issue.executionState);
|
|
2565
|
+
const executionStageWakeup = buildExecutionStageWakeup({
|
|
2566
|
+
issueId: issue.id,
|
|
2567
|
+
previousState: previousExecutionState,
|
|
2568
|
+
nextState: nextExecutionState,
|
|
2569
|
+
interruptedRunId,
|
|
2570
|
+
requestedByActorType: actor.actorType,
|
|
2571
|
+
requestedByActorId: actor.actorId,
|
|
2572
|
+
});
|
|
2573
|
+
// Merge all wakeups from this update into one enqueue per agent to avoid duplicate runs.
|
|
2574
|
+
void (async () => {
|
|
2575
|
+
const wakeups = new Map();
|
|
2576
|
+
const addWakeup = (agentId, wakeup) => {
|
|
2577
|
+
const wakeIssueId = wakeup.payload && typeof wakeup.payload === "object" && typeof wakeup.payload.issueId === "string"
|
|
2578
|
+
? wakeup.payload.issueId
|
|
2579
|
+
: issue.id;
|
|
2580
|
+
wakeups.set(`${agentId}:${wakeIssueId}`, { agentId, wakeup });
|
|
2581
|
+
};
|
|
2582
|
+
if (executionStageWakeup) {
|
|
2583
|
+
addWakeup(executionStageWakeup.agentId, executionStageWakeup.wakeup);
|
|
2584
|
+
}
|
|
2585
|
+
else if (assigneeChanged && issue.assigneeAgentId && issue.status !== "backlog") {
|
|
2586
|
+
addWakeup(issue.assigneeAgentId, {
|
|
2587
|
+
source: "assignment",
|
|
2588
|
+
triggerDetail: "system",
|
|
2589
|
+
reason: "issue_assigned",
|
|
2590
|
+
payload: {
|
|
2591
|
+
issueId: issue.id,
|
|
2592
|
+
...(comment ? { commentId: comment.id } : {}),
|
|
2593
|
+
mutation: "update",
|
|
2594
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
2595
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
2596
|
+
},
|
|
2597
|
+
requestedByActorType: actor.actorType,
|
|
2598
|
+
requestedByActorId: actor.actorId,
|
|
2599
|
+
contextSnapshot: {
|
|
2600
|
+
issueId: issue.id,
|
|
2601
|
+
...(comment
|
|
2602
|
+
? {
|
|
2603
|
+
taskId: issue.id,
|
|
2604
|
+
commentId: comment.id,
|
|
2605
|
+
wakeCommentId: comment.id,
|
|
2606
|
+
}
|
|
2607
|
+
: {}),
|
|
2608
|
+
source: "issue.update",
|
|
2609
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
2610
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
2611
|
+
},
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
if (!assigneeChanged &&
|
|
2615
|
+
(statusChangedFromBacklog || statusChangedFromBlockedToTodo || statusChangedFromClosedToTodo) &&
|
|
2616
|
+
issue.assigneeAgentId) {
|
|
2617
|
+
addWakeup(issue.assigneeAgentId, {
|
|
2618
|
+
source: "automation",
|
|
2619
|
+
triggerDetail: "system",
|
|
2620
|
+
reason: "issue_status_changed",
|
|
2621
|
+
payload: {
|
|
2622
|
+
issueId: issue.id,
|
|
2623
|
+
mutation: "update",
|
|
2624
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
2625
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
2626
|
+
},
|
|
2627
|
+
requestedByActorType: actor.actorType,
|
|
2628
|
+
requestedByActorId: actor.actorId,
|
|
2629
|
+
contextSnapshot: {
|
|
2630
|
+
issueId: issue.id,
|
|
2631
|
+
source: "issue.status_change",
|
|
2632
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
2633
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
2634
|
+
},
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
if (commentBody && comment) {
|
|
2638
|
+
const assigneeId = issue.assigneeAgentId;
|
|
2639
|
+
const actorIsAgent = actor.actorType === "agent";
|
|
2640
|
+
const selfComment = actorIsAgent && actor.actorId === assigneeId;
|
|
2641
|
+
const skipAssigneeCommentWake = selfComment || isClosed;
|
|
2642
|
+
if (assigneeId && !assigneeChanged && (reopened || !skipAssigneeCommentWake)) {
|
|
2643
|
+
addWakeup(assigneeId, {
|
|
2644
|
+
source: "automation",
|
|
2645
|
+
triggerDetail: "system",
|
|
2646
|
+
reason: reopened ? "issue_reopened_via_comment" : "issue_commented",
|
|
2647
|
+
payload: {
|
|
2648
|
+
issueId: id,
|
|
2649
|
+
commentId: comment.id,
|
|
2650
|
+
mutation: "comment",
|
|
2651
|
+
...(reopened ? { reopenedFrom: reopenFromStatus } : {}),
|
|
2652
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
2653
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
2654
|
+
},
|
|
2655
|
+
requestedByActorType: actor.actorType,
|
|
2656
|
+
requestedByActorId: actor.actorId,
|
|
2657
|
+
contextSnapshot: {
|
|
2658
|
+
issueId: id,
|
|
2659
|
+
taskId: id,
|
|
2660
|
+
commentId: comment.id,
|
|
2661
|
+
wakeCommentId: comment.id,
|
|
2662
|
+
source: reopened ? "issue.comment.reopen" : "issue.comment",
|
|
2663
|
+
wakeReason: reopened ? "issue_reopened_via_comment" : "issue_commented",
|
|
2664
|
+
...(reopened ? { reopenedFrom: reopenFromStatus } : {}),
|
|
2665
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
2666
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
2667
|
+
},
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
let mentionedIds = [];
|
|
2671
|
+
try {
|
|
2672
|
+
mentionedIds = await svc.findMentionedAgents(issue.companyId, commentBody);
|
|
2673
|
+
}
|
|
2674
|
+
catch (err) {
|
|
2675
|
+
logger.warn({ err, issueId: id }, "failed to resolve @-mentions");
|
|
2676
|
+
}
|
|
2677
|
+
for (const mentionedId of mentionedIds) {
|
|
2678
|
+
if (actor.actorType === "agent" && actor.actorId === mentionedId)
|
|
2679
|
+
continue;
|
|
2680
|
+
addWakeup(mentionedId, {
|
|
2681
|
+
source: "automation",
|
|
2682
|
+
triggerDetail: "system",
|
|
2683
|
+
reason: "issue_comment_mentioned",
|
|
2684
|
+
payload: { issueId: id, commentId: comment.id },
|
|
2685
|
+
requestedByActorType: actor.actorType,
|
|
2686
|
+
requestedByActorId: actor.actorId,
|
|
2687
|
+
contextSnapshot: {
|
|
2688
|
+
issueId: id,
|
|
2689
|
+
taskId: id,
|
|
2690
|
+
commentId: comment.id,
|
|
2691
|
+
wakeCommentId: comment.id,
|
|
2692
|
+
wakeReason: "issue_comment_mentioned",
|
|
2693
|
+
source: "comment.mention",
|
|
2694
|
+
},
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
const becameDone = existing.status !== "done" && issue.status === "done";
|
|
2699
|
+
if (becameDone) {
|
|
2700
|
+
const dependents = await svc.listWakeableBlockedDependents(issue.id);
|
|
2701
|
+
for (const dependent of dependents) {
|
|
2702
|
+
addWakeup(dependent.assigneeAgentId, {
|
|
2703
|
+
source: "automation",
|
|
2704
|
+
triggerDetail: "system",
|
|
2705
|
+
reason: "issue_blockers_resolved",
|
|
2706
|
+
payload: {
|
|
2707
|
+
issueId: dependent.id,
|
|
2708
|
+
resolvedBlockerIssueId: issue.id,
|
|
2709
|
+
blockerIssueIds: dependent.blockerIssueIds,
|
|
2710
|
+
},
|
|
2711
|
+
requestedByActorType: actor.actorType,
|
|
2712
|
+
requestedByActorId: actor.actorId,
|
|
2713
|
+
contextSnapshot: {
|
|
2714
|
+
issueId: dependent.id,
|
|
2715
|
+
taskId: dependent.id,
|
|
2716
|
+
wakeReason: "issue_blockers_resolved",
|
|
2717
|
+
source: "issue.blockers_resolved",
|
|
2718
|
+
resolvedBlockerIssueId: issue.id,
|
|
2719
|
+
blockerIssueIds: dependent.blockerIssueIds,
|
|
2720
|
+
},
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
const becameTerminal = !["done", "cancelled"].includes(existing.status) && ["done", "cancelled"].includes(issue.status);
|
|
2725
|
+
if (becameTerminal && issue.parentId) {
|
|
2726
|
+
const parent = await svc.getWakeableParentAfterChildCompletion(issue.parentId);
|
|
2727
|
+
if (parent) {
|
|
2728
|
+
addWakeup(parent.assigneeAgentId, {
|
|
2729
|
+
source: "automation",
|
|
2730
|
+
triggerDetail: "system",
|
|
2731
|
+
reason: "issue_children_completed",
|
|
2732
|
+
payload: {
|
|
2733
|
+
issueId: parent.id,
|
|
2734
|
+
completedChildIssueId: issue.id,
|
|
2735
|
+
childIssueIds: parent.childIssueIds,
|
|
2736
|
+
childIssueSummaries: parent.childIssueSummaries,
|
|
2737
|
+
childIssueSummaryTruncated: parent.childIssueSummaryTruncated,
|
|
2738
|
+
},
|
|
2739
|
+
requestedByActorType: actor.actorType,
|
|
2740
|
+
requestedByActorId: actor.actorId,
|
|
2741
|
+
contextSnapshot: {
|
|
2742
|
+
issueId: parent.id,
|
|
2743
|
+
taskId: parent.id,
|
|
2744
|
+
wakeReason: "issue_children_completed",
|
|
2745
|
+
source: "issue.children_completed",
|
|
2746
|
+
completedChildIssueId: issue.id,
|
|
2747
|
+
childIssueIds: parent.childIssueIds,
|
|
2748
|
+
childIssueSummaries: parent.childIssueSummaries,
|
|
2749
|
+
childIssueSummaryTruncated: parent.childIssueSummaryTruncated,
|
|
2750
|
+
},
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
for (const { agentId, wakeup } of wakeups.values()) {
|
|
2755
|
+
heartbeat
|
|
2756
|
+
.wakeup(agentId, wakeup)
|
|
2757
|
+
.catch((err) => logger.warn({ err, issueId: issue.id, agentId }, "failed to wake agent on issue update"));
|
|
2758
|
+
}
|
|
2759
|
+
})();
|
|
2760
|
+
res.json({ ...issueResponse, comment });
|
|
2761
|
+
});
|
|
2762
|
+
router.delete("/issues/:id", async (req, res) => {
|
|
2763
|
+
const id = req.params.id;
|
|
2764
|
+
const existing = await svc.getById(id);
|
|
2765
|
+
if (!existing) {
|
|
2766
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
assertCompanyAccess(req, existing.companyId);
|
|
2770
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, existing)))
|
|
2771
|
+
return;
|
|
2772
|
+
const attachments = await svc.listAttachments(id);
|
|
2773
|
+
const issue = await svc.remove(id);
|
|
2774
|
+
if (!issue) {
|
|
2775
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
for (const attachment of attachments) {
|
|
2779
|
+
try {
|
|
2780
|
+
await storage.deleteObject(attachment.companyId, attachment.objectKey);
|
|
2781
|
+
}
|
|
2782
|
+
catch (err) {
|
|
2783
|
+
logger.warn({ err, issueId: id, attachmentId: attachment.id }, "failed to delete attachment object during issue delete");
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
const actor = getActorInfo(req);
|
|
2787
|
+
await logActivity(db, {
|
|
2788
|
+
companyId: issue.companyId,
|
|
2789
|
+
actorType: actor.actorType,
|
|
2790
|
+
actorId: actor.actorId,
|
|
2791
|
+
agentId: actor.agentId,
|
|
2792
|
+
runId: actor.runId,
|
|
2793
|
+
action: "issue.deleted",
|
|
2794
|
+
entityType: "issue",
|
|
2795
|
+
entityId: issue.id,
|
|
2796
|
+
});
|
|
2797
|
+
res.json(issue);
|
|
2798
|
+
});
|
|
2799
|
+
router.post("/issues/:id/checkout", validate(checkoutIssueSchema), async (req, res) => {
|
|
2800
|
+
const id = req.params.id;
|
|
2801
|
+
const issue = await svc.getById(id);
|
|
2802
|
+
if (!issue) {
|
|
2803
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
assertCompanyAccess(req, issue.companyId);
|
|
2807
|
+
if (issue.projectId) {
|
|
2808
|
+
const project = await projectsSvc.getById(issue.projectId);
|
|
2809
|
+
if (project?.pausedAt) {
|
|
2810
|
+
res.status(409).json({
|
|
2811
|
+
error: project.pauseReason === "budget"
|
|
2812
|
+
? "Project is paused because its budget hard-stop was reached"
|
|
2813
|
+
: "Project is paused",
|
|
2814
|
+
});
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
if (req.actor.type === "agent" && req.actor.agentId !== req.body.agentId) {
|
|
2819
|
+
res.status(403).json({ error: "Agent can only checkout as itself" });
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
const closedExecutionWorkspace = await getClosedIssueExecutionWorkspace(issue);
|
|
2823
|
+
if (closedExecutionWorkspace) {
|
|
2824
|
+
respondClosedIssueExecutionWorkspace(res, closedExecutionWorkspace);
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
const checkoutRunId = requireAgentRunId(req, res);
|
|
2828
|
+
if (req.actor.type === "agent" && !checkoutRunId)
|
|
2829
|
+
return;
|
|
2830
|
+
const updated = await svc.checkout(id, req.body.agentId, req.body.expectedStatuses, checkoutRunId);
|
|
2831
|
+
const actor = getActorInfo(req);
|
|
2832
|
+
await logActivity(db, {
|
|
2833
|
+
companyId: issue.companyId,
|
|
2834
|
+
actorType: actor.actorType,
|
|
2835
|
+
actorId: actor.actorId,
|
|
2836
|
+
agentId: actor.agentId,
|
|
2837
|
+
runId: actor.runId,
|
|
2838
|
+
action: "issue.checked_out",
|
|
2839
|
+
entityType: "issue",
|
|
2840
|
+
entityId: issue.id,
|
|
2841
|
+
details: { agentId: req.body.agentId },
|
|
2842
|
+
});
|
|
2843
|
+
if (shouldWakeAssigneeOnCheckout({
|
|
2844
|
+
actorType: req.actor.type,
|
|
2845
|
+
actorAgentId: req.actor.type === "agent" ? req.actor.agentId ?? null : null,
|
|
2846
|
+
checkoutAgentId: req.body.agentId,
|
|
2847
|
+
checkoutRunId,
|
|
2848
|
+
})) {
|
|
2849
|
+
void heartbeat
|
|
2850
|
+
.wakeup(req.body.agentId, {
|
|
2851
|
+
source: "assignment",
|
|
2852
|
+
triggerDetail: "system",
|
|
2853
|
+
reason: "issue_checked_out",
|
|
2854
|
+
payload: { issueId: issue.id, mutation: "checkout" },
|
|
2855
|
+
requestedByActorType: actor.actorType,
|
|
2856
|
+
requestedByActorId: actor.actorId,
|
|
2857
|
+
contextSnapshot: { issueId: issue.id, source: "issue.checkout" },
|
|
2858
|
+
})
|
|
2859
|
+
.catch((err) => logger.warn({ err, issueId: issue.id }, "failed to wake assignee on issue checkout"));
|
|
2860
|
+
}
|
|
2861
|
+
res.json(updated);
|
|
2862
|
+
});
|
|
2863
|
+
router.post("/issues/:id/release", async (req, res) => {
|
|
2864
|
+
const id = req.params.id;
|
|
2865
|
+
const existing = await svc.getById(id);
|
|
2866
|
+
if (!existing) {
|
|
2867
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
assertCompanyAccess(req, existing.companyId);
|
|
2871
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, existing)))
|
|
2872
|
+
return;
|
|
2873
|
+
const actorRunId = requireAgentRunId(req, res);
|
|
2874
|
+
if (req.actor.type === "agent" && !actorRunId)
|
|
2875
|
+
return;
|
|
2876
|
+
const released = await svc.release(id, req.actor.type === "agent" ? req.actor.agentId : undefined, actorRunId);
|
|
2877
|
+
if (!released) {
|
|
2878
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
const actor = getActorInfo(req);
|
|
2882
|
+
await logActivity(db, {
|
|
2883
|
+
companyId: released.companyId,
|
|
2884
|
+
actorType: actor.actorType,
|
|
2885
|
+
actorId: actor.actorId,
|
|
2886
|
+
agentId: actor.agentId,
|
|
2887
|
+
runId: actor.runId,
|
|
2888
|
+
action: "issue.released",
|
|
2889
|
+
entityType: "issue",
|
|
2890
|
+
entityId: released.id,
|
|
2891
|
+
});
|
|
2892
|
+
res.json(released);
|
|
2893
|
+
});
|
|
2894
|
+
router.post("/issues/:id/admin/force-release", async (req, res) => {
|
|
2895
|
+
if (req.actor.type !== "board") {
|
|
2896
|
+
res.status(403).json({ error: "Board access required" });
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
if (!req.actor.userId) {
|
|
2900
|
+
throw forbidden("Board user context required");
|
|
2901
|
+
}
|
|
2902
|
+
const id = req.params.id;
|
|
2903
|
+
const existing = await svc.getById(id);
|
|
2904
|
+
if (!existing) {
|
|
2905
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
assertCompanyAccess(req, existing.companyId);
|
|
2909
|
+
const clearAssignee = req.query.clearAssignee === "true";
|
|
2910
|
+
const result = await svc.adminForceRelease(id, { clearAssignee });
|
|
2911
|
+
if (!result) {
|
|
2912
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
const actor = getActorInfo(req);
|
|
2916
|
+
await logActivity(db, {
|
|
2917
|
+
companyId: result.issue.companyId,
|
|
2918
|
+
actorType: actor.actorType,
|
|
2919
|
+
actorId: actor.actorId,
|
|
2920
|
+
agentId: actor.agentId,
|
|
2921
|
+
runId: actor.runId,
|
|
2922
|
+
action: "issue.admin_force_release",
|
|
2923
|
+
entityType: "issue",
|
|
2924
|
+
entityId: result.issue.id,
|
|
2925
|
+
details: {
|
|
2926
|
+
issueId: result.issue.id,
|
|
2927
|
+
actorUserId: req.actor.userId,
|
|
2928
|
+
prevCheckoutRunId: result.previous.checkoutRunId,
|
|
2929
|
+
prevExecutionRunId: result.previous.executionRunId,
|
|
2930
|
+
clearAssignee,
|
|
2931
|
+
},
|
|
2932
|
+
});
|
|
2933
|
+
res.json(result);
|
|
2934
|
+
});
|
|
2935
|
+
router.get("/issues/:id/comments", async (req, res) => {
|
|
2936
|
+
const id = req.params.id;
|
|
2937
|
+
const issue = await svc.getById(id);
|
|
2938
|
+
if (!issue) {
|
|
2939
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
assertCompanyAccess(req, issue.companyId);
|
|
2943
|
+
const afterCommentId = typeof req.query.after === "string" && req.query.after.trim().length > 0
|
|
2944
|
+
? req.query.after.trim()
|
|
2945
|
+
: typeof req.query.afterCommentId === "string" && req.query.afterCommentId.trim().length > 0
|
|
2946
|
+
? req.query.afterCommentId.trim()
|
|
2947
|
+
: null;
|
|
2948
|
+
const order = typeof req.query.order === "string" && req.query.order.trim().toLowerCase() === "asc"
|
|
2949
|
+
? "asc"
|
|
2950
|
+
: "desc";
|
|
2951
|
+
const limitRaw = typeof req.query.limit === "string" && req.query.limit.trim().length > 0
|
|
2952
|
+
? Number(req.query.limit)
|
|
2953
|
+
: null;
|
|
2954
|
+
const limit = limitRaw && Number.isFinite(limitRaw) && limitRaw > 0
|
|
2955
|
+
? Math.min(Math.floor(limitRaw), MAX_ISSUE_COMMENT_LIMIT)
|
|
2956
|
+
: null;
|
|
2957
|
+
const comments = await svc.listComments(id, {
|
|
2958
|
+
afterCommentId,
|
|
2959
|
+
order,
|
|
2960
|
+
limit,
|
|
2961
|
+
});
|
|
2962
|
+
res.json(comments);
|
|
2963
|
+
});
|
|
2964
|
+
router.get("/issues/:id/interactions", async (req, res) => {
|
|
2965
|
+
const id = req.params.id;
|
|
2966
|
+
const issue = await svc.getById(id);
|
|
2967
|
+
if (!issue) {
|
|
2968
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
assertCompanyAccess(req, issue.companyId);
|
|
2972
|
+
const interactions = await issueThreadInteractionService(db).listForIssue(id);
|
|
2973
|
+
res.json(interactions);
|
|
2974
|
+
});
|
|
2975
|
+
router.post("/issues/:id/interactions", validate(createIssueThreadInteractionSchema), async (req, res) => {
|
|
2976
|
+
const id = req.params.id;
|
|
2977
|
+
const issue = await svc.getById(id);
|
|
2978
|
+
if (!issue) {
|
|
2979
|
+
res.status(404).json({ error: "Issue not found" });
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
assertCompanyAccess(req, issue.companyId);
|
|
2983
|
+
if (req.actor.type === "agent") {
|
|
2984
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
2985
|
+
return;
|
|
2986
|
+
}
|
|
2987
|
+
else {
|
|
2988
|
+
assertBoard(req);
|
|
2989
|
+
}
|
|
2990
|
+
const actor = getActorInfo(req);
|
|
2991
|
+
const agentSourceRunId = req.actor.type === "agent" ? requireAgentRunId(req, res) : null;
|
|
2992
|
+
if (req.actor.type === "agent" && !agentSourceRunId)
|
|
2993
|
+
return;
|
|
2994
|
+
const interaction = await issueThreadInteractionService(db).create(issue, {
|
|
2995
|
+
...req.body,
|
|
2996
|
+
sourceRunId: req.actor.type === "agent" ? agentSourceRunId : req.body.sourceRunId ?? null,
|
|
2997
|
+
}, {
|
|
2998
|
+
agentId: actor.agentId,
|
|
2999
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
3000
|
+
});
|
|
3001
|
+
await logActivity(db, {
|
|
3002
|
+
companyId: issue.companyId,
|
|
3003
|
+
actorType: actor.actorType,
|
|
3004
|
+
actorId: actor.actorId,
|
|
3005
|
+
agentId: actor.agentId,
|
|
3006
|
+
runId: actor.runId,
|
|
3007
|
+
action: "issue.thread_interaction_created",
|
|
3008
|
+
entityType: "issue",
|
|
3009
|
+
entityId: issue.id,
|
|
3010
|
+
details: {
|
|
3011
|
+
interactionId: interaction.id,
|
|
3012
|
+
interactionKind: interaction.kind,
|
|
3013
|
+
interactionStatus: interaction.status,
|
|
3014
|
+
continuationPolicy: interaction.continuationPolicy,
|
|
3015
|
+
},
|
|
3016
|
+
});
|
|
3017
|
+
res.status(201).json(interaction);
|
|
3018
|
+
});
|
|
3019
|
+
router.post("/issues/:id/interactions/:interactionId/accept", validate(acceptIssueThreadInteractionSchema), async (req, res) => {
|
|
3020
|
+
const id = req.params.id;
|
|
3021
|
+
const interactionId = req.params.interactionId;
|
|
3022
|
+
const issue = await svc.getById(id);
|
|
3023
|
+
if (!issue) {
|
|
3024
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3028
|
+
assertBoard(req);
|
|
3029
|
+
const actor = getActorInfo(req);
|
|
3030
|
+
const { interaction, createdIssues, continuationIssue } = await issueThreadInteractionService(db).acceptInteraction(issue, interactionId, req.body, {
|
|
3031
|
+
agentId: actor.agentId,
|
|
3032
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
3033
|
+
});
|
|
3034
|
+
const continuationWakeIssue = continuationIssue ?? issue;
|
|
3035
|
+
await logActivity(db, {
|
|
3036
|
+
companyId: issue.companyId,
|
|
3037
|
+
actorType: actor.actorType,
|
|
3038
|
+
actorId: actor.actorId,
|
|
3039
|
+
agentId: actor.agentId,
|
|
3040
|
+
runId: actor.runId,
|
|
3041
|
+
action: interaction.status === "expired"
|
|
3042
|
+
? "issue.thread_interaction_expired"
|
|
3043
|
+
: "issue.thread_interaction_accepted",
|
|
3044
|
+
entityType: "issue",
|
|
3045
|
+
entityId: issue.id,
|
|
3046
|
+
details: {
|
|
3047
|
+
interactionId: interaction.id,
|
|
3048
|
+
interactionKind: interaction.kind,
|
|
3049
|
+
interactionStatus: interaction.status,
|
|
3050
|
+
createdTaskCount: interaction.kind === "suggest_tasks"
|
|
3051
|
+
? (interaction.result?.createdTasks?.length ?? 0)
|
|
3052
|
+
: 0,
|
|
3053
|
+
skippedTaskCount: interaction.kind === "suggest_tasks"
|
|
3054
|
+
? (interaction.result?.skippedClientKeys?.length ?? 0)
|
|
3055
|
+
: 0,
|
|
3056
|
+
},
|
|
3057
|
+
});
|
|
3058
|
+
if (continuationIssue) {
|
|
3059
|
+
await logActivity(db, {
|
|
3060
|
+
companyId: issue.companyId,
|
|
3061
|
+
actorType: actor.actorType,
|
|
3062
|
+
actorId: actor.actorId,
|
|
3063
|
+
agentId: actor.agentId,
|
|
3064
|
+
runId: actor.runId,
|
|
3065
|
+
action: "issue.updated",
|
|
3066
|
+
entityType: "issue",
|
|
3067
|
+
entityId: issue.id,
|
|
3068
|
+
details: {
|
|
3069
|
+
identifier: issue.identifier,
|
|
3070
|
+
status: continuationIssue.status,
|
|
3071
|
+
assigneeAgentId: continuationIssue.assigneeAgentId ?? null,
|
|
3072
|
+
assigneeUserId: continuationIssue.assigneeUserId ?? null,
|
|
3073
|
+
source: "request_confirmation_accept",
|
|
3074
|
+
interactionId: interaction.id,
|
|
3075
|
+
_previous: {
|
|
3076
|
+
status: issue.status,
|
|
3077
|
+
assigneeAgentId: issue.assigneeAgentId ?? null,
|
|
3078
|
+
assigneeUserId: issue.assigneeUserId ?? null,
|
|
3079
|
+
},
|
|
3080
|
+
},
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
for (const createdIssue of createdIssues) {
|
|
3084
|
+
void queueIssueAssignmentWakeup({
|
|
3085
|
+
heartbeat,
|
|
3086
|
+
issue: createdIssue,
|
|
3087
|
+
reason: "issue_assigned",
|
|
3088
|
+
mutation: "interaction_accept",
|
|
3089
|
+
contextSource: "issue.interaction.accept",
|
|
3090
|
+
requestedByActorType: actor.actorType,
|
|
3091
|
+
requestedByActorId: actor.actorId,
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
queueResolvedInteractionContinuationWakeup({
|
|
3095
|
+
heartbeat,
|
|
3096
|
+
issue: continuationWakeIssue,
|
|
3097
|
+
interaction,
|
|
3098
|
+
actor,
|
|
3099
|
+
source: "issue.interaction.accept",
|
|
3100
|
+
});
|
|
3101
|
+
res.json(interaction);
|
|
3102
|
+
});
|
|
3103
|
+
router.post("/issues/:id/interactions/:interactionId/reject", validate(rejectIssueThreadInteractionSchema), async (req, res) => {
|
|
3104
|
+
const id = req.params.id;
|
|
3105
|
+
const interactionId = req.params.interactionId;
|
|
3106
|
+
const issue = await svc.getById(id);
|
|
3107
|
+
if (!issue) {
|
|
3108
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3112
|
+
assertBoard(req);
|
|
3113
|
+
const actor = getActorInfo(req);
|
|
3114
|
+
const interaction = await issueThreadInteractionService(db).rejectInteraction(issue, interactionId, req.body, {
|
|
3115
|
+
agentId: actor.agentId,
|
|
3116
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
3117
|
+
});
|
|
3118
|
+
await logActivity(db, {
|
|
3119
|
+
companyId: issue.companyId,
|
|
3120
|
+
actorType: actor.actorType,
|
|
3121
|
+
actorId: actor.actorId,
|
|
3122
|
+
agentId: actor.agentId,
|
|
3123
|
+
runId: actor.runId,
|
|
3124
|
+
action: interaction.status === "expired"
|
|
3125
|
+
? "issue.thread_interaction_expired"
|
|
3126
|
+
: "issue.thread_interaction_rejected",
|
|
3127
|
+
entityType: "issue",
|
|
3128
|
+
entityId: issue.id,
|
|
3129
|
+
details: {
|
|
3130
|
+
interactionId: interaction.id,
|
|
3131
|
+
interactionKind: interaction.kind,
|
|
3132
|
+
interactionStatus: interaction.status,
|
|
3133
|
+
rejectionReason: interaction.kind === "suggest_tasks"
|
|
3134
|
+
? (interaction.result?.rejectionReason ?? null)
|
|
3135
|
+
: interaction.kind === "request_confirmation"
|
|
3136
|
+
? (interaction.result?.reason ?? null)
|
|
3137
|
+
: null,
|
|
3138
|
+
},
|
|
3139
|
+
});
|
|
3140
|
+
queueResolvedInteractionContinuationWakeup({
|
|
3141
|
+
heartbeat,
|
|
3142
|
+
issue,
|
|
3143
|
+
interaction,
|
|
3144
|
+
actor,
|
|
3145
|
+
source: "issue.interaction.reject",
|
|
3146
|
+
});
|
|
3147
|
+
res.json(interaction);
|
|
3148
|
+
});
|
|
3149
|
+
router.post("/issues/:id/interactions/:interactionId/respond", validate(respondIssueThreadInteractionSchema), async (req, res) => {
|
|
3150
|
+
const id = req.params.id;
|
|
3151
|
+
const interactionId = req.params.interactionId;
|
|
3152
|
+
const issue = await svc.getById(id);
|
|
3153
|
+
if (!issue) {
|
|
3154
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3158
|
+
assertBoard(req);
|
|
3159
|
+
const actor = getActorInfo(req);
|
|
3160
|
+
const interaction = await issueThreadInteractionService(db).answerQuestions(issue, interactionId, req.body, {
|
|
3161
|
+
agentId: actor.agentId,
|
|
3162
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
3163
|
+
});
|
|
3164
|
+
await logActivity(db, {
|
|
3165
|
+
companyId: issue.companyId,
|
|
3166
|
+
actorType: actor.actorType,
|
|
3167
|
+
actorId: actor.actorId,
|
|
3168
|
+
agentId: actor.agentId,
|
|
3169
|
+
runId: actor.runId,
|
|
3170
|
+
action: "issue.thread_interaction_answered",
|
|
3171
|
+
entityType: "issue",
|
|
3172
|
+
entityId: issue.id,
|
|
3173
|
+
details: {
|
|
3174
|
+
interactionId: interaction.id,
|
|
3175
|
+
interactionKind: interaction.kind,
|
|
3176
|
+
interactionStatus: interaction.status,
|
|
3177
|
+
answeredQuestionCount: interaction.kind === "ask_user_questions"
|
|
3178
|
+
? (interaction.result?.answers?.length ?? 0)
|
|
3179
|
+
: 0,
|
|
3180
|
+
},
|
|
3181
|
+
});
|
|
3182
|
+
queueResolvedInteractionContinuationWakeup({
|
|
3183
|
+
heartbeat,
|
|
3184
|
+
issue,
|
|
3185
|
+
interaction,
|
|
3186
|
+
actor,
|
|
3187
|
+
source: "issue.interaction.respond",
|
|
3188
|
+
});
|
|
3189
|
+
res.json(interaction);
|
|
3190
|
+
});
|
|
3191
|
+
router.post("/issues/:id/interactions/:interactionId/cancel", validate(cancelIssueThreadInteractionSchema), async (req, res) => {
|
|
3192
|
+
const id = req.params.id;
|
|
3193
|
+
const interactionId = req.params.interactionId;
|
|
3194
|
+
const issue = await svc.getById(id);
|
|
3195
|
+
if (!issue) {
|
|
3196
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3200
|
+
assertBoard(req);
|
|
3201
|
+
const actor = getActorInfo(req);
|
|
3202
|
+
const interaction = await issueThreadInteractionService(db).cancelQuestions(issue, interactionId, req.body, {
|
|
3203
|
+
agentId: actor.agentId,
|
|
3204
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
3205
|
+
});
|
|
3206
|
+
await logActivity(db, {
|
|
3207
|
+
companyId: issue.companyId,
|
|
3208
|
+
actorType: actor.actorType,
|
|
3209
|
+
actorId: actor.actorId,
|
|
3210
|
+
agentId: actor.agentId,
|
|
3211
|
+
runId: actor.runId,
|
|
3212
|
+
action: "issue.thread_interaction_cancelled",
|
|
3213
|
+
entityType: "issue",
|
|
3214
|
+
entityId: issue.id,
|
|
3215
|
+
details: {
|
|
3216
|
+
interactionId: interaction.id,
|
|
3217
|
+
interactionKind: interaction.kind,
|
|
3218
|
+
interactionStatus: interaction.status,
|
|
3219
|
+
cancellationReason: interaction.kind === "ask_user_questions"
|
|
3220
|
+
? (interaction.result?.cancellationReason ?? null)
|
|
3221
|
+
: null,
|
|
3222
|
+
},
|
|
3223
|
+
});
|
|
3224
|
+
queueResolvedInteractionContinuationWakeup({
|
|
3225
|
+
heartbeat,
|
|
3226
|
+
issue,
|
|
3227
|
+
interaction,
|
|
3228
|
+
actor,
|
|
3229
|
+
source: "issue.interaction.cancel",
|
|
3230
|
+
});
|
|
3231
|
+
res.json(interaction);
|
|
3232
|
+
});
|
|
3233
|
+
router.get("/issues/:id/comments/:commentId", async (req, res) => {
|
|
3234
|
+
const id = req.params.id;
|
|
3235
|
+
const commentId = req.params.commentId;
|
|
3236
|
+
const issue = await svc.getById(id);
|
|
3237
|
+
if (!issue) {
|
|
3238
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3242
|
+
const comment = await svc.getComment(commentId);
|
|
3243
|
+
if (!comment || comment.issueId !== id) {
|
|
3244
|
+
res.status(404).json({ error: "Comment not found" });
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
res.json(comment);
|
|
3248
|
+
});
|
|
3249
|
+
router.delete("/issues/:id/comments/:commentId", async (req, res) => {
|
|
3250
|
+
const id = req.params.id;
|
|
3251
|
+
const commentId = req.params.commentId;
|
|
3252
|
+
const issue = await svc.getById(id);
|
|
3253
|
+
if (!issue) {
|
|
3254
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3258
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
3259
|
+
return;
|
|
3260
|
+
const comment = await svc.getComment(commentId);
|
|
3261
|
+
if (!comment || comment.issueId !== id) {
|
|
3262
|
+
res.status(404).json({ error: "Comment not found" });
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
const actor = getActorInfo(req);
|
|
3266
|
+
const actorOwnsComment = actor.actorType === "agent"
|
|
3267
|
+
? comment.authorAgentId === actor.agentId
|
|
3268
|
+
: comment.authorUserId === actor.actorId;
|
|
3269
|
+
if (!actorOwnsComment) {
|
|
3270
|
+
res.status(403).json({ error: "Only the comment author can cancel queued comments" });
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
const activeRun = await resolveActiveIssueRun(issue);
|
|
3274
|
+
if (!activeRun) {
|
|
3275
|
+
res.status(409).json({ error: "Queued comment can no longer be canceled" });
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
if (!isQueuedIssueCommentForActiveRun({ comment, activeRun })) {
|
|
3279
|
+
res.status(409).json({ error: "Only queued comments can be canceled" });
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
const removed = await svc.removeComment(commentId);
|
|
3283
|
+
if (!removed) {
|
|
3284
|
+
res.status(404).json({ error: "Comment not found" });
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
await logActivity(db, {
|
|
3288
|
+
companyId: issue.companyId,
|
|
3289
|
+
actorType: actor.actorType,
|
|
3290
|
+
actorId: actor.actorId,
|
|
3291
|
+
agentId: actor.agentId,
|
|
3292
|
+
runId: actor.runId,
|
|
3293
|
+
action: "issue.comment_cancelled",
|
|
3294
|
+
entityType: "issue",
|
|
3295
|
+
entityId: issue.id,
|
|
3296
|
+
details: {
|
|
3297
|
+
commentId: removed.id,
|
|
3298
|
+
bodySnippet: removed.body.slice(0, 120),
|
|
3299
|
+
identifier: issue.identifier,
|
|
3300
|
+
issueTitle: issue.title,
|
|
3301
|
+
source: "queue_cancel",
|
|
3302
|
+
queueTargetRunId: activeRun.id,
|
|
3303
|
+
},
|
|
3304
|
+
});
|
|
3305
|
+
res.json(removed);
|
|
3306
|
+
});
|
|
3307
|
+
router.get("/issues/:id/feedback-votes", async (req, res) => {
|
|
3308
|
+
const id = req.params.id;
|
|
3309
|
+
const issue = await svc.getById(id);
|
|
3310
|
+
if (!issue) {
|
|
3311
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3315
|
+
if (req.actor.type !== "board") {
|
|
3316
|
+
res.status(403).json({ error: "Only board users can view feedback votes" });
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
const votes = await feedback.listIssueVotesForUser(id, req.actor.userId ?? "local-board");
|
|
3320
|
+
res.json(votes);
|
|
3321
|
+
});
|
|
3322
|
+
router.get("/issues/:id/feedback-traces", async (req, res) => {
|
|
3323
|
+
const id = req.params.id;
|
|
3324
|
+
const issue = await svc.getById(id);
|
|
3325
|
+
if (!issue) {
|
|
3326
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3330
|
+
if (req.actor.type !== "board") {
|
|
3331
|
+
res.status(403).json({ error: "Only board users can view feedback traces" });
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
const targetTypeRaw = typeof req.query.targetType === "string" ? req.query.targetType : undefined;
|
|
3335
|
+
const voteRaw = typeof req.query.vote === "string" ? req.query.vote : undefined;
|
|
3336
|
+
const statusRaw = typeof req.query.status === "string" ? req.query.status : undefined;
|
|
3337
|
+
const targetType = targetTypeRaw ? feedbackTargetTypeSchema.parse(targetTypeRaw) : undefined;
|
|
3338
|
+
const vote = voteRaw ? feedbackVoteValueSchema.parse(voteRaw) : undefined;
|
|
3339
|
+
const status = statusRaw ? feedbackTraceStatusSchema.parse(statusRaw) : undefined;
|
|
3340
|
+
const traces = await feedback.listFeedbackTraces({
|
|
3341
|
+
companyId: issue.companyId,
|
|
3342
|
+
issueId: issue.id,
|
|
3343
|
+
targetType,
|
|
3344
|
+
vote,
|
|
3345
|
+
status,
|
|
3346
|
+
from: parseDateQuery(req.query.from, "from"),
|
|
3347
|
+
to: parseDateQuery(req.query.to, "to"),
|
|
3348
|
+
sharedOnly: parseBooleanQuery(req.query.sharedOnly),
|
|
3349
|
+
includePayload: parseBooleanQuery(req.query.includePayload),
|
|
3350
|
+
});
|
|
3351
|
+
res.json(traces);
|
|
3352
|
+
});
|
|
3353
|
+
router.get("/feedback-traces/:traceId", async (req, res) => {
|
|
3354
|
+
const traceId = req.params.traceId;
|
|
3355
|
+
if (req.actor.type !== "board") {
|
|
3356
|
+
res.status(403).json({ error: "Only board users can view feedback traces" });
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
const includePayload = parseBooleanQuery(req.query.includePayload) || req.query.includePayload === undefined;
|
|
3360
|
+
const trace = await feedback.getFeedbackTraceById(traceId, includePayload);
|
|
3361
|
+
if (!trace || !actorCanAccessCompany(req, trace.companyId)) {
|
|
3362
|
+
res.status(404).json({ error: "Feedback trace not found" });
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
res.json(trace);
|
|
3366
|
+
});
|
|
3367
|
+
router.get("/feedback-traces/:traceId/bundle", async (req, res) => {
|
|
3368
|
+
const traceId = req.params.traceId;
|
|
3369
|
+
if (req.actor.type !== "board") {
|
|
3370
|
+
res.status(403).json({ error: "Only board users can view feedback trace bundles" });
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
const bundle = await feedback.getFeedbackTraceBundle(traceId);
|
|
3374
|
+
if (!bundle || !actorCanAccessCompany(req, bundle.companyId)) {
|
|
3375
|
+
res.status(404).json({ error: "Feedback trace not found" });
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
res.json(bundle);
|
|
3379
|
+
});
|
|
3380
|
+
router.post("/issues/:id/comments", validate(addIssueCommentSchema), async (req, res) => {
|
|
3381
|
+
const id = req.params.id;
|
|
3382
|
+
const issue = await svc.getById(id);
|
|
3383
|
+
if (!issue) {
|
|
3384
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3388
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
3389
|
+
return;
|
|
3390
|
+
if (!assertStructuredCommentFieldsAllowed(req, res, {
|
|
3391
|
+
presentation: req.body.presentation,
|
|
3392
|
+
metadata: req.body.metadata,
|
|
3393
|
+
}))
|
|
3394
|
+
return;
|
|
3395
|
+
const closedExecutionWorkspace = await getClosedIssueExecutionWorkspace(issue);
|
|
3396
|
+
if (closedExecutionWorkspace) {
|
|
3397
|
+
respondClosedIssueExecutionWorkspace(res, closedExecutionWorkspace);
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
const actor = getActorInfo(req);
|
|
3401
|
+
const reopenRequested = req.body.reopen === true;
|
|
3402
|
+
const resumeRequested = req.body.resume === true;
|
|
3403
|
+
const interruptRequested = req.body.interrupt === true;
|
|
3404
|
+
if (resumeRequested === true && !(await assertExplicitResumeIntentAllowed(req, res, issue)))
|
|
3405
|
+
return;
|
|
3406
|
+
if (resumeRequested !== true && reopenRequested === true && req.actor.type === "agent") {
|
|
3407
|
+
if (!(await assertExplicitResumeIntentAllowed(req, res, issue)))
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
const isClosed = isClosedIssueStatus(issue.status);
|
|
3411
|
+
const isBlocked = issue.status === "blocked";
|
|
3412
|
+
const explicitMoveToTodoRequested = reopenRequested || resumeRequested === true;
|
|
3413
|
+
const effectiveMoveToTodoRequested = explicitMoveToTodoRequested ||
|
|
3414
|
+
shouldImplicitlyMoveCommentedIssueToTodo({
|
|
3415
|
+
issueStatus: issue.status,
|
|
3416
|
+
assigneeAgentId: issue.assigneeAgentId,
|
|
3417
|
+
actorType: actor.actorType,
|
|
3418
|
+
actorId: actor.actorId,
|
|
3419
|
+
});
|
|
3420
|
+
const hasUnresolvedFirstClassBlockers = isBlocked && effectiveMoveToTodoRequested
|
|
3421
|
+
? (await svc.getDependencyReadiness(issue.id)).unresolvedBlockerCount > 0
|
|
3422
|
+
: false;
|
|
3423
|
+
if (resumeRequested === true && isBlocked && hasUnresolvedFirstClassBlockers) {
|
|
3424
|
+
res.status(409).json({ error: "Issue follow-up blocked by unresolved blockers" });
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
let reopened = false;
|
|
3428
|
+
let reopenFromStatus = null;
|
|
3429
|
+
let interruptedRunId = null;
|
|
3430
|
+
let currentIssue = issue;
|
|
3431
|
+
const commentReferenceSummaryBefore = await issueReferencesSvc.listIssueReferenceSummary(issue.id);
|
|
3432
|
+
if (effectiveMoveToTodoRequested && (isClosed || (isBlocked && !hasUnresolvedFirstClassBlockers))) {
|
|
3433
|
+
const reopenedIssue = await svc.update(id, { status: "todo" });
|
|
3434
|
+
if (!reopenedIssue) {
|
|
3435
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
reopened = true;
|
|
3439
|
+
reopenFromStatus = issue.status;
|
|
3440
|
+
currentIssue = reopenedIssue;
|
|
3441
|
+
await logActivity(db, {
|
|
3442
|
+
companyId: currentIssue.companyId,
|
|
3443
|
+
actorType: actor.actorType,
|
|
3444
|
+
actorId: actor.actorId,
|
|
3445
|
+
agentId: actor.agentId,
|
|
3446
|
+
runId: actor.runId,
|
|
3447
|
+
action: "issue.updated",
|
|
3448
|
+
entityType: "issue",
|
|
3449
|
+
entityId: currentIssue.id,
|
|
3450
|
+
details: {
|
|
3451
|
+
status: "todo",
|
|
3452
|
+
reopened: true,
|
|
3453
|
+
reopenedFrom: reopenFromStatus,
|
|
3454
|
+
source: "comment",
|
|
3455
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
3456
|
+
identifier: currentIssue.identifier,
|
|
3457
|
+
},
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
if (interruptRequested) {
|
|
3461
|
+
if (req.actor.type !== "board") {
|
|
3462
|
+
res.status(403).json({ error: "Only board users can interrupt active runs from issue comments" });
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
const runToInterrupt = await resolveActiveIssueRun(currentIssue);
|
|
3466
|
+
if (runToInterrupt) {
|
|
3467
|
+
const cancelled = await heartbeat.cancelRun(runToInterrupt.id);
|
|
3468
|
+
if (cancelled) {
|
|
3469
|
+
interruptedRunId = cancelled.id;
|
|
3470
|
+
await logActivity(db, {
|
|
3471
|
+
companyId: cancelled.companyId,
|
|
3472
|
+
actorType: actor.actorType,
|
|
3473
|
+
actorId: actor.actorId,
|
|
3474
|
+
agentId: actor.agentId,
|
|
3475
|
+
runId: actor.runId,
|
|
3476
|
+
action: "heartbeat.cancelled",
|
|
3477
|
+
entityType: "heartbeat_run",
|
|
3478
|
+
entityId: cancelled.id,
|
|
3479
|
+
details: { agentId: cancelled.agentId, source: "issue_comment_interrupt", issueId: currentIssue.id },
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
const comment = await svc.addComment(id, req.body.body, {
|
|
3485
|
+
agentId: actor.agentId ?? undefined,
|
|
3486
|
+
userId: actor.actorType === "user" ? actor.actorId : undefined,
|
|
3487
|
+
runId: actor.runId,
|
|
3488
|
+
}, {
|
|
3489
|
+
authorType: req.body.authorType ?? (actor.actorType === "agent" ? "agent" : "user"),
|
|
3490
|
+
presentation: req.body.presentation ?? null,
|
|
3491
|
+
metadata: req.body.metadata ?? null,
|
|
3492
|
+
});
|
|
3493
|
+
await issueReferencesSvc.syncComment(comment.id);
|
|
3494
|
+
const commentReferenceSummaryAfter = await issueReferencesSvc.listIssueReferenceSummary(currentIssue.id);
|
|
3495
|
+
const commentReferenceDiff = issueReferencesSvc.diffIssueReferenceSummary(commentReferenceSummaryBefore, commentReferenceSummaryAfter);
|
|
3496
|
+
if (actor.runId) {
|
|
3497
|
+
await heartbeat.reportRunActivity(actor.runId).catch((err) => logger.warn({ err, runId: actor.runId }, "failed to clear detached run warning after issue comment"));
|
|
3498
|
+
}
|
|
3499
|
+
await logActivity(db, {
|
|
3500
|
+
companyId: currentIssue.companyId,
|
|
3501
|
+
actorType: actor.actorType,
|
|
3502
|
+
actorId: actor.actorId,
|
|
3503
|
+
agentId: actor.agentId,
|
|
3504
|
+
runId: actor.runId,
|
|
3505
|
+
action: "issue.comment_added",
|
|
3506
|
+
entityType: "issue",
|
|
3507
|
+
entityId: currentIssue.id,
|
|
3508
|
+
details: {
|
|
3509
|
+
commentId: comment.id,
|
|
3510
|
+
bodySnippet: comment.body.slice(0, 120),
|
|
3511
|
+
identifier: currentIssue.identifier,
|
|
3512
|
+
issueTitle: currentIssue.title,
|
|
3513
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
3514
|
+
...(reopened ? { reopened: true, reopenedFrom: reopenFromStatus, source: "comment" } : {}),
|
|
3515
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
3516
|
+
...summarizeIssueReferenceActivityDetails({
|
|
3517
|
+
addedReferencedIssues: commentReferenceDiff.addedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
3518
|
+
removedReferencedIssues: commentReferenceDiff.removedReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
3519
|
+
currentReferencedIssues: commentReferenceDiff.currentReferencedIssues.map(summarizeIssueRelationForActivity),
|
|
3520
|
+
}),
|
|
3521
|
+
},
|
|
3522
|
+
});
|
|
3523
|
+
const expiredInteractions = await issueThreadInteractionService(db).expireRequestConfirmationsSupersededByComment(currentIssue, comment, {
|
|
3524
|
+
agentId: actor.agentId,
|
|
3525
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
3526
|
+
});
|
|
3527
|
+
await logExpiredRequestConfirmations({
|
|
3528
|
+
issue: currentIssue,
|
|
3529
|
+
interactions: expiredInteractions,
|
|
3530
|
+
actor,
|
|
3531
|
+
source: "issue.comment",
|
|
3532
|
+
});
|
|
3533
|
+
// Merge all wakeups from this comment into one enqueue per agent to avoid duplicate runs.
|
|
3534
|
+
void (async () => {
|
|
3535
|
+
const wakeups = new Map();
|
|
3536
|
+
const assigneeId = currentIssue.assigneeAgentId;
|
|
3537
|
+
const actorIsAgent = actor.actorType === "agent";
|
|
3538
|
+
const selfComment = actorIsAgent && actor.actorId === assigneeId;
|
|
3539
|
+
const skipWake = selfComment || isClosed;
|
|
3540
|
+
if (assigneeId && (reopened || !skipWake)) {
|
|
3541
|
+
if (reopened) {
|
|
3542
|
+
wakeups.set(assigneeId, {
|
|
3543
|
+
source: "automation",
|
|
3544
|
+
triggerDetail: "system",
|
|
3545
|
+
reason: "issue_reopened_via_comment",
|
|
3546
|
+
payload: {
|
|
3547
|
+
issueId: currentIssue.id,
|
|
3548
|
+
commentId: comment.id,
|
|
3549
|
+
reopenedFrom: reopenFromStatus,
|
|
3550
|
+
mutation: "comment",
|
|
3551
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
3552
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
3553
|
+
},
|
|
3554
|
+
requestedByActorType: actor.actorType,
|
|
3555
|
+
requestedByActorId: actor.actorId,
|
|
3556
|
+
contextSnapshot: {
|
|
3557
|
+
issueId: currentIssue.id,
|
|
3558
|
+
taskId: currentIssue.id,
|
|
3559
|
+
commentId: comment.id,
|
|
3560
|
+
wakeCommentId: comment.id,
|
|
3561
|
+
source: "issue.comment.reopen",
|
|
3562
|
+
wakeReason: "issue_reopened_via_comment",
|
|
3563
|
+
reopenedFrom: reopenFromStatus,
|
|
3564
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
3565
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
3566
|
+
},
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
else {
|
|
3570
|
+
wakeups.set(assigneeId, {
|
|
3571
|
+
source: "automation",
|
|
3572
|
+
triggerDetail: "system",
|
|
3573
|
+
reason: "issue_commented",
|
|
3574
|
+
payload: {
|
|
3575
|
+
issueId: currentIssue.id,
|
|
3576
|
+
commentId: comment.id,
|
|
3577
|
+
mutation: "comment",
|
|
3578
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
3579
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
3580
|
+
},
|
|
3581
|
+
requestedByActorType: actor.actorType,
|
|
3582
|
+
requestedByActorId: actor.actorId,
|
|
3583
|
+
contextSnapshot: {
|
|
3584
|
+
issueId: currentIssue.id,
|
|
3585
|
+
taskId: currentIssue.id,
|
|
3586
|
+
commentId: comment.id,
|
|
3587
|
+
wakeCommentId: comment.id,
|
|
3588
|
+
source: "issue.comment",
|
|
3589
|
+
wakeReason: "issue_commented",
|
|
3590
|
+
...(resumeRequested === true ? { resumeIntent: true, followUpRequested: true } : {}),
|
|
3591
|
+
...(interruptedRunId ? { interruptedRunId } : {}),
|
|
3592
|
+
},
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
let mentionedIds = [];
|
|
3597
|
+
try {
|
|
3598
|
+
mentionedIds = await svc.findMentionedAgents(issue.companyId, req.body.body);
|
|
3599
|
+
}
|
|
3600
|
+
catch (err) {
|
|
3601
|
+
logger.warn({ err, issueId: id }, "failed to resolve @-mentions");
|
|
3602
|
+
}
|
|
3603
|
+
for (const mentionedId of mentionedIds) {
|
|
3604
|
+
if (wakeups.has(mentionedId))
|
|
3605
|
+
continue;
|
|
3606
|
+
if (actorIsAgent && actor.actorId === mentionedId)
|
|
3607
|
+
continue;
|
|
3608
|
+
wakeups.set(mentionedId, {
|
|
3609
|
+
source: "automation",
|
|
3610
|
+
triggerDetail: "system",
|
|
3611
|
+
reason: "issue_comment_mentioned",
|
|
3612
|
+
payload: { issueId: id, commentId: comment.id },
|
|
3613
|
+
requestedByActorType: actor.actorType,
|
|
3614
|
+
requestedByActorId: actor.actorId,
|
|
3615
|
+
contextSnapshot: {
|
|
3616
|
+
issueId: id,
|
|
3617
|
+
taskId: id,
|
|
3618
|
+
commentId: comment.id,
|
|
3619
|
+
wakeCommentId: comment.id,
|
|
3620
|
+
wakeReason: "issue_comment_mentioned",
|
|
3621
|
+
source: "comment.mention",
|
|
3622
|
+
},
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
for (const [agentId, wakeup] of wakeups.entries()) {
|
|
3626
|
+
heartbeat
|
|
3627
|
+
.wakeup(agentId, wakeup)
|
|
3628
|
+
.catch((err) => logger.warn({ err, issueId: currentIssue.id, agentId }, "failed to wake agent on issue comment"));
|
|
3629
|
+
}
|
|
3630
|
+
})();
|
|
3631
|
+
res.status(201).json(comment);
|
|
3632
|
+
});
|
|
3633
|
+
router.post("/issues/:id/feedback-votes", validate(upsertIssueFeedbackVoteSchema), async (req, res) => {
|
|
3634
|
+
const id = req.params.id;
|
|
3635
|
+
const issue = await svc.getById(id);
|
|
3636
|
+
if (!issue) {
|
|
3637
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3641
|
+
if (req.actor.type !== "board") {
|
|
3642
|
+
res.status(403).json({ error: "Only board users can vote on AI feedback" });
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
const actor = getActorInfo(req);
|
|
3646
|
+
const result = await feedback.saveIssueVote({
|
|
3647
|
+
issueId: id,
|
|
3648
|
+
targetType: req.body.targetType,
|
|
3649
|
+
targetId: req.body.targetId,
|
|
3650
|
+
vote: req.body.vote,
|
|
3651
|
+
reason: req.body.reason,
|
|
3652
|
+
authorUserId: req.actor.userId ?? "local-board",
|
|
3653
|
+
allowSharing: req.body.allowSharing === true,
|
|
3654
|
+
});
|
|
3655
|
+
await logActivity(db, {
|
|
3656
|
+
companyId: issue.companyId,
|
|
3657
|
+
actorType: actor.actorType,
|
|
3658
|
+
actorId: actor.actorId,
|
|
3659
|
+
agentId: actor.agentId,
|
|
3660
|
+
runId: actor.runId,
|
|
3661
|
+
action: "issue.feedback_vote_saved",
|
|
3662
|
+
entityType: "issue",
|
|
3663
|
+
entityId: issue.id,
|
|
3664
|
+
details: {
|
|
3665
|
+
identifier: issue.identifier,
|
|
3666
|
+
targetType: result.vote.targetType,
|
|
3667
|
+
targetId: result.vote.targetId,
|
|
3668
|
+
vote: result.vote.vote,
|
|
3669
|
+
hasReason: Boolean(result.vote.reason),
|
|
3670
|
+
sharingEnabled: result.sharingEnabled,
|
|
3671
|
+
},
|
|
3672
|
+
});
|
|
3673
|
+
if (result.consentEnabledNow) {
|
|
3674
|
+
await logActivity(db, {
|
|
3675
|
+
companyId: issue.companyId,
|
|
3676
|
+
actorType: actor.actorType,
|
|
3677
|
+
actorId: actor.actorId,
|
|
3678
|
+
agentId: actor.agentId,
|
|
3679
|
+
runId: actor.runId,
|
|
3680
|
+
action: "company.feedback_data_sharing_updated",
|
|
3681
|
+
entityType: "company",
|
|
3682
|
+
entityId: issue.companyId,
|
|
3683
|
+
details: {
|
|
3684
|
+
feedbackDataSharingEnabled: true,
|
|
3685
|
+
source: "issue_feedback_vote",
|
|
3686
|
+
},
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3689
|
+
if (result.persistedSharingPreference) {
|
|
3690
|
+
const settings = await instanceSettings.get();
|
|
3691
|
+
const companyIds = await instanceSettings.listCompanyIds();
|
|
3692
|
+
await Promise.all(companyIds.map((companyId) => logActivity(db, {
|
|
3693
|
+
companyId,
|
|
3694
|
+
actorType: actor.actorType,
|
|
3695
|
+
actorId: actor.actorId,
|
|
3696
|
+
agentId: actor.agentId,
|
|
3697
|
+
runId: actor.runId,
|
|
3698
|
+
action: "instance.settings.general_updated",
|
|
3699
|
+
entityType: "instance_settings",
|
|
3700
|
+
entityId: settings.id,
|
|
3701
|
+
details: {
|
|
3702
|
+
general: settings.general,
|
|
3703
|
+
changedKeys: ["feedbackDataSharingPreference"],
|
|
3704
|
+
source: "issue_feedback_vote",
|
|
3705
|
+
},
|
|
3706
|
+
})));
|
|
3707
|
+
}
|
|
3708
|
+
if (result.sharingEnabled && result.traceId && feedbackExportService) {
|
|
3709
|
+
try {
|
|
3710
|
+
await feedbackExportService.flushPendingFeedbackTraces({
|
|
3711
|
+
companyId: issue.companyId,
|
|
3712
|
+
traceId: result.traceId,
|
|
3713
|
+
limit: 1,
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
catch (err) {
|
|
3717
|
+
logger.warn({ err, issueId: issue.id, traceId: result.traceId }, "failed to flush shared feedback trace immediately");
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
res.status(201).json(result.vote);
|
|
3721
|
+
});
|
|
3722
|
+
router.get("/issues/:id/attachments", async (req, res) => {
|
|
3723
|
+
const issueId = req.params.id;
|
|
3724
|
+
const issue = await svc.getById(issueId);
|
|
3725
|
+
if (!issue) {
|
|
3726
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3730
|
+
const attachments = await svc.listAttachments(issueId);
|
|
3731
|
+
res.json(attachments.map(withContentPath));
|
|
3732
|
+
});
|
|
3733
|
+
router.post("/companies/:companyId/issues/:issueId/attachments", async (req, res) => {
|
|
3734
|
+
const companyId = req.params.companyId;
|
|
3735
|
+
const issueId = req.params.issueId;
|
|
3736
|
+
assertCompanyAccess(req, companyId);
|
|
3737
|
+
const issue = await svc.getById(issueId);
|
|
3738
|
+
if (!issue) {
|
|
3739
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
if (issue.companyId !== companyId) {
|
|
3743
|
+
res.status(422).json({ error: "Issue does not belong to company" });
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
3747
|
+
return;
|
|
3748
|
+
const company = await companiesSvc.getById(companyId);
|
|
3749
|
+
const attachmentMaxBytes = normalizeIssueAttachmentMaxBytes(company?.attachmentMaxBytes);
|
|
3750
|
+
try {
|
|
3751
|
+
await runSingleFileUpload(req, res, attachmentMaxBytes);
|
|
3752
|
+
}
|
|
3753
|
+
catch (err) {
|
|
3754
|
+
if (err instanceof multer.MulterError) {
|
|
3755
|
+
if (err.code === "LIMIT_FILE_SIZE") {
|
|
3756
|
+
res.status(422).json({ error: `Attachment exceeds ${attachmentMaxBytes} bytes` });
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
res.status(400).json({ error: err.message });
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
throw err;
|
|
3763
|
+
}
|
|
3764
|
+
const file = req.file;
|
|
3765
|
+
if (!file) {
|
|
3766
|
+
res.status(400).json({ error: "Missing file field 'file'" });
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
const contentType = normalizeContentType(file.mimetype);
|
|
3770
|
+
if (file.buffer.length <= 0) {
|
|
3771
|
+
res.status(422).json({ error: "Attachment is empty" });
|
|
3772
|
+
return;
|
|
3773
|
+
}
|
|
3774
|
+
const parsedMeta = createIssueAttachmentMetadataSchema.safeParse(req.body ?? {});
|
|
3775
|
+
if (!parsedMeta.success) {
|
|
3776
|
+
res.status(400).json({ error: "Invalid attachment metadata", details: parsedMeta.error.issues });
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
const actor = getActorInfo(req);
|
|
3780
|
+
const stored = await storage.putFile({
|
|
3781
|
+
companyId,
|
|
3782
|
+
namespace: `issues/${issueId}`,
|
|
3783
|
+
originalFilename: file.originalname || null,
|
|
3784
|
+
contentType,
|
|
3785
|
+
body: file.buffer,
|
|
3786
|
+
});
|
|
3787
|
+
const attachment = await svc.createAttachment({
|
|
3788
|
+
issueId,
|
|
3789
|
+
issueCommentId: parsedMeta.data.issueCommentId ?? null,
|
|
3790
|
+
provider: stored.provider,
|
|
3791
|
+
objectKey: stored.objectKey,
|
|
3792
|
+
contentType: stored.contentType,
|
|
3793
|
+
byteSize: stored.byteSize,
|
|
3794
|
+
sha256: stored.sha256,
|
|
3795
|
+
originalFilename: stored.originalFilename,
|
|
3796
|
+
createdByAgentId: actor.agentId,
|
|
3797
|
+
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
3798
|
+
});
|
|
3799
|
+
await logActivity(db, {
|
|
3800
|
+
companyId,
|
|
3801
|
+
actorType: actor.actorType,
|
|
3802
|
+
actorId: actor.actorId,
|
|
3803
|
+
agentId: actor.agentId,
|
|
3804
|
+
runId: actor.runId,
|
|
3805
|
+
action: "issue.attachment_added",
|
|
3806
|
+
entityType: "issue",
|
|
3807
|
+
entityId: issueId,
|
|
3808
|
+
details: {
|
|
3809
|
+
attachmentId: attachment.id,
|
|
3810
|
+
originalFilename: attachment.originalFilename,
|
|
3811
|
+
contentType: attachment.contentType,
|
|
3812
|
+
byteSize: attachment.byteSize,
|
|
3813
|
+
},
|
|
3814
|
+
});
|
|
3815
|
+
res.status(201).json(withContentPath(attachment));
|
|
3816
|
+
});
|
|
3817
|
+
router.get("/attachments/:attachmentId/content", async (req, res, next) => {
|
|
3818
|
+
const attachmentId = req.params.attachmentId;
|
|
3819
|
+
const attachment = await svc.getAttachmentById(attachmentId);
|
|
3820
|
+
if (!attachment) {
|
|
3821
|
+
res.status(404).json({ error: "Attachment not found" });
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
assertCompanyAccess(req, attachment.companyId);
|
|
3825
|
+
const object = await storage.getObject(attachment.companyId, attachment.objectKey);
|
|
3826
|
+
const responseContentType = normalizeContentType(attachment.contentType || object.contentType);
|
|
3827
|
+
res.setHeader("Content-Type", responseContentType);
|
|
3828
|
+
res.setHeader("Content-Length", String(attachment.byteSize || object.contentLength || 0));
|
|
3829
|
+
res.setHeader("Cache-Control", "private, max-age=60");
|
|
3830
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
3831
|
+
if (responseContentType === SVG_CONTENT_TYPE) {
|
|
3832
|
+
res.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'");
|
|
3833
|
+
}
|
|
3834
|
+
const filename = attachment.originalFilename ?? "attachment";
|
|
3835
|
+
const disposition = isInlineAttachmentContentType(responseContentType) ? "inline" : "attachment";
|
|
3836
|
+
res.setHeader("Content-Disposition", `${disposition}; filename=\"${filename.replaceAll("\"", "")}\"`);
|
|
3837
|
+
object.stream.on("error", (err) => {
|
|
3838
|
+
next(err);
|
|
3839
|
+
});
|
|
3840
|
+
object.stream.pipe(res);
|
|
3841
|
+
});
|
|
3842
|
+
router.delete("/attachments/:attachmentId", async (req, res) => {
|
|
3843
|
+
const attachmentId = req.params.attachmentId;
|
|
3844
|
+
const attachment = await svc.getAttachmentById(attachmentId);
|
|
3845
|
+
if (!attachment) {
|
|
3846
|
+
res.status(404).json({ error: "Attachment not found" });
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
assertCompanyAccess(req, attachment.companyId);
|
|
3850
|
+
const issue = await svc.getById(attachment.issueId);
|
|
3851
|
+
if (!issue) {
|
|
3852
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
if (!(await assertAgentIssueMutationAllowed(req, res, issue)))
|
|
3856
|
+
return;
|
|
3857
|
+
try {
|
|
3858
|
+
await storage.deleteObject(attachment.companyId, attachment.objectKey);
|
|
3859
|
+
}
|
|
3860
|
+
catch (err) {
|
|
3861
|
+
logger.warn({ err, attachmentId }, "storage delete failed while removing attachment");
|
|
3862
|
+
}
|
|
3863
|
+
const removed = await svc.removeAttachment(attachmentId);
|
|
3864
|
+
if (!removed) {
|
|
3865
|
+
res.status(404).json({ error: "Attachment not found" });
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
const actor = getActorInfo(req);
|
|
3869
|
+
await logActivity(db, {
|
|
3870
|
+
companyId: removed.companyId,
|
|
3871
|
+
actorType: actor.actorType,
|
|
3872
|
+
actorId: actor.actorId,
|
|
3873
|
+
agentId: actor.agentId,
|
|
3874
|
+
runId: actor.runId,
|
|
3875
|
+
action: "issue.attachment_removed",
|
|
3876
|
+
entityType: "issue",
|
|
3877
|
+
entityId: removed.issueId,
|
|
3878
|
+
details: {
|
|
3879
|
+
attachmentId: removed.id,
|
|
3880
|
+
},
|
|
3881
|
+
});
|
|
3882
|
+
res.json({ ok: true });
|
|
3883
|
+
});
|
|
3884
|
+
return router;
|
|
3885
|
+
}
|
|
3886
|
+
//# sourceMappingURL=issues.js.map
|