@slaw-ai/server 2026.611.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/LICENSE +26 -0
- 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 +18 -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 +598 -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 +39 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +386 -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 +98 -0
- package/dist/attachment-types.js.map +1 -0
- package/dist/auth/better-auth.d.ts +40 -0
- package/dist/auth/better-auth.d.ts.map +1 -0
- package/dist/auth/better-auth.js +148 -0
- package/dist/auth/better-auth.js.map +1 -0
- package/dist/config-file.d.ts +24 -0
- package/dist/config-file.d.ts.map +1 -0
- package/dist/config-file.js +73 -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 +247 -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 +101 -0
- package/dist/dev-runner-worktree.js.map +1 -0
- package/dist/dev-server-status.d.ts +33 -0
- package/dist/dev-server-status.d.ts.map +1 -0
- package/dist/dev-server-status.js +89 -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/first-admin-claim.d.ts +17 -0
- package/dist/first-admin-claim.d.ts.map +1 -0
- package/dist/first-admin-claim.js +30 -0
- package/dist/first-admin-claim.js.map +1 -0
- package/dist/home-paths.d.ts +15 -0
- package/dist/home-paths.d.ts.map +1 -0
- package/dist/home-paths.js +48 -0
- package/dist/home-paths.js.map +1 -0
- package/dist/http/body-limits.d.ts +4 -0
- package/dist/http/body-limits.d.ts.map +1 -0
- package/dist/http/body-limits.js +4 -0
- package/dist/http/body-limits.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +786 -0
- package/dist/index.js.map +1 -0
- package/dist/instance-claim.d.ts +23 -0
- package/dist/instance-claim.d.ts.map +1 -0
- package/dist/instance-claim.js +126 -0
- package/dist/instance-claim.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/error-handler.d.ts +17 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +46 -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/operator-mutation-guard.d.ts +3 -0
- package/dist/middleware/operator-mutation-guard.d.ts.map +1 -0
- package/dist/middleware/operator-mutation-guard.js +70 -0
- package/dist/middleware/operator-mutation-guard.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/default/AGENTS.md +18 -0
- package/dist/onboarding-assets/squad_lead/AGENTS.md +61 -0
- package/dist/onboarding-assets/squad_lead/HEARTBEAT.md +85 -0
- package/dist/onboarding-assets/squad_lead/SOUL.md +33 -0
- package/dist/onboarding-assets/squad_lead/TOOLS.md +3 -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 +125 -0
- package/dist/redaction.js.map +1 -0
- package/dist/routes/access.d.ts +75 -0
- package/dist/routes/access.d.ts.map +1 -0
- package/dist/routes/access.js +3070 -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 +539 -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 +2733 -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/botfather.d.ts +9 -0
- package/dist/routes/botfather.d.ts.map +1 -0
- package/dist/routes/botfather.js +127 -0
- package/dist/routes/botfather.js.map +1 -0
- package/dist/routes/cloud-upstreams.d.ts +5 -0
- package/dist/routes/cloud-upstreams.d.ts.map +1 -0
- package/dist/routes/cloud-upstreams.js +103 -0
- package/dist/routes/cloud-upstreams.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 +414 -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 +537 -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 +95 -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 +143 -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 +24 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +24 -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 +373 -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 +15 -0
- package/dist/routes/issues.d.ts.map +1 -0
- package/dist/routes/issues.js +5276 -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/openapi.d.ts +4 -0
- package/dist/routes/openapi.d.ts.map +1 -0
- package/dist/routes/openapi.js +3284 -0
- package/dist/routes/openapi.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 +2390 -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/resource-memberships.d.ts +3 -0
- package/dist/routes/resource-memberships.d.ts.map +1 -0
- package/dist/routes/resource-memberships.js +97 -0
- package/dist/routes/resource-memberships.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 +411 -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 +419 -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/squad-import-paths.d.ts +3 -0
- package/dist/routes/squad-import-paths.d.ts.map +1 -0
- package/dist/routes/squad-import-paths.js +3 -0
- package/dist/routes/squad-import-paths.js.map +1 -0
- package/dist/routes/squad-skills.d.ts +3 -0
- package/dist/routes/squad-skills.d.ts.map +1 -0
- package/dist/routes/squad-skills.js +366 -0
- package/dist/routes/squad-skills.js.map +1 -0
- package/dist/routes/squads.d.ts +4 -0
- package/dist/routes/squads.d.ts.map +1 -0
- package/dist/routes/squads.js +450 -0
- package/dist/routes/squads.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/aws-secrets-manager-provider.d.ts +87 -0
- package/dist/secrets/aws-secrets-manager-provider.d.ts.map +1 -0
- package/dist/secrets/aws-secrets-manager-provider.js +964 -0
- package/dist/secrets/aws-secrets-manager-provider.js.map +1 -0
- package/dist/secrets/configured-provider.d.ts +3 -0
- package/dist/secrets/configured-provider.d.ts.map +1 -0
- package/dist/secrets/configured-provider.js +8 -0
- package/dist/secrets/configured-provider.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 +71 -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 +244 -0
- package/dist/secrets/local-encrypted-provider.js.map +1 -0
- package/dist/secrets/provider-registry.d.ts +6 -0
- package/dist/secrets/provider-registry.d.ts.map +1 -0
- package/dist/secrets/provider-registry.js +24 -0
- package/dist/secrets/provider-registry.js.map +1 -0
- package/dist/secrets/types.d.ts +138 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +36 -0
- package/dist/secrets/types.js.map +1 -0
- package/dist/services/access.d.ts +184 -0
- package/dist/services/access.d.ts.map +1 -0
- package/dist/services/access.js +542 -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 +20 -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/authorization.d.ts +67 -0
- package/dist/services/authorization.d.ts.map +1 -0
- package/dist/services/authorization.js +608 -0
- package/dist/services/authorization.js.map +1 -0
- package/dist/services/botfather/authoring-lock.d.ts +17 -0
- package/dist/services/botfather/authoring-lock.d.ts.map +1 -0
- package/dist/services/botfather/authoring-lock.js +23 -0
- package/dist/services/botfather/authoring-lock.js.map +1 -0
- package/dist/services/botfather/authoring-lock.test.d.ts +2 -0
- package/dist/services/botfather/authoring-lock.test.d.ts.map +1 -0
- package/dist/services/botfather/authoring-lock.test.js +25 -0
- package/dist/services/botfather/authoring-lock.test.js.map +1 -0
- package/dist/services/botfather/client.d.ts +26 -0
- package/dist/services/botfather/client.d.ts.map +1 -0
- package/dist/services/botfather/client.js +113 -0
- package/dist/services/botfather/client.js.map +1 -0
- package/dist/services/botfather/credentials.d.ts +15 -0
- package/dist/services/botfather/credentials.d.ts.map +1 -0
- package/dist/services/botfather/credentials.js +39 -0
- package/dist/services/botfather/credentials.js.map +1 -0
- package/dist/services/botfather/enrollment.d.ts +49 -0
- package/dist/services/botfather/enrollment.d.ts.map +1 -0
- package/dist/services/botfather/enrollment.js +145 -0
- package/dist/services/botfather/enrollment.js.map +1 -0
- package/dist/services/botfather/instance-limit-enforcement.d.ts +44 -0
- package/dist/services/botfather/instance-limit-enforcement.d.ts.map +1 -0
- package/dist/services/botfather/instance-limit-enforcement.js +83 -0
- package/dist/services/botfather/instance-limit-enforcement.js.map +1 -0
- package/dist/services/botfather/instance-limit-enforcement.test.d.ts +2 -0
- package/dist/services/botfather/instance-limit-enforcement.test.d.ts.map +1 -0
- package/dist/services/botfather/instance-limit-enforcement.test.js +66 -0
- package/dist/services/botfather/instance-limit-enforcement.test.js.map +1 -0
- package/dist/services/botfather/limits-store.d.ts +36 -0
- package/dist/services/botfather/limits-store.d.ts.map +1 -0
- package/dist/services/botfather/limits-store.js +94 -0
- package/dist/services/botfather/limits-store.js.map +1 -0
- package/dist/services/botfather/limits-store.test.d.ts +2 -0
- package/dist/services/botfather/limits-store.test.d.ts.map +1 -0
- package/dist/services/botfather/limits-store.test.js +70 -0
- package/dist/services/botfather/limits-store.test.js.map +1 -0
- package/dist/services/botfather/reporter.d.ts +41 -0
- package/dist/services/botfather/reporter.d.ts.map +1 -0
- package/dist/services/botfather/reporter.js +448 -0
- package/dist/services/botfather/reporter.js.map +1 -0
- package/dist/services/botfather/service.d.ts +84 -0
- package/dist/services/botfather/service.d.ts.map +1 -0
- package/dist/services/botfather/service.js +229 -0
- package/dist/services/botfather/service.js.map +1 -0
- package/dist/services/botfather/service.test.d.ts +2 -0
- package/dist/services/botfather/service.test.d.ts.map +1 -0
- package/dist/services/botfather/service.test.js +120 -0
- package/dist/services/botfather/service.test.js.map +1 -0
- package/dist/services/botfather/skill-catalog.d.ts +28 -0
- package/dist/services/botfather/skill-catalog.d.ts.map +1 -0
- package/dist/services/botfather/skill-catalog.js +101 -0
- package/dist/services/botfather/skill-catalog.js.map +1 -0
- package/dist/services/botfather/skill-catalog.test.d.ts +2 -0
- package/dist/services/botfather/skill-catalog.test.d.ts.map +1 -0
- package/dist/services/botfather/skill-catalog.test.js +151 -0
- package/dist/services/botfather/skill-catalog.test.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 +833 -0
- package/dist/services/budgets.js.map +1 -0
- package/dist/services/catalog-provenance.d.ts +7 -0
- package/dist/services/catalog-provenance.d.ts.map +1 -0
- package/dist/services/catalog-provenance.js +64 -0
- package/dist/services/catalog-provenance.js.map +1 -0
- package/dist/services/cloud-upstreams.d.ts +42 -0
- package/dist/services/cloud-upstreams.d.ts.map +1 -0
- package/dist/services/cloud-upstreams.js +1071 -0
- package/dist/services/cloud-upstreams.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/document-annotations.d.ts +160 -0
- package/dist/services/document-annotations.d.ts.map +1 -0
- package/dist/services/document-annotations.js +324 -0
- package/dist/services/document-annotations.js.map +1 -0
- package/dist/services/documents.d.ts +347 -0
- package/dist/services/documents.d.ts.map +1 -0
- package/dist/services/documents.js +638 -0
- package/dist/services/documents.js.map +1 -0
- package/dist/services/environment-config.d.ts +55 -0
- package/dist/services/environment-config.d.ts.map +1 -0
- package/dist/services/environment-config.js +441 -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 +121 -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 +968 -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 +42 -0
- package/dist/services/execution-workspace-policy.d.ts.map +1 -0
- package/dist/services/execution-workspace-policy.js +262 -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 +645 -0
- package/dist/services/execution-workspaces.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-circuit-breaker.d.ts +89 -0
- package/dist/services/heartbeat-circuit-breaker.d.ts.map +1 -0
- package/dist/services/heartbeat-circuit-breaker.js +156 -0
- package/dist/services/heartbeat-circuit-breaker.js.map +1 -0
- package/dist/services/heartbeat-circuit-breaker.test.d.ts +2 -0
- package/dist/services/heartbeat-circuit-breaker.test.d.ts.map +1 -0
- package/dist/services/heartbeat-circuit-breaker.test.js +97 -0
- package/dist/services/heartbeat-circuit-breaker.test.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 +1578 -0
- package/dist/services/heartbeat.d.ts.map +1 -0
- package/dist/services/heartbeat.js +8274 -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 +50 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +49 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/instance-settings.d.ts +12 -0
- package/dist/services/instance-settings.d.ts.map +1 -0
- package/dist/services/instance-settings.js +142 -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 +71 -0
- package/dist/services/issue-continuation-summary.d.ts.map +1 -0
- package/dist/services/issue-continuation-summary.js +222 -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-recovery-actions.d.ts +40 -0
- package/dist/services/issue-recovery-actions.d.ts.map +1 -0
- package/dist/services/issue-recovery-actions.js +204 -0
- package/dist/services/issue-recovery-actions.js.map +1 -0
- package/dist/services/issue-references.d.ts +22 -0
- package/dist/services/issue-references.d.ts.map +1 -0
- package/dist/services/issue-references.js +341 -0
- package/dist/services/issue-references.js.map +1 -0
- package/dist/services/issue-thread-interactions.d.ts +81 -0
- package/dist/services/issue-thread-interactions.d.ts.map +1 -0
- package/dist/services/issue-thread-interactions.js +1017 -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 +898 -0
- package/dist/services/issues.d.ts.map +1 -0
- package/dist/services/issues.js +4705 -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/operator-auth.d.ts +271 -0
- package/dist/services/operator-auth.d.ts.map +1 -0
- package/dist/services/operator-auth.js +361 -0
- package/dist/services/operator-auth.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 +314 -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 +475 -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 +246 -0
- package/dist/services/plugin-dev-watcher.js.map +1 -0
- package/dist/services/plugin-environment-driver.d.ts +126 -0
- package/dist/services/plugin-environment-driver.d.ts.map +1 -0
- package/dist/services/plugin-environment-driver.js +226 -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 +2460 -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 +501 -0
- package/dist/services/plugin-lifecycle.js.map +1 -0
- package/dist/services/plugin-loader.d.ts +453 -0
- package/dist/services/plugin-loader.d.ts.map +1 -0
- package/dist/services/plugin-loader.js +1295 -0
- package/dist/services/plugin-loader.js.map +1 -0
- package/dist/services/plugin-local-folders.d.ts +49 -0
- package/dist/services/plugin-local-folders.d.ts.map +1 -0
- package/dist/services/plugin-local-folders.js +510 -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 +457 -0
- package/dist/services/plugin-managed-agents.js.map +1 -0
- package/dist/services/plugin-managed-routines.d.ts +42 -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-managed-skills.d.ts +14 -0
- package/dist/services/plugin-managed-skills.d.ts.map +1 -0
- package/dist/services/plugin-managed-skills.js +264 -0
- package/dist/services/plugin-managed-skills.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 +83 -0
- package/dist/services/plugin-secrets-handler.d.ts.map +1 -0
- package/dist/services/plugin-secrets-handler.js +168 -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 +181 -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 +942 -0
- package/dist/services/plugin-worker-manager.js.map +1 -0
- package/dist/services/portable-path.d.ts +2 -0
- package/dist/services/portable-path.d.ts.map +1 -0
- package/dist/services/portable-path.js +15 -0
- package/dist/services/portable-path.js.map +1 -0
- package/dist/services/principal-access-compatibility.d.ts +26 -0
- package/dist/services/principal-access-compatibility.d.ts.map +1 -0
- package/dist/services/principal-access-compatibility.js +94 -0
- package/dist/services/principal-access-compatibility.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 +356 -0
- package/dist/services/recovery/issue-graph-liveness.js.map +1 -0
- package/dist/services/recovery/model-profile-hint.d.ts +21 -0
- package/dist/services/recovery/model-profile-hint.d.ts.map +1 -0
- package/dist/services/recovery/model-profile-hint.js +36 -0
- package/dist/services/recovery/model-profile-hint.js.map +1 -0
- package/dist/services/recovery/model-profile-hint.test.d.ts +2 -0
- package/dist/services/recovery/model-profile-hint.test.d.ts.map +1 -0
- package/dist/services/recovery/model-profile-hint.test.js +38 -0
- package/dist/services/recovery/model-profile-hint.test.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 +258 -0
- package/dist/services/recovery/service.d.ts.map +1 -0
- package/dist/services/recovery/service.js +2892 -0
- package/dist/services/recovery/service.js.map +1 -0
- package/dist/services/recovery/successful-run-handoff.d.ts +89 -0
- package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -0
- package/dist/services/recovery/successful-run-handoff.js +304 -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 +276 -0
- package/dist/services/recovery/successful-run-handoff.test.js.map +1 -0
- package/dist/services/resource-memberships.d.ts +55 -0
- package/dist/services/resource-memberships.d.ts.map +1 -0
- package/dist/services/resource-memberships.js +213 -0
- package/dist/services/resource-memberships.js.map +1 -0
- package/dist/services/routines.d.ts +170 -0
- package/dist/services/routines.d.ts.map +1 -0
- package/dist/services/routines.js +2015 -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 +1991 -0
- package/dist/services/secrets.d.ts.map +1 -0
- package/dist/services/secrets.js +1781 -0
- package/dist/services/secrets.js.map +1 -0
- package/dist/services/session-workspace-cwd.d.ts +2 -0
- package/dist/services/session-workspace-cwd.d.ts.map +1 -0
- package/dist/services/session-workspace-cwd.js +24 -0
- package/dist/services/session-workspace-cwd.js.map +1 -0
- package/dist/services/session-workspace-cwd.test.d.ts +2 -0
- package/dist/services/session-workspace-cwd.test.d.ts.map +1 -0
- package/dist/services/session-workspace-cwd.test.js +25 -0
- package/dist/services/session-workspace-cwd.test.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/skills-catalog.d.ts +14 -0
- package/dist/services/skills-catalog.d.ts.map +1 -0
- package/dist/services/skills-catalog.js +171 -0
- package/dist/services/skills-catalog.js.map +1 -0
- package/dist/services/squad-export-readme.d.ts +17 -0
- package/dist/services/squad-export-readme.d.ts.map +1 -0
- package/dist/services/squad-export-readme.js +148 -0
- package/dist/services/squad-export-readme.js.map +1 -0
- package/dist/services/squad-member-roles.d.ts +9 -0
- package/dist/services/squad-member-roles.d.ts.map +1 -0
- package/dist/services/squad-member-roles.js +48 -0
- package/dist/services/squad-member-roles.js.map +1 -0
- package/dist/services/squad-portability.d.ts +24 -0
- package/dist/services/squad-portability.d.ts.map +1 -0
- package/dist/services/squad-portability.js +4093 -0
- package/dist/services/squad-portability.js.map +1 -0
- package/dist/services/squad-search-rate-limit.d.ts +22 -0
- package/dist/services/squad-search-rate-limit.d.ts.map +1 -0
- package/dist/services/squad-search-rate-limit.js +38 -0
- package/dist/services/squad-search-rate-limit.js.map +1 -0
- package/dist/services/squad-search.d.ts +8 -0
- package/dist/services/squad-search.d.ts.map +1 -0
- package/dist/services/squad-search.js +626 -0
- package/dist/services/squad-search.js.map +1 -0
- package/dist/services/squad-skills.d.ts +107 -0
- package/dist/services/squad-skills.d.ts.map +1 -0
- package/dist/services/squad-skills.js +3044 -0
- package/dist/services/squad-skills.js.map +1 -0
- package/dist/services/squads.d.ts +154 -0
- package/dist/services/squads.d.ts.map +1 -0
- package/dist/services/squads.js +278 -0
- package/dist/services/squads.js.map +1 -0
- package/dist/services/wake-cycle-guard.d.ts +44 -0
- package/dist/services/wake-cycle-guard.d.ts.map +1 -0
- package/dist/services/wake-cycle-guard.js +79 -0
- package/dist/services/wake-cycle-guard.js.map +1 -0
- package/dist/services/wake-cycle-guard.test.d.ts +2 -0
- package/dist/services/wake-cycle-guard.test.d.ts.map +1 -0
- package/dist/services/wake-cycle-guard.test.js +67 -0
- package/dist/services/wake-cycle-guard.test.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 +252 -0
- package/dist/services/workspace-runtime.d.ts.map +1 -0
- package/dist/services/workspace-runtime.js +2519 -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/static-index-html.d.ts +2 -0
- package/dist/static-index-html.d.ts.map +1 -0
- package/dist/static-index-html.js +7 -0
- package/dist/static-index-html.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 +85 -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 +124 -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 +59 -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/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 +373 -0
- package/dist/worktree-config.js.map +1 -0
- package/package.json +92 -0
- package/skills/diagnose-why-work-stopped/SKILL.md +161 -0
- package/skills/para-memory-files/SKILL.md +104 -0
- package/skills/para-memory-files/references/schemas.md +35 -0
- package/skills/slaw/SKILL.md +371 -0
- package/skills/slaw/references/api-reference.md +879 -0
- package/skills/slaw/references/artifacts.md +44 -0
- package/skills/slaw/references/issue-workspaces.md +80 -0
- package/skills/slaw/references/routines.md +187 -0
- package/skills/slaw/references/squad-skills.md +258 -0
- package/skills/slaw/references/workflows.md +113 -0
- package/skills/slaw/scripts/slaw-upload-artifact.sh +371 -0
- package/skills/slaw-converting-plans-to-tasks/SKILL.md +42 -0
- package/skills/slaw-create-agent/SKILL.md +163 -0
- package/skills/slaw-create-agent/references/agent-instruction-templates.md +123 -0
- package/skills/slaw-create-agent/references/agents/coder.md +64 -0
- package/skills/slaw-create-agent/references/agents/qa.md +88 -0
- package/skills/slaw-create-agent/references/agents/securityengineer.md +135 -0
- package/skills/slaw-create-agent/references/agents/uxdesigner.md +115 -0
- package/skills/slaw-create-agent/references/api-reference.md +110 -0
- package/skills/slaw-create-agent/references/baseline-role-guide.md +168 -0
- package/skills/slaw-create-agent/references/draft-review-checklist.md +95 -0
- package/skills/slaw-create-plugin/SKILL.md +154 -0
- package/skills/slaw-dev/SKILL.md +267 -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/apl-B4CMkyY2.js +1 -0
- package/ui-dist/assets/arc-xbLjL0VN.js +1 -0
- package/ui-dist/assets/architectureDiagram-3BPJPVTR-KcFd4B-U.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-GPEHLZMM-CSD4otEL.js +132 -0
- package/ui-dist/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/ui-dist/assets/c4Diagram-AAUBKEIU-Cre_NEHp.js +10 -0
- package/ui-dist/assets/channel-BFN8obi8.js +1 -0
- package/ui-dist/assets/chunk-2J33WTMH-CssLBsbh.js +1 -0
- package/ui-dist/assets/chunk-4BX2VUAB-DjiavNFv.js +1 -0
- package/ui-dist/assets/chunk-55IACEB6-C_F0yeYq.js +1 -0
- package/ui-dist/assets/chunk-727SXJPM-B1FAOW4a.js +206 -0
- package/ui-dist/assets/chunk-AQP2D5EJ-Do1241W-.js +231 -0
- package/ui-dist/assets/chunk-FMBD7UC4-BQRrOMZD.js +15 -0
- package/ui-dist/assets/chunk-ND2GUHAM-BPSt3kZ1.js +1 -0
- package/ui-dist/assets/chunk-QZHKN3VN-BSpmhWDD.js +1 -0
- package/ui-dist/assets/classDiagram-4FO5ZUOK-1Ay0zFCU.js +1 -0
- package/ui-dist/assets/classDiagram-v2-Q7XG4LA2-1Ay0zFCU.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/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-CK2f2Te4.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-D8joxN9f.js +321 -0
- package/ui-dist/assets/d-pRatUO7H.js +1 -0
- package/ui-dist/assets/dagre-BM42HDAG-DaOXTN9-.js +4 -0
- package/ui-dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/ui-dist/assets/diagram-2AECGRRQ-D0ScQUGy.js +43 -0
- package/ui-dist/assets/diagram-5GNKFQAL-7mH4Cncd.js +10 -0
- package/ui-dist/assets/diagram-KO2AKTUF-aA9kuK-7.js +3 -0
- package/ui-dist/assets/diagram-LMA3HP47-C9UXfmdK.js +24 -0
- package/ui-dist/assets/diagram-OG6HWLK6-Ba3U-x1r.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-TEJ5UH35-CmskPKH1.js +85 -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-I6XJVG4X-B0iEPqGd.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-6RSMTGT7-DtpxlgWQ.js +292 -0
- package/ui-dist/assets/gas-Bneqetm1.js +1 -0
- package/ui-dist/assets/gherkin-heZmZLOM.js +1 -0
- package/ui-dist/assets/gitGraphDiagram-PVQCEYII-VefBjqya.js +106 -0
- package/ui-dist/assets/graph-CAnANduQ.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-B9KxOFt-.js +1 -0
- package/ui-dist/assets/index-BMPCuc-W.js +1 -0
- package/ui-dist/assets/index-Bbfs2D7R.js +1 -0
- package/ui-dist/assets/index-BrgHE5Lg.js +1 -0
- package/ui-dist/assets/index-C5q-Cwlp.js +7 -0
- package/ui-dist/assets/index-C6LpKpr3.js +1 -0
- package/ui-dist/assets/index-CIzt5DFV.js +1 -0
- package/ui-dist/assets/index-CRwAuYPj.js +1 -0
- package/ui-dist/assets/index-CTEnIXsJ.js +1 -0
- package/ui-dist/assets/index-CXGemv2V.js +1 -0
- package/ui-dist/assets/index-ClDiS51u.js +1 -0
- package/ui-dist/assets/index-CvKYfvpz.js +1 -0
- package/ui-dist/assets/index-D2IqxlXD.js +1 -0
- package/ui-dist/assets/index-D97fJMFR.js +522 -0
- package/ui-dist/assets/index-DDHdUa2f.js +1 -0
- package/ui-dist/assets/index-DMZ0QXqi.js +1 -0
- package/ui-dist/assets/index-DMi4KpxO.js +6 -0
- package/ui-dist/assets/index-DZB48Gve.js +1 -0
- package/ui-dist/assets/index-Drr9zRdK.css +1 -0
- package/ui-dist/assets/index-DtGqpE43.js +1 -0
- package/ui-dist/assets/index-Du18kURt.js +2 -0
- package/ui-dist/assets/index-KaLXuTqA.js +1 -0
- package/ui-dist/assets/index-j5NgiILm.js +13 -0
- package/ui-dist/assets/index-u0SfLZ3g.js +3 -0
- package/ui-dist/assets/infoDiagram-5YYISTIA-D2OGH-dO.js +2 -0
- package/ui-dist/assets/init-Gi6I4Gst.js +1 -0
- package/ui-dist/assets/ishikawaDiagram-YF4QCWOH-CnMf3BJj.js +70 -0
- package/ui-dist/assets/javascript-iXu5QeM3.js +1 -0
- package/ui-dist/assets/journeyDiagram-JHISSGLW-BaXdD53T.js +139 -0
- package/ui-dist/assets/julia-DuME0IfC.js +1 -0
- package/ui-dist/assets/kanban-definition-UN3LZRKU-Brt7LjHm.js +89 -0
- package/ui-dist/assets/katex-yT8l5JNH.js +257 -0
- package/ui-dist/assets/layout-DGIYPm2g.js +1 -0
- package/ui-dist/assets/linear-536T6Mkh.js +1 -0
- package/ui-dist/assets/livescript-BwQOo05w.js +1 -0
- package/ui-dist/assets/lua-VAEuO923.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-CURTLVBm.js +303 -0
- package/ui-dist/assets/mindmap-definition-RKZ34NQL-S2tDCU-U.js +96 -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-4H26LBE5-DD_Ih32z.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-W4KKPZXB-DA5BPBIK.js +7 -0
- package/ui-dist/assets/r-B6wPVr8A.js +1 -0
- package/ui-dist/assets/requirementDiagram-4Y6WPE33-Em8SPCro.js +84 -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-5OEKKPKP-BJVC4haY.js +40 -0
- package/ui-dist/assets/sas-B4kiWyti.js +1 -0
- package/ui-dist/assets/scheme-C41bIUwD.js +1 -0
- package/ui-dist/assets/sequenceDiagram-3UESZ5HK-Cskntadf.js +162 -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-AJRCARHV-CxlfdaOi.js +1 -0
- package/ui-dist/assets/stateDiagram-v2-BHNVJYJU-eTgftUjW.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-PNZ67QCA-LOdaWSSa.js +120 -0
- package/ui-dist/assets/toml-Bm5Em-hy.js +1 -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/vennDiagram-CIIHVFJN-CJ4ji6B3.js +34 -0
- package/ui-dist/assets/verilog-C6RDOZhf.js +1 -0
- package/ui-dist/assets/vhdl-lSbBsy5d.js +1 -0
- package/ui-dist/assets/wardley-L42UT6IY-CxnVdUVH.js +153 -0
- package/ui-dist/assets/wardleyDiagram-YWT4CUSO-CgGDttpl.js +78 -0
- package/ui-dist/assets/webidl-ZXfAyPTL.js +1 -0
- package/ui-dist/assets/xquery-DzFWVndE.js +1 -0
- package/ui-dist/assets/xychartDiagram-2RQKCTM6-zuQa7bqx.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 +8 -0
- package/ui-dist/index.html +46 -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,4705 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { and, asc, desc, eq, gt, inArray, isNull, like, lt, ne, notInArray, or, sql } from "drizzle-orm";
|
|
4
|
+
import { activityLog, agentWakeupRequests, agents, approvals, assets, squads, squadMemberships, documentRevisions, documents, goals, heartbeatRuns, executionWorkspaces, issueApprovals, issueAttachments, issueInboxArchives, issueLabels, issuePlanDecompositions, issueRecoveryActions, issueRelations, issueComments, issueDocuments, issueReadStates, issueThreadInteractions, issues, labels, projectWorkspaces, projects, workspaceOperations, } from "@slaw-ai/db";
|
|
5
|
+
import { clampIssueRequestDepth, extractAgentMentionIds, extractProjectMentionIds, issueCommentAuthorTypeSchema, issueCommentMetadataSchema, issueCommentPresentationSchema, isUuidLike, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@slaw-ai/shared";
|
|
6
|
+
import { conflict, HttpError, notFound, unprocessable } from "../errors.js";
|
|
7
|
+
import { logger } from "../middleware/logger.js";
|
|
8
|
+
import { parseObject } from "../adapters/utils.js";
|
|
9
|
+
import { defaultIssueExecutionWorkspaceSettingsForProject, gateProjectExecutionWorkspacePolicy, issueExecutionWorkspaceModeForPersistedWorkspace, parseIssueExecutionWorkspaceSettings, parseProjectExecutionWorkspacePolicy, } from "./execution-workspace-policy.js";
|
|
10
|
+
import { mergeExecutionWorkspaceConfig } from "./execution-workspaces.js";
|
|
11
|
+
import { buildInitialIssueMonitorFields, normalizeIssueExecutionPolicy } from "./issue-execution-policy.js";
|
|
12
|
+
import { instanceSettingsService } from "./instance-settings.js";
|
|
13
|
+
import { redactCurrentUserText } from "../log-redaction.js";
|
|
14
|
+
import { redactSensitiveText } from "../redaction.js";
|
|
15
|
+
import { resolveIssueGoalId, resolveNextIssueGoalId } from "./issue-goal-fallback.js";
|
|
16
|
+
import { getRunLogStore } from "./run-log-store.js";
|
|
17
|
+
import { getDefaultSquadGoal } from "./goals.js";
|
|
18
|
+
import { isVerifiedIssueTreeControlInteractionWake, issueTreeControlService, } from "./issue-tree-control.js";
|
|
19
|
+
import { parseIssueGraphLivenessIncidentKey, RECOVERY_ORIGIN_KINDS, } from "./recovery/origins.js";
|
|
20
|
+
import { classifyIssueGraphLiveness } from "./recovery/issue-graph-liveness.js";
|
|
21
|
+
const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"];
|
|
22
|
+
const MAX_ISSUE_COMMENT_PAGE_LIMIT = 500;
|
|
23
|
+
export const ISSUE_LIST_DEFAULT_LIMIT = 500;
|
|
24
|
+
export const ISSUE_LIST_MAX_LIMIT = 1000;
|
|
25
|
+
const ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE = 500;
|
|
26
|
+
export const MAX_CHILD_ISSUES_CREATED_BY_HELPER = 25;
|
|
27
|
+
const MAX_CHILD_COMPLETION_SUMMARIES = 20;
|
|
28
|
+
const CHILD_COMPLETION_SUMMARY_BODY_MAX_CHARS = 500;
|
|
29
|
+
const ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_LOG_BYTES = 2_000_000;
|
|
30
|
+
const ISSUE_COMMENT_RUN_LOG_DERIVATION_CHUNK_BYTES = 256_000;
|
|
31
|
+
const ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS = 60_000;
|
|
32
|
+
const ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_PARALLEL_READS = 8;
|
|
33
|
+
function assertTransition(from, to) {
|
|
34
|
+
if (from === to)
|
|
35
|
+
return;
|
|
36
|
+
if (!ALL_ISSUE_STATUSES.includes(to)) {
|
|
37
|
+
throw conflict(`Unknown issue status: ${to}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function applyStatusSideEffects(status, patch) {
|
|
41
|
+
if (!status)
|
|
42
|
+
return patch;
|
|
43
|
+
if (status === "in_progress" && !patch.startedAt) {
|
|
44
|
+
patch.startedAt = new Date();
|
|
45
|
+
}
|
|
46
|
+
if (status === "done") {
|
|
47
|
+
patch.completedAt = new Date();
|
|
48
|
+
}
|
|
49
|
+
if (status === "cancelled") {
|
|
50
|
+
patch.cancelledAt = new Date();
|
|
51
|
+
}
|
|
52
|
+
return patch;
|
|
53
|
+
}
|
|
54
|
+
function readStringFromRecord(record, key) {
|
|
55
|
+
if (!record || typeof record !== "object")
|
|
56
|
+
return null;
|
|
57
|
+
const value = record[key];
|
|
58
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
59
|
+
}
|
|
60
|
+
function buildReusedExecutionWorkspaceConfigPatchFromIssueSettings(settings) {
|
|
61
|
+
return {
|
|
62
|
+
environmentId: settings?.environmentId ?? null,
|
|
63
|
+
provisionCommand: settings?.workspaceStrategy?.provisionCommand ?? null,
|
|
64
|
+
teardownCommand: settings?.workspaceStrategy?.teardownCommand ?? null,
|
|
65
|
+
workspaceRuntime: settings?.workspaceRuntime ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function toTimestampMs(value) {
|
|
69
|
+
if (!value)
|
|
70
|
+
return null;
|
|
71
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
72
|
+
const timestamp = date.getTime();
|
|
73
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
74
|
+
}
|
|
75
|
+
export function deriveIssueCommentRunLogAttribution(comments, runs) {
|
|
76
|
+
const derivedByCommentId = new Map();
|
|
77
|
+
for (const comment of comments) {
|
|
78
|
+
if (comment.authorAgentId || !comment.authorUserId || comment.createdByRunId)
|
|
79
|
+
continue;
|
|
80
|
+
const commentCreatedAtMs = toTimestampMs(comment.createdAt);
|
|
81
|
+
if (commentCreatedAtMs === null)
|
|
82
|
+
continue;
|
|
83
|
+
let bestMatch = null;
|
|
84
|
+
for (const run of runs) {
|
|
85
|
+
const runStartMs = toTimestampMs(run.startedAt ?? run.createdAt);
|
|
86
|
+
const runEndMs = toTimestampMs(run.finishedAt ?? run.createdAt);
|
|
87
|
+
if (runStartMs === null || runEndMs === null)
|
|
88
|
+
continue;
|
|
89
|
+
if (commentCreatedAtMs < runStartMs
|
|
90
|
+
|| commentCreatedAtMs > runEndMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!run.logContent.includes(`comment id: ${comment.id}`))
|
|
94
|
+
continue;
|
|
95
|
+
const distanceMs = Math.abs(runEndMs - commentCreatedAtMs);
|
|
96
|
+
if (!bestMatch || distanceMs < bestMatch.distanceMs) {
|
|
97
|
+
bestMatch = {
|
|
98
|
+
runId: run.runId,
|
|
99
|
+
agentId: run.agentId,
|
|
100
|
+
distanceMs,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!bestMatch)
|
|
105
|
+
continue;
|
|
106
|
+
derivedByCommentId.set(comment.id, {
|
|
107
|
+
derivedAuthorAgentId: bestMatch.agentId,
|
|
108
|
+
derivedCreatedByRunId: bestMatch.runId,
|
|
109
|
+
derivedAuthorSource: "run_log_comment_post",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return derivedByCommentId;
|
|
113
|
+
}
|
|
114
|
+
function serializeAcceptedPlanDecomposition(decomposition) {
|
|
115
|
+
return {
|
|
116
|
+
id: decomposition.id,
|
|
117
|
+
squadId: decomposition.squadId,
|
|
118
|
+
sourceIssueId: decomposition.sourceIssueId,
|
|
119
|
+
acceptedPlanRevisionId: decomposition.acceptedPlanRevisionId,
|
|
120
|
+
acceptedInteractionId: decomposition.acceptedInteractionId,
|
|
121
|
+
status: decomposition.status,
|
|
122
|
+
requestFingerprint: decomposition.requestFingerprint,
|
|
123
|
+
// Intentionally omit requestedChildren here; the API only needs stable counts
|
|
124
|
+
// and child ids, while the durable table keeps the full child draft payload.
|
|
125
|
+
requestedChildCount: decomposition.requestedChildCount,
|
|
126
|
+
childIssueIds: normalizeIssuePlanDecompositionChildIds(decomposition.childIssueIds),
|
|
127
|
+
ownerAgentId: decomposition.ownerAgentId,
|
|
128
|
+
ownerUserId: decomposition.ownerUserId,
|
|
129
|
+
ownerRunId: decomposition.ownerRunId,
|
|
130
|
+
completedAt: decomposition.completedAt,
|
|
131
|
+
createdAt: decomposition.createdAt,
|
|
132
|
+
updatedAt: decomposition.updatedAt,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function sameRunLock(checkoutRunId, actorRunId) {
|
|
136
|
+
if (actorRunId)
|
|
137
|
+
return checkoutRunId === actorRunId;
|
|
138
|
+
return checkoutRunId == null;
|
|
139
|
+
}
|
|
140
|
+
const TERMINAL_HEARTBEAT_RUN_STATUSES = new Set(["succeeded", "failed", "cancelled", "timed_out"]);
|
|
141
|
+
const ISSUE_LIST_DESCRIPTION_MAX_CHARS = 1200;
|
|
142
|
+
const ISSUE_LIST_DESCRIPTION_MAX_BYTES = ISSUE_LIST_DESCRIPTION_MAX_CHARS * 4;
|
|
143
|
+
function escapeLikePattern(value) {
|
|
144
|
+
return value.replace(/[\\%_]/g, "\\$&");
|
|
145
|
+
}
|
|
146
|
+
export function clampIssueListLimit(limit) {
|
|
147
|
+
return Math.min(ISSUE_LIST_MAX_LIMIT, Math.max(1, Math.floor(limit)));
|
|
148
|
+
}
|
|
149
|
+
function chunkList(values, size) {
|
|
150
|
+
const chunks = [];
|
|
151
|
+
for (let index = 0; index < values.length; index += size) {
|
|
152
|
+
chunks.push(values.slice(index, index + size));
|
|
153
|
+
}
|
|
154
|
+
return chunks;
|
|
155
|
+
}
|
|
156
|
+
function truncateInlineSummary(value, maxChars = CHILD_COMPLETION_SUMMARY_BODY_MAX_CHARS) {
|
|
157
|
+
const normalized = value?.trim();
|
|
158
|
+
if (!normalized)
|
|
159
|
+
return null;
|
|
160
|
+
return normalized.length > maxChars ? `${normalized.slice(0, Math.max(0, maxChars - 15)).trimEnd()} [truncated]` : normalized;
|
|
161
|
+
}
|
|
162
|
+
function truncateByCodePoint(value, maxChars) {
|
|
163
|
+
if (value.length <= maxChars)
|
|
164
|
+
return value;
|
|
165
|
+
return Array.from(value).slice(0, maxChars).join("");
|
|
166
|
+
}
|
|
167
|
+
function decodeDatabaseTextPreview(value, maxChars) {
|
|
168
|
+
if (value == null)
|
|
169
|
+
return null;
|
|
170
|
+
return truncateByCodePoint(Buffer.from(value, "base64").toString("utf8"), maxChars);
|
|
171
|
+
}
|
|
172
|
+
function appendAcceptanceCriteriaToDescription(description, acceptanceCriteria) {
|
|
173
|
+
const criteria = (acceptanceCriteria ?? []).map((item) => item.trim()).filter(Boolean);
|
|
174
|
+
if (criteria.length === 0)
|
|
175
|
+
return description ?? null;
|
|
176
|
+
const base = description?.trim() ?? "";
|
|
177
|
+
const criteriaMarkdown = ["## Acceptance Criteria", "", ...criteria.map((item) => `- ${item}`)].join("\n");
|
|
178
|
+
return base ? `${base}\n\n${criteriaMarkdown}` : criteriaMarkdown;
|
|
179
|
+
}
|
|
180
|
+
function normalizeAcceptedPlanDecompositionFingerprintValue(value) {
|
|
181
|
+
if (value === undefined)
|
|
182
|
+
return null;
|
|
183
|
+
if (value == null ||
|
|
184
|
+
typeof value === "string" ||
|
|
185
|
+
typeof value === "number" ||
|
|
186
|
+
typeof value === "boolean") {
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
if (value instanceof Date)
|
|
190
|
+
return value.toISOString();
|
|
191
|
+
if (Array.isArray(value)) {
|
|
192
|
+
return value.map((item) => normalizeAcceptedPlanDecompositionFingerprintValue(item));
|
|
193
|
+
}
|
|
194
|
+
if (typeof value === "object") {
|
|
195
|
+
const record = value;
|
|
196
|
+
return Object.fromEntries(Object.keys(record)
|
|
197
|
+
.sort()
|
|
198
|
+
.map((key) => [key, normalizeAcceptedPlanDecompositionFingerprintValue(record[key])]));
|
|
199
|
+
}
|
|
200
|
+
return String(value);
|
|
201
|
+
}
|
|
202
|
+
const ACCEPTED_PLAN_DECOMPOSITION_FINGERPRINT_CHILD_METADATA_KEYS = new Set([
|
|
203
|
+
"id",
|
|
204
|
+
"squadId",
|
|
205
|
+
"parentId",
|
|
206
|
+
"identifier",
|
|
207
|
+
"checkoutRunId",
|
|
208
|
+
"executionRunId",
|
|
209
|
+
"executionLockedAt",
|
|
210
|
+
"startedAt",
|
|
211
|
+
"completedAt",
|
|
212
|
+
"cancelledAt",
|
|
213
|
+
"hiddenAt",
|
|
214
|
+
"createdAt",
|
|
215
|
+
"updatedAt",
|
|
216
|
+
"createdByAgentId",
|
|
217
|
+
"createdByUserId",
|
|
218
|
+
"updatedByAgentId",
|
|
219
|
+
"updatedByUserId",
|
|
220
|
+
"actorAgentId",
|
|
221
|
+
"actorUserId",
|
|
222
|
+
]);
|
|
223
|
+
function normalizeAcceptedPlanDecompositionFingerprintChild(child) {
|
|
224
|
+
return Object.fromEntries(Object.entries(child).filter(([key]) => !ACCEPTED_PLAN_DECOMPOSITION_FINGERPRINT_CHILD_METADATA_KEYS.has(key)));
|
|
225
|
+
}
|
|
226
|
+
function createAcceptedPlanDecompositionRequestFingerprint(input) {
|
|
227
|
+
const canonical = JSON.stringify(normalizeAcceptedPlanDecompositionFingerprintValue({
|
|
228
|
+
acceptedPlanRevisionId: input.acceptedPlanRevisionId,
|
|
229
|
+
children: input.children.map(normalizeAcceptedPlanDecompositionFingerprintChild),
|
|
230
|
+
}));
|
|
231
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
232
|
+
}
|
|
233
|
+
function normalizeIssuePlanDecompositionChildIds(value) {
|
|
234
|
+
if (!Array.isArray(value))
|
|
235
|
+
return [];
|
|
236
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
237
|
+
}
|
|
238
|
+
export function readAcceptedPlanConfirmationTarget(payload) {
|
|
239
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
240
|
+
return null;
|
|
241
|
+
const target = payload.target;
|
|
242
|
+
if (!target || typeof target !== "object" || Array.isArray(target))
|
|
243
|
+
return null;
|
|
244
|
+
const record = target;
|
|
245
|
+
if (record.type !== "issue_document")
|
|
246
|
+
return null;
|
|
247
|
+
const revisionId = readStringFromRecord(record, "revisionId");
|
|
248
|
+
const key = readStringFromRecord(record, "key");
|
|
249
|
+
const issueId = readStringFromRecord(record, "issueId");
|
|
250
|
+
if (!revisionId || !key || !issueId)
|
|
251
|
+
return null;
|
|
252
|
+
return { revisionId, key, issueId };
|
|
253
|
+
}
|
|
254
|
+
async function resolveAcceptedPlanClaimOwner(input) {
|
|
255
|
+
const nextOwner = {
|
|
256
|
+
ownerAgentId: input.actorAgentId ?? null,
|
|
257
|
+
ownerUserId: input.actorUserId ?? null,
|
|
258
|
+
ownerRunId: input.actorRunId ?? null,
|
|
259
|
+
};
|
|
260
|
+
if (input.claim.ownerAgentId === nextOwner.ownerAgentId
|
|
261
|
+
&& input.claim.ownerUserId === nextOwner.ownerUserId
|
|
262
|
+
&& input.claim.ownerRunId === nextOwner.ownerRunId) {
|
|
263
|
+
return nextOwner;
|
|
264
|
+
}
|
|
265
|
+
if (!input.claim.ownerRunId) {
|
|
266
|
+
return nextOwner;
|
|
267
|
+
}
|
|
268
|
+
const existingOwnerRun = await input.dbOrTx
|
|
269
|
+
.select({ status: heartbeatRuns.status })
|
|
270
|
+
.from(heartbeatRuns)
|
|
271
|
+
.where(eq(heartbeatRuns.id, input.claim.ownerRunId))
|
|
272
|
+
.then((rows) => rows[0] ?? null);
|
|
273
|
+
if (existingOwnerRun && !TERMINAL_HEARTBEAT_RUN_STATUSES.has(existingOwnerRun.status)) {
|
|
274
|
+
return {
|
|
275
|
+
ownerAgentId: input.claim.ownerAgentId,
|
|
276
|
+
ownerUserId: input.claim.ownerUserId,
|
|
277
|
+
ownerRunId: input.claim.ownerRunId,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return nextOwner;
|
|
281
|
+
}
|
|
282
|
+
async function findAcceptedPlanDocumentInteraction(dbOrTx, input) {
|
|
283
|
+
const rows = await dbOrTx
|
|
284
|
+
.select({
|
|
285
|
+
id: issueThreadInteractions.id,
|
|
286
|
+
payload: issueThreadInteractions.payload,
|
|
287
|
+
})
|
|
288
|
+
.from(issueThreadInteractions)
|
|
289
|
+
.where(and(eq(issueThreadInteractions.squadId, input.squadId), eq(issueThreadInteractions.issueId, input.sourceIssueId), eq(issueThreadInteractions.kind, "request_confirmation"), eq(issueThreadInteractions.status, "accepted")))
|
|
290
|
+
.orderBy(desc(issueThreadInteractions.resolvedAt), desc(issueThreadInteractions.createdAt));
|
|
291
|
+
for (const row of rows) {
|
|
292
|
+
const target = readAcceptedPlanConfirmationTarget(row.payload);
|
|
293
|
+
if (target?.issueId === input.sourceIssueId &&
|
|
294
|
+
target.key === "plan" &&
|
|
295
|
+
target.revisionId === input.acceptedPlanRevisionId) {
|
|
296
|
+
return { id: row.id };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
function createIssueDependencyReadiness(issueId) {
|
|
302
|
+
return {
|
|
303
|
+
issueId,
|
|
304
|
+
blockerIssueIds: [],
|
|
305
|
+
unresolvedBlockerIssueIds: [],
|
|
306
|
+
unresolvedBlockerCount: 0,
|
|
307
|
+
pendingFinalizeBlockerIssueIds: [],
|
|
308
|
+
allBlockersDone: true,
|
|
309
|
+
isDependencyReady: true,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Returns the set of execution-workspace ids whose most recent workspace operation
|
|
314
|
+
* is NOT a successful `workspace_finalize`. These workspaces have either an in-flight
|
|
315
|
+
* run, a failed finalize, or never reached the finalize barrier — dependents that
|
|
316
|
+
* read this workspace must wait until finalize succeeds.
|
|
317
|
+
*
|
|
318
|
+
* Workspaces with no recorded operations are considered finalized (nothing has
|
|
319
|
+
* touched them since they were realized).
|
|
320
|
+
*/
|
|
321
|
+
export async function listUnfinalizedExecutionWorkspaceIds(dbOrTx, squadId, executionWorkspaceIds) {
|
|
322
|
+
const unfinalized = new Set();
|
|
323
|
+
if (executionWorkspaceIds.length === 0)
|
|
324
|
+
return unfinalized;
|
|
325
|
+
// Pull every workspace op for the candidate workspaces and pick the latest per
|
|
326
|
+
// workspace in memory. Per-workspace LATERAL queries would be tighter, but the
|
|
327
|
+
// candidate set is tiny in practice (one workspace per blocker per readiness call).
|
|
328
|
+
const rows = await dbOrTx
|
|
329
|
+
.select({
|
|
330
|
+
executionWorkspaceId: workspaceOperations.executionWorkspaceId,
|
|
331
|
+
phase: workspaceOperations.phase,
|
|
332
|
+
status: workspaceOperations.status,
|
|
333
|
+
startedAt: workspaceOperations.startedAt,
|
|
334
|
+
})
|
|
335
|
+
.from(workspaceOperations)
|
|
336
|
+
.where(and(eq(workspaceOperations.squadId, squadId), inArray(workspaceOperations.executionWorkspaceId, executionWorkspaceIds)));
|
|
337
|
+
const latestByWorkspace = new Map();
|
|
338
|
+
for (const row of rows) {
|
|
339
|
+
if (!row.executionWorkspaceId)
|
|
340
|
+
continue;
|
|
341
|
+
const current = latestByWorkspace.get(row.executionWorkspaceId);
|
|
342
|
+
if (!current || row.startedAt > current.startedAt) {
|
|
343
|
+
latestByWorkspace.set(row.executionWorkspaceId, {
|
|
344
|
+
phase: row.phase,
|
|
345
|
+
status: row.status,
|
|
346
|
+
startedAt: row.startedAt,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const workspaceId of executionWorkspaceIds) {
|
|
351
|
+
const latest = latestByWorkspace.get(workspaceId);
|
|
352
|
+
if (!latest)
|
|
353
|
+
continue; // no ops recorded → treat as finalized
|
|
354
|
+
if (latest.phase === "workspace_finalize" && latest.status === "succeeded")
|
|
355
|
+
continue;
|
|
356
|
+
unfinalized.add(workspaceId);
|
|
357
|
+
}
|
|
358
|
+
return unfinalized;
|
|
359
|
+
}
|
|
360
|
+
async function listIssueDependencyReadinessMap(dbOrTx, squadId, issueIds) {
|
|
361
|
+
const uniqueIssueIds = [...new Set(issueIds.filter(Boolean))];
|
|
362
|
+
const readinessMap = new Map();
|
|
363
|
+
for (const issueId of uniqueIssueIds) {
|
|
364
|
+
readinessMap.set(issueId, createIssueDependencyReadiness(issueId));
|
|
365
|
+
}
|
|
366
|
+
if (uniqueIssueIds.length === 0)
|
|
367
|
+
return readinessMap;
|
|
368
|
+
const blockerRows = await dbOrTx
|
|
369
|
+
.select({
|
|
370
|
+
issueId: issueRelations.relatedIssueId,
|
|
371
|
+
blockerIssueId: issueRelations.issueId,
|
|
372
|
+
blockerStatus: issues.status,
|
|
373
|
+
blockerExecutionWorkspaceId: issues.executionWorkspaceId,
|
|
374
|
+
})
|
|
375
|
+
.from(issueRelations)
|
|
376
|
+
.innerJoin(issues, eq(issueRelations.issueId, issues.id))
|
|
377
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.type, "blocks"), inArray(issueRelations.relatedIssueId, uniqueIssueIds)));
|
|
378
|
+
// Collect executionWorkspaceIds of "done" blockers — these are the only ones
|
|
379
|
+
// subject to the workspace-finalize barrier. Blockers that aren't done already
|
|
380
|
+
// mark the dependent as not-ready and don't need a finalize check.
|
|
381
|
+
const doneBlockerWorkspaceIds = new Set();
|
|
382
|
+
for (const row of blockerRows) {
|
|
383
|
+
if (row.blockerStatus === "done" && row.blockerExecutionWorkspaceId) {
|
|
384
|
+
doneBlockerWorkspaceIds.add(row.blockerExecutionWorkspaceId);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const unfinalizedWorkspaceIds = await listUnfinalizedExecutionWorkspaceIds(dbOrTx, squadId, [...doneBlockerWorkspaceIds]);
|
|
388
|
+
for (const row of blockerRows) {
|
|
389
|
+
const current = readinessMap.get(row.issueId) ?? createIssueDependencyReadiness(row.issueId);
|
|
390
|
+
current.blockerIssueIds.push(row.blockerIssueId);
|
|
391
|
+
// Only done blockers resolve dependents; cancelled blockers stay unresolved
|
|
392
|
+
// until an operator removes or replaces the blocker relationship explicitly.
|
|
393
|
+
if (row.blockerStatus !== "done") {
|
|
394
|
+
current.unresolvedBlockerIssueIds.push(row.blockerIssueId);
|
|
395
|
+
current.unresolvedBlockerCount += 1;
|
|
396
|
+
current.allBlockersDone = false;
|
|
397
|
+
current.isDependencyReady = false;
|
|
398
|
+
}
|
|
399
|
+
else if (row.blockerExecutionWorkspaceId &&
|
|
400
|
+
unfinalizedWorkspaceIds.has(row.blockerExecutionWorkspaceId)) {
|
|
401
|
+
// Workspace-finalize barrier: the blocker's most recent run on its
|
|
402
|
+
// execution workspace hasn't recorded a successful workspace_finalize.
|
|
403
|
+
// Treat the dependent as not-ready until sync-back lands (or the run
|
|
404
|
+
// finalizes); a subsequent finalize wake will re-evaluate readiness.
|
|
405
|
+
// `allBlockersDone` is cleared too so that callers using it as a
|
|
406
|
+
// proxy for "this dependent can proceed" still see the gate.
|
|
407
|
+
current.unresolvedBlockerIssueIds.push(row.blockerIssueId);
|
|
408
|
+
current.unresolvedBlockerCount += 1;
|
|
409
|
+
current.pendingFinalizeBlockerIssueIds.push(row.blockerIssueId);
|
|
410
|
+
current.allBlockersDone = false;
|
|
411
|
+
current.isDependencyReady = false;
|
|
412
|
+
}
|
|
413
|
+
readinessMap.set(row.issueId, current);
|
|
414
|
+
}
|
|
415
|
+
return readinessMap;
|
|
416
|
+
}
|
|
417
|
+
async function listUnresolvedBlockerIssueIds(dbOrTx, squadId, blockerIssueIds) {
|
|
418
|
+
const uniqueBlockerIssueIds = [...new Set(blockerIssueIds.filter(Boolean))];
|
|
419
|
+
if (uniqueBlockerIssueIds.length === 0)
|
|
420
|
+
return [];
|
|
421
|
+
return dbOrTx
|
|
422
|
+
.select({ id: issues.id })
|
|
423
|
+
.from(issues)
|
|
424
|
+
.where(and(eq(issues.squadId, squadId), inArray(issues.id, uniqueBlockerIssueIds),
|
|
425
|
+
// Cancelled blockers intentionally remain unresolved until the relation changes.
|
|
426
|
+
ne(issues.status, "done")))
|
|
427
|
+
.then((rows) => rows.map((row) => row.id));
|
|
428
|
+
}
|
|
429
|
+
async function getProjectDefaultGoalId(db, squadId, projectId) {
|
|
430
|
+
if (!projectId)
|
|
431
|
+
return null;
|
|
432
|
+
const row = await db
|
|
433
|
+
.select({ goalId: projects.goalId })
|
|
434
|
+
.from(projects)
|
|
435
|
+
.where(and(eq(projects.id, projectId), eq(projects.squadId, squadId)))
|
|
436
|
+
.then((rows) => rows[0] ?? null);
|
|
437
|
+
return row?.goalId ?? null;
|
|
438
|
+
}
|
|
439
|
+
async function getWorkspaceInheritanceIssue(db, squadId, issueId) {
|
|
440
|
+
const issue = await db
|
|
441
|
+
.select({
|
|
442
|
+
id: issues.id,
|
|
443
|
+
projectId: issues.projectId,
|
|
444
|
+
projectWorkspaceId: issues.projectWorkspaceId,
|
|
445
|
+
executionWorkspaceId: issues.executionWorkspaceId,
|
|
446
|
+
executionWorkspaceSettings: issues.executionWorkspaceSettings,
|
|
447
|
+
})
|
|
448
|
+
.from(issues)
|
|
449
|
+
.where(and(eq(issues.id, issueId), eq(issues.squadId, squadId)))
|
|
450
|
+
.then((rows) => rows[0] ?? null);
|
|
451
|
+
if (!issue) {
|
|
452
|
+
throw notFound("Workspace inheritance issue not found");
|
|
453
|
+
}
|
|
454
|
+
return issue;
|
|
455
|
+
}
|
|
456
|
+
function touchedByUserCondition(squadId, userId) {
|
|
457
|
+
return sql `
|
|
458
|
+
(
|
|
459
|
+
${issues.createdByUserId} = ${userId}
|
|
460
|
+
OR ${issues.assigneeUserId} = ${userId}
|
|
461
|
+
OR EXISTS (
|
|
462
|
+
SELECT 1
|
|
463
|
+
FROM ${issueReadStates}
|
|
464
|
+
WHERE ${issueReadStates.issueId} = ${issues.id}
|
|
465
|
+
AND ${issueReadStates.squadId} = ${squadId}
|
|
466
|
+
AND ${issueReadStates.userId} = ${userId}
|
|
467
|
+
)
|
|
468
|
+
OR EXISTS (
|
|
469
|
+
SELECT 1
|
|
470
|
+
FROM ${issueComments}
|
|
471
|
+
WHERE ${issueComments.issueId} = ${issues.id}
|
|
472
|
+
AND ${issueComments.squadId} = ${squadId}
|
|
473
|
+
AND ${issueComments.authorUserId} = ${userId}
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
`;
|
|
477
|
+
}
|
|
478
|
+
function participatedByAgentCondition(squadId, agentId) {
|
|
479
|
+
return sql `
|
|
480
|
+
(
|
|
481
|
+
${issues.createdByAgentId} = ${agentId}
|
|
482
|
+
OR ${issues.assigneeAgentId} = ${agentId}
|
|
483
|
+
OR EXISTS (
|
|
484
|
+
SELECT 1
|
|
485
|
+
FROM ${issueComments}
|
|
486
|
+
WHERE ${issueComments.issueId} = ${issues.id}
|
|
487
|
+
AND ${issueComments.squadId} = ${squadId}
|
|
488
|
+
AND ${issueComments.authorAgentId} = ${agentId}
|
|
489
|
+
)
|
|
490
|
+
OR EXISTS (
|
|
491
|
+
SELECT 1
|
|
492
|
+
FROM ${activityLog}
|
|
493
|
+
WHERE ${activityLog.squadId} = ${squadId}
|
|
494
|
+
AND ${activityLog.entityType} = 'issue'
|
|
495
|
+
AND ${activityLog.entityId} = ${issues.id}::text
|
|
496
|
+
AND ${activityLog.agentId} = ${agentId}
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
`;
|
|
500
|
+
}
|
|
501
|
+
function myLastCommentAtExpr(squadId, userId) {
|
|
502
|
+
return sql `
|
|
503
|
+
(
|
|
504
|
+
SELECT MAX(${issueComments.createdAt})
|
|
505
|
+
FROM ${issueComments}
|
|
506
|
+
WHERE ${issueComments.issueId} = ${issues.id}
|
|
507
|
+
AND ${issueComments.squadId} = ${squadId}
|
|
508
|
+
AND ${issueComments.authorUserId} = ${userId}
|
|
509
|
+
)
|
|
510
|
+
`;
|
|
511
|
+
}
|
|
512
|
+
function myLastReadAtExpr(squadId, userId) {
|
|
513
|
+
return sql `
|
|
514
|
+
(
|
|
515
|
+
SELECT MAX(${issueReadStates.lastReadAt})
|
|
516
|
+
FROM ${issueReadStates}
|
|
517
|
+
WHERE ${issueReadStates.issueId} = ${issues.id}
|
|
518
|
+
AND ${issueReadStates.squadId} = ${squadId}
|
|
519
|
+
AND ${issueReadStates.userId} = ${userId}
|
|
520
|
+
)
|
|
521
|
+
`;
|
|
522
|
+
}
|
|
523
|
+
function myLastTouchAtExpr(squadId, userId) {
|
|
524
|
+
const myLastCommentAt = myLastCommentAtExpr(squadId, userId);
|
|
525
|
+
const myLastReadAt = myLastReadAtExpr(squadId, userId);
|
|
526
|
+
return sql `
|
|
527
|
+
GREATEST(
|
|
528
|
+
COALESCE(${myLastCommentAt}, to_timestamp(0)),
|
|
529
|
+
COALESCE(${myLastReadAt}, to_timestamp(0)),
|
|
530
|
+
COALESCE(CASE WHEN ${issues.createdByUserId} = ${userId} THEN ${issues.createdAt} ELSE NULL END, to_timestamp(0)),
|
|
531
|
+
COALESCE(CASE WHEN ${issues.assigneeUserId} = ${userId} THEN ${issues.updatedAt} ELSE NULL END, to_timestamp(0))
|
|
532
|
+
)
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
function lastExternalCommentAtExpr(squadId, userId) {
|
|
536
|
+
return sql `
|
|
537
|
+
(
|
|
538
|
+
SELECT MAX(${issueComments.createdAt})
|
|
539
|
+
FROM ${issueComments}
|
|
540
|
+
WHERE ${issueComments.issueId} = ${issues.id}
|
|
541
|
+
AND ${issueComments.squadId} = ${squadId}
|
|
542
|
+
AND (
|
|
543
|
+
${issueComments.authorUserId} IS NULL
|
|
544
|
+
OR ${issueComments.authorUserId} <> ${userId}
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
function issueLastActivityAtExpr(squadId, userId) {
|
|
550
|
+
const lastExternalCommentAt = lastExternalCommentAtExpr(squadId, userId);
|
|
551
|
+
const myLastTouchAt = myLastTouchAtExpr(squadId, userId);
|
|
552
|
+
return sql `
|
|
553
|
+
GREATEST(
|
|
554
|
+
COALESCE(${lastExternalCommentAt}, to_timestamp(0)),
|
|
555
|
+
CASE
|
|
556
|
+
WHEN ${issues.updatedAt} > COALESCE(${myLastTouchAt}, to_timestamp(0))
|
|
557
|
+
THEN ${issues.updatedAt}
|
|
558
|
+
ELSE to_timestamp(0)
|
|
559
|
+
END
|
|
560
|
+
)
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
const ISSUE_LOCAL_INBOX_ACTIVITY_ACTIONS = [
|
|
564
|
+
"issue.read_marked",
|
|
565
|
+
"issue.read_unmarked",
|
|
566
|
+
"issue.inbox_archived",
|
|
567
|
+
"issue.inbox_unarchived",
|
|
568
|
+
];
|
|
569
|
+
function issueLatestCommentAtExpr(squadId) {
|
|
570
|
+
return sql `
|
|
571
|
+
(
|
|
572
|
+
SELECT MAX(${issueComments.createdAt})
|
|
573
|
+
FROM ${issueComments}
|
|
574
|
+
WHERE ${issueComments.issueId} = ${issues.id}
|
|
575
|
+
AND ${issueComments.squadId} = ${squadId}
|
|
576
|
+
)
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
function issueLatestLogAtExpr(squadId) {
|
|
580
|
+
return sql `
|
|
581
|
+
(
|
|
582
|
+
SELECT MAX(${activityLog.createdAt})
|
|
583
|
+
FROM ${activityLog}
|
|
584
|
+
WHERE ${activityLog.squadId} = ${squadId}
|
|
585
|
+
AND ${activityLog.entityType} = 'issue'
|
|
586
|
+
AND ${activityLog.entityId} = ${issues.id}::text
|
|
587
|
+
AND ${activityLog.action} NOT IN (${sql.join(ISSUE_LOCAL_INBOX_ACTIVITY_ACTIONS.map((action) => sql `${action}`), sql `, `)})
|
|
588
|
+
)
|
|
589
|
+
`;
|
|
590
|
+
}
|
|
591
|
+
function issueCanonicalLastActivityAtExpr(squadId) {
|
|
592
|
+
const latestCommentAt = issueLatestCommentAtExpr(squadId);
|
|
593
|
+
const latestLogAt = issueLatestLogAtExpr(squadId);
|
|
594
|
+
return sql `
|
|
595
|
+
GREATEST(
|
|
596
|
+
${issues.updatedAt},
|
|
597
|
+
COALESCE(${latestCommentAt}, to_timestamp(0)),
|
|
598
|
+
COALESCE(${latestLogAt}, to_timestamp(0))
|
|
599
|
+
)
|
|
600
|
+
`;
|
|
601
|
+
}
|
|
602
|
+
function unreadForUserCondition(squadId, userId) {
|
|
603
|
+
const touchedCondition = touchedByUserCondition(squadId, userId);
|
|
604
|
+
const myLastTouchAt = myLastTouchAtExpr(squadId, userId);
|
|
605
|
+
return sql `
|
|
606
|
+
(
|
|
607
|
+
${touchedCondition}
|
|
608
|
+
AND EXISTS (
|
|
609
|
+
SELECT 1
|
|
610
|
+
FROM ${issueComments}
|
|
611
|
+
WHERE ${issueComments.issueId} = ${issues.id}
|
|
612
|
+
AND ${issueComments.squadId} = ${squadId}
|
|
613
|
+
AND (
|
|
614
|
+
${issueComments.authorUserId} IS NULL
|
|
615
|
+
OR ${issueComments.authorUserId} <> ${userId}
|
|
616
|
+
)
|
|
617
|
+
AND ${issueComments.createdAt} > ${myLastTouchAt}
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
`;
|
|
621
|
+
}
|
|
622
|
+
function inboxVisibleForUserCondition(squadId, userId) {
|
|
623
|
+
const issueLastActivityAt = issueLastActivityAtExpr(squadId, userId);
|
|
624
|
+
return sql `
|
|
625
|
+
NOT EXISTS (
|
|
626
|
+
SELECT 1
|
|
627
|
+
FROM ${issueInboxArchives}
|
|
628
|
+
WHERE ${issueInboxArchives.issueId} = ${issues.id}
|
|
629
|
+
AND ${issueInboxArchives.squadId} = ${squadId}
|
|
630
|
+
AND ${issueInboxArchives.userId} = ${userId}
|
|
631
|
+
AND ${issueInboxArchives.archivedAt} >= ${issueLastActivityAt}
|
|
632
|
+
)
|
|
633
|
+
`;
|
|
634
|
+
}
|
|
635
|
+
const LEGACY_PLUGIN_OPERATION_ORIGIN_KINDS = [
|
|
636
|
+
"plugin:slaw.content-machine:case",
|
|
637
|
+
"plugin:slaw.content-machine:evaluation",
|
|
638
|
+
"plugin:slaw.content-machine:source-sync",
|
|
639
|
+
];
|
|
640
|
+
function nonPluginOperationIssueCondition() {
|
|
641
|
+
return sql `NOT (
|
|
642
|
+
${issues.originKind} LIKE 'plugin:%:operation'
|
|
643
|
+
OR ${issues.originKind} LIKE 'plugin:%:operation:%'
|
|
644
|
+
OR ${inArray(issues.originKind, LEGACY_PLUGIN_OPERATION_ORIGIN_KINDS)}
|
|
645
|
+
)`;
|
|
646
|
+
}
|
|
647
|
+
function shouldIncludePluginOperationIssues(filters) {
|
|
648
|
+
return Boolean(filters?.includePluginOperations ||
|
|
649
|
+
filters?.originKind ||
|
|
650
|
+
filters?.originKindPrefix ||
|
|
651
|
+
filters?.originId ||
|
|
652
|
+
filters?.projectId);
|
|
653
|
+
}
|
|
654
|
+
/** Named entities commonly emitted in saved issue bodies; unknown `&name;` sequences are left unchanged. */
|
|
655
|
+
const WELL_KNOWN_NAMED_HTML_ENTITIES = {
|
|
656
|
+
amp: "&",
|
|
657
|
+
apos: "'",
|
|
658
|
+
copy: "\u00A9",
|
|
659
|
+
gt: ">",
|
|
660
|
+
lt: "<",
|
|
661
|
+
nbsp: "\u00A0",
|
|
662
|
+
quot: '"',
|
|
663
|
+
ensp: "\u2002",
|
|
664
|
+
emsp: "\u2003",
|
|
665
|
+
thinsp: "\u2009",
|
|
666
|
+
};
|
|
667
|
+
function decodeNumericHtmlEntity(digits, radix) {
|
|
668
|
+
const n = Number.parseInt(digits, radix);
|
|
669
|
+
if (Number.isNaN(n) || n < 0 || n > 0x10ffff)
|
|
670
|
+
return null;
|
|
671
|
+
try {
|
|
672
|
+
return String.fromCodePoint(n);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** Decodes HTML character references in a raw @mention capture so UI-encoded bodies match agent names. */
|
|
679
|
+
export function normalizeAgentMentionToken(raw) {
|
|
680
|
+
let s = raw.replace(/&#x([0-9a-fA-F]+);/gi, (full, hex) => decodeNumericHtmlEntity(hex, 16) ?? full);
|
|
681
|
+
s = s.replace(/&#([0-9]+);/g, (full, dec) => decodeNumericHtmlEntity(dec, 10) ?? full);
|
|
682
|
+
s = s.replace(/&([a-z][a-z0-9]*);/gi, (full, name) => {
|
|
683
|
+
const decoded = WELL_KNOWN_NAMED_HTML_ENTITIES[name.toLowerCase()];
|
|
684
|
+
return decoded !== undefined ? decoded : full;
|
|
685
|
+
});
|
|
686
|
+
return s.trim();
|
|
687
|
+
}
|
|
688
|
+
export function deriveIssueUserContext(issue, userId, stats) {
|
|
689
|
+
const normalizeDate = (value) => {
|
|
690
|
+
if (!value)
|
|
691
|
+
return null;
|
|
692
|
+
if (value instanceof Date)
|
|
693
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
694
|
+
const parsed = new Date(value);
|
|
695
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
696
|
+
};
|
|
697
|
+
const myLastCommentAt = normalizeDate(stats?.myLastCommentAt);
|
|
698
|
+
const myLastReadAt = normalizeDate(stats?.myLastReadAt);
|
|
699
|
+
const createdTouchAt = issue.createdByUserId === userId ? normalizeDate(issue.createdAt) : null;
|
|
700
|
+
const assignedTouchAt = issue.assigneeUserId === userId ? normalizeDate(issue.updatedAt) : null;
|
|
701
|
+
const myLastTouchAt = [myLastCommentAt, myLastReadAt, createdTouchAt, assignedTouchAt]
|
|
702
|
+
.filter((value) => value instanceof Date)
|
|
703
|
+
.sort((a, b) => b.getTime() - a.getTime())[0] ?? null;
|
|
704
|
+
const lastExternalCommentAt = normalizeDate(stats?.lastExternalCommentAt);
|
|
705
|
+
const isUnreadForMe = Boolean(myLastTouchAt &&
|
|
706
|
+
lastExternalCommentAt &&
|
|
707
|
+
lastExternalCommentAt.getTime() > myLastTouchAt.getTime());
|
|
708
|
+
return {
|
|
709
|
+
myLastTouchAt,
|
|
710
|
+
lastExternalCommentAt,
|
|
711
|
+
isUnreadForMe,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function latestIssueActivityAt(...values) {
|
|
715
|
+
const normalized = values
|
|
716
|
+
.map((value) => {
|
|
717
|
+
if (!value)
|
|
718
|
+
return null;
|
|
719
|
+
if (value instanceof Date)
|
|
720
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
721
|
+
const parsed = new Date(value);
|
|
722
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
723
|
+
})
|
|
724
|
+
.filter((value) => value instanceof Date)
|
|
725
|
+
.sort((a, b) => b.getTime() - a.getTime());
|
|
726
|
+
return normalized[0] ?? null;
|
|
727
|
+
}
|
|
728
|
+
function issueListOrderBy(squadId, { hasSearch, priorityOrder, searchOrder, sortField, sortDir, }) {
|
|
729
|
+
const canonicalLastActivityAt = issueCanonicalLastActivityAtExpr(squadId);
|
|
730
|
+
if (sortField === "updated") {
|
|
731
|
+
const activityOrder = sortDir === "asc"
|
|
732
|
+
? asc(canonicalLastActivityAt)
|
|
733
|
+
: desc(canonicalLastActivityAt);
|
|
734
|
+
const updatedOrder = sortDir === "asc" ? asc(issues.updatedAt) : desc(issues.updatedAt);
|
|
735
|
+
const idOrder = sortDir === "asc" ? asc(issues.id) : desc(issues.id);
|
|
736
|
+
return hasSearch
|
|
737
|
+
? [asc(searchOrder), activityOrder, updatedOrder, idOrder]
|
|
738
|
+
: [activityOrder, updatedOrder, idOrder];
|
|
739
|
+
}
|
|
740
|
+
return [
|
|
741
|
+
hasSearch ? asc(searchOrder) : asc(priorityOrder),
|
|
742
|
+
asc(priorityOrder),
|
|
743
|
+
desc(canonicalLastActivityAt),
|
|
744
|
+
desc(issues.updatedAt),
|
|
745
|
+
desc(issues.id),
|
|
746
|
+
];
|
|
747
|
+
}
|
|
748
|
+
async function labelMapForIssues(dbOrTx, issueIds) {
|
|
749
|
+
const map = new Map();
|
|
750
|
+
if (issueIds.length === 0)
|
|
751
|
+
return map;
|
|
752
|
+
for (const issueIdChunk of chunkList(issueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
753
|
+
const rows = await dbOrTx
|
|
754
|
+
.select({
|
|
755
|
+
issueId: issueLabels.issueId,
|
|
756
|
+
label: labels,
|
|
757
|
+
})
|
|
758
|
+
.from(issueLabels)
|
|
759
|
+
.innerJoin(labels, eq(issueLabels.labelId, labels.id))
|
|
760
|
+
.where(inArray(issueLabels.issueId, issueIdChunk))
|
|
761
|
+
.orderBy(asc(labels.name), asc(labels.id));
|
|
762
|
+
for (const row of rows) {
|
|
763
|
+
const existing = map.get(row.issueId);
|
|
764
|
+
if (existing)
|
|
765
|
+
existing.push(row.label);
|
|
766
|
+
else
|
|
767
|
+
map.set(row.issueId, [row.label]);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return map;
|
|
771
|
+
}
|
|
772
|
+
async function withIssueLabels(dbOrTx, rows) {
|
|
773
|
+
if (rows.length === 0)
|
|
774
|
+
return [];
|
|
775
|
+
const labelsByIssueId = await labelMapForIssues(dbOrTx, rows.map((row) => row.id));
|
|
776
|
+
return rows.map((row) => {
|
|
777
|
+
const issueLabels = labelsByIssueId.get(row.id) ?? [];
|
|
778
|
+
return {
|
|
779
|
+
...row,
|
|
780
|
+
labels: issueLabels,
|
|
781
|
+
labelIds: issueLabels.map((label) => label.id),
|
|
782
|
+
};
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const ACTIVE_RUN_STATUSES = ["queued", "running"];
|
|
786
|
+
const BLOCKER_ATTENTION_ACTIVE_RUN_STATUSES = ["queued", "running"];
|
|
787
|
+
const BLOCKER_ATTENTION_ACTIVE_WAKE_STATUSES = ["queued", "deferred_issue_execution"];
|
|
788
|
+
const BLOCKER_ATTENTION_PENDING_INTERACTION_STATUSES = ["pending"];
|
|
789
|
+
const BLOCKER_ATTENTION_PENDING_APPROVAL_STATUSES = ["pending", "revision_requested"];
|
|
790
|
+
const BLOCKER_ATTENTION_OPEN_RECOVERY_ORIGIN_KIND = "harness_liveness_escalation";
|
|
791
|
+
const PRODUCTIVITY_REVIEW_ORIGIN_KIND = "issue_productivity_review";
|
|
792
|
+
const PRODUCTIVITY_REVIEW_TERMINAL_STATUSES = ["done", "cancelled"];
|
|
793
|
+
const PRODUCTIVITY_REVIEW_ACTIVITY_ACTIONS = [
|
|
794
|
+
"issue.productivity_review_created",
|
|
795
|
+
"issue.productivity_review_updated",
|
|
796
|
+
];
|
|
797
|
+
const PRODUCTIVITY_REVIEW_TRIGGERS = [
|
|
798
|
+
"no_comment_streak",
|
|
799
|
+
"long_active_duration",
|
|
800
|
+
"high_churn",
|
|
801
|
+
];
|
|
802
|
+
const BLOCKER_ATTENTION_OPEN_RECOVERY_TERMINAL_STATUSES = ["done", "cancelled"];
|
|
803
|
+
const BLOCKER_ATTENTION_MAX_DEPTH = 8;
|
|
804
|
+
const BLOCKER_ATTENTION_MAX_NODES = 2000;
|
|
805
|
+
const BLOCKER_ATTENTION_INVOKABLE_AGENT_STATUSES = new Set(["active", "idle", "running", "error"]);
|
|
806
|
+
async function activeRunMapForIssues(dbOrTx, issueRows) {
|
|
807
|
+
const map = new Map();
|
|
808
|
+
const runIds = issueRows
|
|
809
|
+
.map((row) => row.executionRunId)
|
|
810
|
+
.filter((id) => id != null);
|
|
811
|
+
if (runIds.length === 0)
|
|
812
|
+
return map;
|
|
813
|
+
for (const runIdChunk of chunkList([...new Set(runIds)], ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
814
|
+
const rows = await dbOrTx
|
|
815
|
+
.select({
|
|
816
|
+
id: heartbeatRuns.id,
|
|
817
|
+
status: heartbeatRuns.status,
|
|
818
|
+
agentId: heartbeatRuns.agentId,
|
|
819
|
+
invocationSource: heartbeatRuns.invocationSource,
|
|
820
|
+
triggerDetail: heartbeatRuns.triggerDetail,
|
|
821
|
+
startedAt: heartbeatRuns.startedAt,
|
|
822
|
+
finishedAt: heartbeatRuns.finishedAt,
|
|
823
|
+
createdAt: heartbeatRuns.createdAt,
|
|
824
|
+
})
|
|
825
|
+
.from(heartbeatRuns)
|
|
826
|
+
.where(and(inArray(heartbeatRuns.id, runIdChunk), inArray(heartbeatRuns.status, ACTIVE_RUN_STATUSES)));
|
|
827
|
+
for (const row of rows) {
|
|
828
|
+
map.set(row.id, row);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return map;
|
|
832
|
+
}
|
|
833
|
+
function createIssueBlockerAttention(input = {}) {
|
|
834
|
+
return {
|
|
835
|
+
state: input.state ?? "none",
|
|
836
|
+
reason: input.reason ?? null,
|
|
837
|
+
unresolvedBlockerCount: input.unresolvedBlockerCount ?? 0,
|
|
838
|
+
coveredBlockerCount: input.coveredBlockerCount ?? 0,
|
|
839
|
+
stalledBlockerCount: input.stalledBlockerCount ?? 0,
|
|
840
|
+
attentionBlockerCount: input.attentionBlockerCount ?? 0,
|
|
841
|
+
sampleBlockerIdentifier: input.sampleBlockerIdentifier ?? null,
|
|
842
|
+
sampleStalledBlockerIdentifier: input.sampleStalledBlockerIdentifier ?? null,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function blockerSampleIdentifier(node) {
|
|
846
|
+
return node?.identifier ?? node?.id ?? null;
|
|
847
|
+
}
|
|
848
|
+
function appendBlockerAttentionEdges(edgesByIssueId, rows) {
|
|
849
|
+
for (const row of rows) {
|
|
850
|
+
const existing = edgesByIssueId.get(row.issueId) ?? [];
|
|
851
|
+
if (!existing.some((edge) => edge.blockerIssueId === row.blockerIssueId)) {
|
|
852
|
+
existing.push(row);
|
|
853
|
+
edgesByIssueId.set(row.issueId, existing);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function summarizeIssueRelationRow(row) {
|
|
858
|
+
return {
|
|
859
|
+
id: row.relatedId,
|
|
860
|
+
identifier: row.identifier,
|
|
861
|
+
title: row.title,
|
|
862
|
+
status: row.status,
|
|
863
|
+
priority: row.priority,
|
|
864
|
+
assigneeAgentId: row.assigneeAgentId,
|
|
865
|
+
assigneeUserId: row.assigneeUserId,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async function terminalExplicitBlockersByRoot(squadId, roots, dbOrTx) {
|
|
869
|
+
const rootIds = [...new Set(roots.map((root) => root.id))];
|
|
870
|
+
const terminalByRoot = new Map();
|
|
871
|
+
if (rootIds.length === 0)
|
|
872
|
+
return terminalByRoot;
|
|
873
|
+
const nodesById = new Map();
|
|
874
|
+
const edgesByIssueId = new Map();
|
|
875
|
+
for (const root of roots)
|
|
876
|
+
nodesById.set(root.id, root);
|
|
877
|
+
let frontier = rootIds;
|
|
878
|
+
for (let depth = 0; frontier.length > 0 && depth < BLOCKER_ATTENTION_MAX_DEPTH; depth += 1) {
|
|
879
|
+
const nextFrontier = new Set();
|
|
880
|
+
for (const chunk of chunkList([...new Set(frontier)], ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
881
|
+
const rows = await dbOrTx
|
|
882
|
+
.select({
|
|
883
|
+
currentIssueId: issueRelations.relatedIssueId,
|
|
884
|
+
relatedId: issues.id,
|
|
885
|
+
identifier: issues.identifier,
|
|
886
|
+
title: issues.title,
|
|
887
|
+
status: issues.status,
|
|
888
|
+
priority: issues.priority,
|
|
889
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
890
|
+
assigneeUserId: issues.assigneeUserId,
|
|
891
|
+
})
|
|
892
|
+
.from(issueRelations)
|
|
893
|
+
.innerJoin(issues, eq(issueRelations.issueId, issues.id))
|
|
894
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.type, "blocks"), inArray(issueRelations.relatedIssueId, chunk), eq(issues.squadId, squadId), ne(issues.status, "done")));
|
|
895
|
+
for (const row of rows) {
|
|
896
|
+
const existingEdges = edgesByIssueId.get(row.currentIssueId) ?? [];
|
|
897
|
+
if (!existingEdges.includes(row.relatedId)) {
|
|
898
|
+
existingEdges.push(row.relatedId);
|
|
899
|
+
edgesByIssueId.set(row.currentIssueId, existingEdges);
|
|
900
|
+
}
|
|
901
|
+
if (!nodesById.has(row.relatedId)) {
|
|
902
|
+
nodesById.set(row.relatedId, summarizeIssueRelationRow(row));
|
|
903
|
+
nextFrontier.add(row.relatedId);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (nodesById.size > BLOCKER_ATTENTION_MAX_NODES)
|
|
908
|
+
break;
|
|
909
|
+
frontier = [...nextFrontier];
|
|
910
|
+
}
|
|
911
|
+
const collectTerminal = (issueId, seen) => {
|
|
912
|
+
if (seen.has(issueId))
|
|
913
|
+
return [];
|
|
914
|
+
const node = nodesById.get(issueId);
|
|
915
|
+
if (!node || node.status === "done")
|
|
916
|
+
return [];
|
|
917
|
+
const nextSeen = new Set(seen);
|
|
918
|
+
nextSeen.add(issueId);
|
|
919
|
+
const downstreamIds = edgesByIssueId.get(issueId) ?? [];
|
|
920
|
+
if (downstreamIds.length === 0)
|
|
921
|
+
return [node];
|
|
922
|
+
return downstreamIds.flatMap((downstreamId) => collectTerminal(downstreamId, nextSeen));
|
|
923
|
+
};
|
|
924
|
+
for (const rootId of rootIds) {
|
|
925
|
+
const deduped = new Map();
|
|
926
|
+
for (const blocker of collectTerminal(rootId, new Set())) {
|
|
927
|
+
if (blocker.id !== rootId)
|
|
928
|
+
deduped.set(blocker.id, blocker);
|
|
929
|
+
}
|
|
930
|
+
if (deduped.size > 0) {
|
|
931
|
+
terminalByRoot.set(rootId, [...deduped.values()].sort((a, b) => a.title.localeCompare(b.title)));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return terminalByRoot;
|
|
935
|
+
}
|
|
936
|
+
function readProductivityReviewTrigger(value) {
|
|
937
|
+
if (typeof value !== "string")
|
|
938
|
+
return null;
|
|
939
|
+
return PRODUCTIVITY_REVIEW_TRIGGERS.includes(value)
|
|
940
|
+
? value
|
|
941
|
+
: null;
|
|
942
|
+
}
|
|
943
|
+
function readProductivityReviewStreak(value) {
|
|
944
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
945
|
+
return null;
|
|
946
|
+
return Math.floor(value);
|
|
947
|
+
}
|
|
948
|
+
async function listIssueProductivityReviewMap(dbOrTx, squadId, sourceIssueIds) {
|
|
949
|
+
const map = new Map();
|
|
950
|
+
if (sourceIssueIds.length === 0)
|
|
951
|
+
return map;
|
|
952
|
+
const reviewRows = [];
|
|
953
|
+
for (const chunk of chunkList([...new Set(sourceIssueIds)], ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
954
|
+
const rows = await dbOrTx
|
|
955
|
+
.select({
|
|
956
|
+
sourceIssueId: issues.originId,
|
|
957
|
+
reviewIssueId: issues.id,
|
|
958
|
+
reviewIdentifier: issues.identifier,
|
|
959
|
+
status: issues.status,
|
|
960
|
+
priority: issues.priority,
|
|
961
|
+
createdAt: issues.createdAt,
|
|
962
|
+
updatedAt: issues.updatedAt,
|
|
963
|
+
})
|
|
964
|
+
.from(issues)
|
|
965
|
+
.where(and(eq(issues.squadId, squadId), eq(issues.originKind, PRODUCTIVITY_REVIEW_ORIGIN_KIND), inArray(issues.originId, chunk), isNull(issues.hiddenAt), notInArray(issues.status, PRODUCTIVITY_REVIEW_TERMINAL_STATUSES)))
|
|
966
|
+
.orderBy(desc(issues.createdAt), desc(issues.id));
|
|
967
|
+
reviewRows.push(...rows);
|
|
968
|
+
}
|
|
969
|
+
if (reviewRows.length === 0)
|
|
970
|
+
return map;
|
|
971
|
+
const reviewIssueIds = reviewRows.map((row) => row.reviewIssueId);
|
|
972
|
+
const triggerByReviewIssueId = new Map();
|
|
973
|
+
for (const chunk of chunkList(reviewIssueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
974
|
+
const detailRows = await dbOrTx
|
|
975
|
+
.select({
|
|
976
|
+
entityId: activityLog.entityId,
|
|
977
|
+
details: activityLog.details,
|
|
978
|
+
createdAt: activityLog.createdAt,
|
|
979
|
+
})
|
|
980
|
+
.from(activityLog)
|
|
981
|
+
.where(and(eq(activityLog.squadId, squadId), eq(activityLog.entityType, "issue"), inArray(activityLog.entityId, chunk), inArray(activityLog.action, PRODUCTIVITY_REVIEW_ACTIVITY_ACTIONS)))
|
|
982
|
+
.orderBy(desc(activityLog.createdAt));
|
|
983
|
+
for (const row of detailRows) {
|
|
984
|
+
if (triggerByReviewIssueId.has(row.entityId))
|
|
985
|
+
continue;
|
|
986
|
+
triggerByReviewIssueId.set(row.entityId, {
|
|
987
|
+
trigger: readProductivityReviewTrigger(row.details?.trigger),
|
|
988
|
+
noCommentStreak: readProductivityReviewStreak(row.details?.noCommentStreak),
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
for (const row of reviewRows) {
|
|
993
|
+
if (!row.sourceIssueId)
|
|
994
|
+
continue;
|
|
995
|
+
if (map.has(row.sourceIssueId))
|
|
996
|
+
continue;
|
|
997
|
+
const detail = triggerByReviewIssueId.get(row.reviewIssueId);
|
|
998
|
+
map.set(row.sourceIssueId, {
|
|
999
|
+
reviewIssueId: row.reviewIssueId,
|
|
1000
|
+
reviewIdentifier: row.reviewIdentifier,
|
|
1001
|
+
status: row.status,
|
|
1002
|
+
priority: row.priority,
|
|
1003
|
+
trigger: detail?.trigger ?? null,
|
|
1004
|
+
noCommentStreak: detail?.noCommentStreak ?? null,
|
|
1005
|
+
createdAt: row.createdAt,
|
|
1006
|
+
updatedAt: row.updatedAt,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return map;
|
|
1010
|
+
}
|
|
1011
|
+
async function listIssueBlockerAttentionMap(dbOrTx, squadId, issueRows) {
|
|
1012
|
+
const roots = issueRows.filter((row) => row.squadId === squadId && row.status === "blocked");
|
|
1013
|
+
const attentionMap = new Map();
|
|
1014
|
+
for (const row of issueRows) {
|
|
1015
|
+
if (row.status !== "blocked") {
|
|
1016
|
+
attentionMap.set(row.id, createIssueBlockerAttention());
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (roots.length === 0)
|
|
1020
|
+
return attentionMap;
|
|
1021
|
+
const nodesById = new Map();
|
|
1022
|
+
const edgesByIssueId = new Map();
|
|
1023
|
+
for (const root of roots)
|
|
1024
|
+
nodesById.set(root.id, { ...root });
|
|
1025
|
+
let frontier = roots.map((root) => root.id);
|
|
1026
|
+
let truncated = false;
|
|
1027
|
+
for (let depth = 0; frontier.length > 0 && depth < BLOCKER_ATTENTION_MAX_DEPTH; depth += 1) {
|
|
1028
|
+
const nextFrontier = new Set();
|
|
1029
|
+
for (const chunk of chunkList([...new Set(frontier)], ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1030
|
+
const explicitBlockerRowsPromise = dbOrTx
|
|
1031
|
+
.select({
|
|
1032
|
+
issueId: issueRelations.relatedIssueId,
|
|
1033
|
+
blockerIssueId: issues.id,
|
|
1034
|
+
id: issues.id,
|
|
1035
|
+
squadId: issues.squadId,
|
|
1036
|
+
parentId: issues.parentId,
|
|
1037
|
+
identifier: issues.identifier,
|
|
1038
|
+
title: issues.title,
|
|
1039
|
+
status: issues.status,
|
|
1040
|
+
executionRunId: issues.executionRunId,
|
|
1041
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
1042
|
+
assigneeUserId: issues.assigneeUserId,
|
|
1043
|
+
})
|
|
1044
|
+
.from(issueRelations)
|
|
1045
|
+
.innerJoin(issues, eq(issueRelations.issueId, issues.id))
|
|
1046
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.type, "blocks"), inArray(issueRelations.relatedIssueId, chunk), eq(issues.squadId, squadId), ne(issues.status, "done")));
|
|
1047
|
+
const childRowsPromise = dbOrTx
|
|
1048
|
+
.select({
|
|
1049
|
+
issueId: issues.parentId,
|
|
1050
|
+
blockerIssueId: issues.id,
|
|
1051
|
+
id: issues.id,
|
|
1052
|
+
squadId: issues.squadId,
|
|
1053
|
+
parentId: issues.parentId,
|
|
1054
|
+
identifier: issues.identifier,
|
|
1055
|
+
title: issues.title,
|
|
1056
|
+
status: issues.status,
|
|
1057
|
+
executionRunId: issues.executionRunId,
|
|
1058
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
1059
|
+
assigneeUserId: issues.assigneeUserId,
|
|
1060
|
+
})
|
|
1061
|
+
.from(issues)
|
|
1062
|
+
.where(and(eq(issues.squadId, squadId), inArray(issues.parentId, chunk), ne(issues.status, "done")));
|
|
1063
|
+
const [explicitBlockerRows, childRows] = await Promise.all([
|
|
1064
|
+
explicitBlockerRowsPromise,
|
|
1065
|
+
childRowsPromise,
|
|
1066
|
+
]);
|
|
1067
|
+
appendBlockerAttentionEdges(edgesByIssueId, [
|
|
1068
|
+
...explicitBlockerRows
|
|
1069
|
+
.filter((row) => row.issueId !== null)
|
|
1070
|
+
.map((row) => ({ issueId: row.issueId, blockerIssueId: row.blockerIssueId })),
|
|
1071
|
+
...childRows
|
|
1072
|
+
.filter((row) => row.issueId !== null)
|
|
1073
|
+
.map((row) => ({ issueId: row.issueId, blockerIssueId: row.blockerIssueId })),
|
|
1074
|
+
]);
|
|
1075
|
+
for (const row of [...explicitBlockerRows, ...childRows]) {
|
|
1076
|
+
if (!row.issueId || nodesById.has(row.blockerIssueId))
|
|
1077
|
+
continue;
|
|
1078
|
+
nodesById.set(row.blockerIssueId, {
|
|
1079
|
+
id: row.blockerIssueId,
|
|
1080
|
+
squadId: row.squadId,
|
|
1081
|
+
parentId: row.parentId,
|
|
1082
|
+
identifier: row.identifier,
|
|
1083
|
+
title: row.title,
|
|
1084
|
+
status: row.status,
|
|
1085
|
+
executionRunId: row.executionRunId,
|
|
1086
|
+
assigneeAgentId: row.assigneeAgentId,
|
|
1087
|
+
assigneeUserId: row.assigneeUserId,
|
|
1088
|
+
});
|
|
1089
|
+
nextFrontier.add(row.blockerIssueId);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (nodesById.size > BLOCKER_ATTENTION_MAX_NODES) {
|
|
1093
|
+
truncated = true;
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
frontier = [...nextFrontier];
|
|
1097
|
+
}
|
|
1098
|
+
if (frontier.length > 0)
|
|
1099
|
+
truncated = true;
|
|
1100
|
+
const nodeIds = [...nodesById.keys()];
|
|
1101
|
+
const activeIssueIds = new Set();
|
|
1102
|
+
const agentIds = new Set();
|
|
1103
|
+
const issueIdByExecutionRunId = new Map();
|
|
1104
|
+
for (const node of nodesById.values()) {
|
|
1105
|
+
if (node.assigneeAgentId)
|
|
1106
|
+
agentIds.add(node.assigneeAgentId);
|
|
1107
|
+
if (node.executionRunId)
|
|
1108
|
+
issueIdByExecutionRunId.set(node.executionRunId, node.id);
|
|
1109
|
+
}
|
|
1110
|
+
for (const chunk of chunkList([...issueIdByExecutionRunId.keys()], ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1111
|
+
const runRows = await dbOrTx
|
|
1112
|
+
.select({
|
|
1113
|
+
id: heartbeatRuns.id,
|
|
1114
|
+
})
|
|
1115
|
+
.from(heartbeatRuns)
|
|
1116
|
+
.where(and(eq(heartbeatRuns.squadId, squadId), inArray(heartbeatRuns.status, BLOCKER_ATTENTION_ACTIVE_RUN_STATUSES), inArray(heartbeatRuns.id, chunk)));
|
|
1117
|
+
for (const row of runRows) {
|
|
1118
|
+
const issueId = issueIdByExecutionRunId.get(row.id);
|
|
1119
|
+
if (issueId)
|
|
1120
|
+
activeIssueIds.add(issueId);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
for (const chunk of chunkList(nodeIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1124
|
+
const wakeRowsPromise = dbOrTx
|
|
1125
|
+
.select({
|
|
1126
|
+
issueId: sql `${agentWakeupRequests.payload} ->> 'issueId'`,
|
|
1127
|
+
})
|
|
1128
|
+
.from(agentWakeupRequests)
|
|
1129
|
+
.where(and(eq(agentWakeupRequests.squadId, squadId), inArray(agentWakeupRequests.status, BLOCKER_ATTENTION_ACTIVE_WAKE_STATUSES), sql `${agentWakeupRequests.runId} is null`, inArray(sql `${agentWakeupRequests.payload} ->> 'issueId'`, chunk)));
|
|
1130
|
+
const wakeRows = await wakeRowsPromise;
|
|
1131
|
+
for (const row of wakeRows) {
|
|
1132
|
+
if (row.issueId)
|
|
1133
|
+
activeIssueIds.add(row.issueId);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
const explicitWaitCandidateIds = [...nodesById.values()]
|
|
1137
|
+
.filter((node) => node.status !== "done")
|
|
1138
|
+
.map((node) => node.id);
|
|
1139
|
+
const explicitWaitingIssueIds = new Set();
|
|
1140
|
+
if (explicitWaitCandidateIds.length > 0) {
|
|
1141
|
+
for (const chunk of chunkList(explicitWaitCandidateIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1142
|
+
const interactionRows = await dbOrTx
|
|
1143
|
+
.select({ issueId: issueThreadInteractions.issueId })
|
|
1144
|
+
.from(issueThreadInteractions)
|
|
1145
|
+
.where(and(eq(issueThreadInteractions.squadId, squadId), inArray(issueThreadInteractions.status, BLOCKER_ATTENTION_PENDING_INTERACTION_STATUSES), inArray(issueThreadInteractions.issueId, chunk)));
|
|
1146
|
+
for (const row of interactionRows)
|
|
1147
|
+
explicitWaitingIssueIds.add(row.issueId);
|
|
1148
|
+
const approvalRows = await dbOrTx
|
|
1149
|
+
.select({ issueId: issueApprovals.issueId })
|
|
1150
|
+
.from(issueApprovals)
|
|
1151
|
+
.innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id))
|
|
1152
|
+
.where(and(eq(issueApprovals.squadId, squadId), inArray(approvals.status, BLOCKER_ATTENTION_PENDING_APPROVAL_STATUSES), inArray(issueApprovals.issueId, chunk)));
|
|
1153
|
+
for (const row of approvalRows)
|
|
1154
|
+
explicitWaitingIssueIds.add(row.issueId);
|
|
1155
|
+
}
|
|
1156
|
+
// Recovery rows are intentionally squad-wide: a liveness escalation for
|
|
1157
|
+
// the same leaf blocker represents an active waiting path even when that
|
|
1158
|
+
// blocker is reached through another blocked graph.
|
|
1159
|
+
const recoveryRows = await dbOrTx
|
|
1160
|
+
.select({ id: issues.id, originId: issues.originId })
|
|
1161
|
+
.from(issues)
|
|
1162
|
+
.where(and(eq(issues.squadId, squadId), eq(issues.originKind, BLOCKER_ATTENTION_OPEN_RECOVERY_ORIGIN_KIND), isNull(issues.hiddenAt), notInArray(issues.status, BLOCKER_ATTENTION_OPEN_RECOVERY_TERMINAL_STATUSES)));
|
|
1163
|
+
for (const row of recoveryRows) {
|
|
1164
|
+
const parsed = parseIssueGraphLivenessIncidentKey(row.originId);
|
|
1165
|
+
if (!parsed || parsed.squadId !== squadId)
|
|
1166
|
+
continue;
|
|
1167
|
+
explicitWaitingIssueIds.add(row.id);
|
|
1168
|
+
explicitWaitingIssueIds.add(parsed.issueId);
|
|
1169
|
+
explicitWaitingIssueIds.add(parsed.leafIssueId);
|
|
1170
|
+
}
|
|
1171
|
+
const recoveryActionRows = await dbOrTx
|
|
1172
|
+
.select({ sourceIssueId: issueRecoveryActions.sourceIssueId })
|
|
1173
|
+
.from(issueRecoveryActions)
|
|
1174
|
+
.where(and(eq(issueRecoveryActions.squadId, squadId), inArray(issueRecoveryActions.status, ["active", "escalated"]), inArray(issueRecoveryActions.sourceIssueId, explicitWaitCandidateIds)));
|
|
1175
|
+
for (const row of recoveryActionRows)
|
|
1176
|
+
explicitWaitingIssueIds.add(row.sourceIssueId);
|
|
1177
|
+
}
|
|
1178
|
+
const agentRows = agentIds.size > 0
|
|
1179
|
+
? await dbOrTx
|
|
1180
|
+
.select({
|
|
1181
|
+
id: agents.id,
|
|
1182
|
+
squadId: agents.squadId,
|
|
1183
|
+
status: agents.status,
|
|
1184
|
+
})
|
|
1185
|
+
.from(agents)
|
|
1186
|
+
.where(and(eq(agents.squadId, squadId), inArray(agents.id, [...agentIds])))
|
|
1187
|
+
: [];
|
|
1188
|
+
const agentsById = new Map(agentRows.map((agent) => [agent.id, agent]));
|
|
1189
|
+
const classifyPath = (nodeId, seen) => {
|
|
1190
|
+
const sample = blockerSampleIdentifier(nodesById.get(nodeId));
|
|
1191
|
+
if (truncated || seen.has(nodeId)) {
|
|
1192
|
+
return { covered: false, stalled: false, sampleBlockerIdentifier: sample, sampleStalledBlockerIdentifier: null };
|
|
1193
|
+
}
|
|
1194
|
+
const node = nodesById.get(nodeId);
|
|
1195
|
+
if (!node || node.squadId !== squadId) {
|
|
1196
|
+
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeId, sampleStalledBlockerIdentifier: null };
|
|
1197
|
+
}
|
|
1198
|
+
const nodeSample = blockerSampleIdentifier(node);
|
|
1199
|
+
if (node.status === "done") {
|
|
1200
|
+
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1201
|
+
}
|
|
1202
|
+
if (explicitWaitingIssueIds.has(node.id)) {
|
|
1203
|
+
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1204
|
+
}
|
|
1205
|
+
if (node.assigneeUserId && node.status !== "cancelled") {
|
|
1206
|
+
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1207
|
+
}
|
|
1208
|
+
if (node.status === "in_review") {
|
|
1209
|
+
const hasWaitingPath = activeIssueIds.has(node.id) || Boolean(node.assigneeUserId);
|
|
1210
|
+
if (hasWaitingPath) {
|
|
1211
|
+
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1212
|
+
}
|
|
1213
|
+
return { covered: false, stalled: true, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: nodeSample };
|
|
1214
|
+
}
|
|
1215
|
+
if (activeIssueIds.has(node.id)) {
|
|
1216
|
+
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1217
|
+
}
|
|
1218
|
+
if (node.status === "cancelled") {
|
|
1219
|
+
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1220
|
+
}
|
|
1221
|
+
if (node.status === "backlog" && node.assigneeAgentId) {
|
|
1222
|
+
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1223
|
+
}
|
|
1224
|
+
const downstream = (edgesByIssueId.get(node.id) ?? []).filter((edge) => nodesById.get(edge.blockerIssueId)?.status !== "done");
|
|
1225
|
+
if (downstream.length > 0) {
|
|
1226
|
+
const nextSeen = new Set(seen);
|
|
1227
|
+
nextSeen.add(nodeId);
|
|
1228
|
+
const classified = downstream.map((edge) => classifyPath(edge.blockerIssueId, nextSeen));
|
|
1229
|
+
const stalledChild = classified.find((result) => result.stalled || result.sampleStalledBlockerIdentifier);
|
|
1230
|
+
const sampleStalled = stalledChild?.sampleStalledBlockerIdentifier ?? null;
|
|
1231
|
+
const hardAttention = classified.find((result) => !result.covered && !result.stalled);
|
|
1232
|
+
if (hardAttention) {
|
|
1233
|
+
return {
|
|
1234
|
+
covered: false,
|
|
1235
|
+
stalled: false,
|
|
1236
|
+
sampleBlockerIdentifier: hardAttention.sampleBlockerIdentifier,
|
|
1237
|
+
sampleStalledBlockerIdentifier: sampleStalled,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
const stalledEntry = classified.find((result) => result.stalled);
|
|
1241
|
+
if (stalledEntry) {
|
|
1242
|
+
return {
|
|
1243
|
+
covered: false,
|
|
1244
|
+
stalled: true,
|
|
1245
|
+
sampleBlockerIdentifier: stalledEntry.sampleBlockerIdentifier,
|
|
1246
|
+
sampleStalledBlockerIdentifier: sampleStalled,
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
covered: true,
|
|
1251
|
+
stalled: false,
|
|
1252
|
+
sampleBlockerIdentifier: classified[0]?.sampleBlockerIdentifier ?? nodeSample,
|
|
1253
|
+
sampleStalledBlockerIdentifier: null,
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
if (node.assigneeAgentId) {
|
|
1257
|
+
const assignee = agentsById.get(node.assigneeAgentId);
|
|
1258
|
+
if (!assignee || assignee.squadId !== squadId || !BLOCKER_ATTENTION_INVOKABLE_AGENT_STATUSES.has(assignee.status)) {
|
|
1259
|
+
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
|
1263
|
+
};
|
|
1264
|
+
for (const root of roots) {
|
|
1265
|
+
const topLevelEdges = (edgesByIssueId.get(root.id) ?? []).filter((edge) => nodesById.get(edge.blockerIssueId)?.status !== "done");
|
|
1266
|
+
if (topLevelEdges.length === 0) {
|
|
1267
|
+
attentionMap.set(root.id, createIssueBlockerAttention({
|
|
1268
|
+
state: "needs_attention",
|
|
1269
|
+
reason: "attention_required",
|
|
1270
|
+
}));
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const classified = topLevelEdges.map((edge) => ({
|
|
1274
|
+
edge,
|
|
1275
|
+
result: classifyPath(edge.blockerIssueId, new Set([root.id])),
|
|
1276
|
+
}));
|
|
1277
|
+
const coveredBlockerCount = classified.filter((entry) => entry.result.covered).length;
|
|
1278
|
+
const stalledBlockerCount = classified.filter((entry) => entry.result.stalled).length;
|
|
1279
|
+
const attentionBlockerCount = classified.length - coveredBlockerCount - stalledBlockerCount;
|
|
1280
|
+
const hardAttentionEntry = classified.find((entry) => !entry.result.covered && !entry.result.stalled);
|
|
1281
|
+
const stalledEntry = classified.find((entry) => entry.result.stalled);
|
|
1282
|
+
const sampleEntry = hardAttentionEntry ?? stalledEntry ?? classified[0] ?? null;
|
|
1283
|
+
const sampleNode = sampleEntry ? nodesById.get(sampleEntry.edge.blockerIssueId) : null;
|
|
1284
|
+
const sampleStalledFromChain = classified
|
|
1285
|
+
.map((entry) => entry.result.sampleStalledBlockerIdentifier)
|
|
1286
|
+
.find((value) => value);
|
|
1287
|
+
let state;
|
|
1288
|
+
let reason;
|
|
1289
|
+
if (attentionBlockerCount > 0) {
|
|
1290
|
+
state = "needs_attention";
|
|
1291
|
+
reason = "attention_required";
|
|
1292
|
+
}
|
|
1293
|
+
else if (stalledBlockerCount > 0) {
|
|
1294
|
+
state = "stalled";
|
|
1295
|
+
reason = "stalled_review";
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
state = "covered";
|
|
1299
|
+
reason = topLevelEdges.every((edge) => nodesById.get(edge.blockerIssueId)?.parentId === root.id)
|
|
1300
|
+
? "active_child"
|
|
1301
|
+
: "active_dependency";
|
|
1302
|
+
}
|
|
1303
|
+
attentionMap.set(root.id, createIssueBlockerAttention({
|
|
1304
|
+
state,
|
|
1305
|
+
reason,
|
|
1306
|
+
unresolvedBlockerCount: topLevelEdges.length,
|
|
1307
|
+
coveredBlockerCount,
|
|
1308
|
+
stalledBlockerCount,
|
|
1309
|
+
attentionBlockerCount,
|
|
1310
|
+
sampleBlockerIdentifier: sampleEntry?.result.sampleBlockerIdentifier ?? blockerSampleIdentifier(sampleNode),
|
|
1311
|
+
sampleStalledBlockerIdentifier: stalledEntry?.result.sampleStalledBlockerIdentifier ?? sampleStalledFromChain ?? null,
|
|
1312
|
+
}));
|
|
1313
|
+
}
|
|
1314
|
+
return attentionMap;
|
|
1315
|
+
}
|
|
1316
|
+
const issueListSelect = {
|
|
1317
|
+
id: issues.id,
|
|
1318
|
+
squadId: issues.squadId,
|
|
1319
|
+
projectId: issues.projectId,
|
|
1320
|
+
projectWorkspaceId: issues.projectWorkspaceId,
|
|
1321
|
+
goalId: issues.goalId,
|
|
1322
|
+
parentId: issues.parentId,
|
|
1323
|
+
title: issues.title,
|
|
1324
|
+
description: sql `
|
|
1325
|
+
CASE
|
|
1326
|
+
WHEN ${issues.description} IS NULL THEN NULL
|
|
1327
|
+
ELSE encode(
|
|
1328
|
+
substring(
|
|
1329
|
+
convert_to(${issues.description}, current_setting('server_encoding'))
|
|
1330
|
+
FROM 1 FOR ${ISSUE_LIST_DESCRIPTION_MAX_BYTES}
|
|
1331
|
+
),
|
|
1332
|
+
'base64'
|
|
1333
|
+
)
|
|
1334
|
+
END
|
|
1335
|
+
`,
|
|
1336
|
+
status: issues.status,
|
|
1337
|
+
workMode: issues.workMode,
|
|
1338
|
+
priority: issues.priority,
|
|
1339
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
1340
|
+
assigneeUserId: issues.assigneeUserId,
|
|
1341
|
+
checkoutRunId: issues.checkoutRunId,
|
|
1342
|
+
executionRunId: issues.executionRunId,
|
|
1343
|
+
executionAgentNameKey: issues.executionAgentNameKey,
|
|
1344
|
+
executionLockedAt: issues.executionLockedAt,
|
|
1345
|
+
createdByAgentId: issues.createdByAgentId,
|
|
1346
|
+
createdByUserId: issues.createdByUserId,
|
|
1347
|
+
issueNumber: issues.issueNumber,
|
|
1348
|
+
identifier: issues.identifier,
|
|
1349
|
+
originKind: issues.originKind,
|
|
1350
|
+
originId: issues.originId,
|
|
1351
|
+
originRunId: issues.originRunId,
|
|
1352
|
+
originFingerprint: issues.originFingerprint,
|
|
1353
|
+
requestDepth: issues.requestDepth,
|
|
1354
|
+
billingCode: issues.billingCode,
|
|
1355
|
+
assigneeAdapterOverrides: issues.assigneeAdapterOverrides,
|
|
1356
|
+
executionPolicy: sql `null`,
|
|
1357
|
+
executionState: sql `null`,
|
|
1358
|
+
monitorNextCheckAt: issues.monitorNextCheckAt,
|
|
1359
|
+
monitorWakeRequestedAt: issues.monitorWakeRequestedAt,
|
|
1360
|
+
monitorLastTriggeredAt: issues.monitorLastTriggeredAt,
|
|
1361
|
+
monitorAttemptCount: issues.monitorAttemptCount,
|
|
1362
|
+
monitorNotes: issues.monitorNotes,
|
|
1363
|
+
monitorScheduledBy: issues.monitorScheduledBy,
|
|
1364
|
+
executionWorkspaceId: issues.executionWorkspaceId,
|
|
1365
|
+
executionWorkspacePreference: issues.executionWorkspacePreference,
|
|
1366
|
+
executionWorkspaceSettings: sql `null`,
|
|
1367
|
+
startedAt: issues.startedAt,
|
|
1368
|
+
completedAt: issues.completedAt,
|
|
1369
|
+
cancelledAt: issues.cancelledAt,
|
|
1370
|
+
hiddenAt: issues.hiddenAt,
|
|
1371
|
+
createdAt: issues.createdAt,
|
|
1372
|
+
updatedAt: issues.updatedAt,
|
|
1373
|
+
};
|
|
1374
|
+
function withActiveRuns(issueRows, runMap) {
|
|
1375
|
+
return issueRows.map((row) => ({
|
|
1376
|
+
...row,
|
|
1377
|
+
activeRun: row.executionRunId ? (runMap.get(row.executionRunId) ?? null) : null,
|
|
1378
|
+
}));
|
|
1379
|
+
}
|
|
1380
|
+
async function userCommentStatsForIssues(dbOrTx, squadId, userId, issueIds) {
|
|
1381
|
+
const stats = [];
|
|
1382
|
+
for (const issueIdChunk of chunkList(issueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1383
|
+
const rows = await dbOrTx
|
|
1384
|
+
.select({
|
|
1385
|
+
issueId: issueComments.issueId,
|
|
1386
|
+
myLastCommentAt: sql `
|
|
1387
|
+
MAX(CASE WHEN ${issueComments.authorUserId} = ${userId} THEN ${issueComments.createdAt} END)
|
|
1388
|
+
`,
|
|
1389
|
+
lastExternalCommentAt: sql `
|
|
1390
|
+
MAX(
|
|
1391
|
+
CASE
|
|
1392
|
+
WHEN ${issueComments.authorUserId} IS NULL OR ${issueComments.authorUserId} <> ${userId}
|
|
1393
|
+
THEN ${issueComments.createdAt}
|
|
1394
|
+
END
|
|
1395
|
+
)
|
|
1396
|
+
`,
|
|
1397
|
+
})
|
|
1398
|
+
.from(issueComments)
|
|
1399
|
+
.where(and(eq(issueComments.squadId, squadId), inArray(issueComments.issueId, issueIdChunk)))
|
|
1400
|
+
.groupBy(issueComments.issueId);
|
|
1401
|
+
stats.push(...rows);
|
|
1402
|
+
}
|
|
1403
|
+
return stats;
|
|
1404
|
+
}
|
|
1405
|
+
async function userReadStatsForIssues(dbOrTx, squadId, userId, issueIds) {
|
|
1406
|
+
const stats = [];
|
|
1407
|
+
for (const issueIdChunk of chunkList(issueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1408
|
+
const rows = await dbOrTx
|
|
1409
|
+
.select({
|
|
1410
|
+
issueId: issueReadStates.issueId,
|
|
1411
|
+
myLastReadAt: issueReadStates.lastReadAt,
|
|
1412
|
+
})
|
|
1413
|
+
.from(issueReadStates)
|
|
1414
|
+
.where(and(eq(issueReadStates.squadId, squadId), eq(issueReadStates.userId, userId), inArray(issueReadStates.issueId, issueIdChunk)));
|
|
1415
|
+
stats.push(...rows);
|
|
1416
|
+
}
|
|
1417
|
+
return stats;
|
|
1418
|
+
}
|
|
1419
|
+
async function lastActivityStatsForIssues(dbOrTx, squadId, issueIds) {
|
|
1420
|
+
const byIssueId = new Map();
|
|
1421
|
+
for (const issueIdChunk of chunkList(issueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1422
|
+
const [commentRows, logRows] = await Promise.all([
|
|
1423
|
+
dbOrTx
|
|
1424
|
+
.select({
|
|
1425
|
+
issueId: issueComments.issueId,
|
|
1426
|
+
latestCommentAt: sql `MAX(${issueComments.createdAt})`,
|
|
1427
|
+
})
|
|
1428
|
+
.from(issueComments)
|
|
1429
|
+
.where(and(eq(issueComments.squadId, squadId), inArray(issueComments.issueId, issueIdChunk)))
|
|
1430
|
+
.groupBy(issueComments.issueId),
|
|
1431
|
+
dbOrTx
|
|
1432
|
+
.select({
|
|
1433
|
+
issueId: activityLog.entityId,
|
|
1434
|
+
latestLogAt: sql `MAX(${activityLog.createdAt})`,
|
|
1435
|
+
})
|
|
1436
|
+
.from(activityLog)
|
|
1437
|
+
.where(and(eq(activityLog.squadId, squadId), eq(activityLog.entityType, "issue"), inArray(activityLog.entityId, issueIdChunk), sql `${activityLog.action} NOT IN (${sql.join(ISSUE_LOCAL_INBOX_ACTIVITY_ACTIONS.map((action) => sql `${action}`), sql `, `)})`))
|
|
1438
|
+
.groupBy(activityLog.entityId),
|
|
1439
|
+
]);
|
|
1440
|
+
for (const row of commentRows) {
|
|
1441
|
+
byIssueId.set(row.issueId, {
|
|
1442
|
+
issueId: row.issueId,
|
|
1443
|
+
latestCommentAt: row.latestCommentAt,
|
|
1444
|
+
latestLogAt: null,
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
for (const row of logRows) {
|
|
1448
|
+
const existing = byIssueId.get(row.issueId);
|
|
1449
|
+
if (existing)
|
|
1450
|
+
existing.latestLogAt = row.latestLogAt;
|
|
1451
|
+
else {
|
|
1452
|
+
byIssueId.set(row.issueId, {
|
|
1453
|
+
issueId: row.issueId,
|
|
1454
|
+
latestCommentAt: null,
|
|
1455
|
+
latestLogAt: row.latestLogAt,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return [...byIssueId.values()];
|
|
1461
|
+
}
|
|
1462
|
+
async function blockedByMapForIssues(dbOrTx, squadId, issueIds) {
|
|
1463
|
+
const map = new Map();
|
|
1464
|
+
const uniqueIssueIds = [...new Set(issueIds)];
|
|
1465
|
+
if (uniqueIssueIds.length === 0)
|
|
1466
|
+
return map;
|
|
1467
|
+
for (const issueId of uniqueIssueIds) {
|
|
1468
|
+
map.set(issueId, []);
|
|
1469
|
+
}
|
|
1470
|
+
for (const issueIdChunk of chunkList(uniqueIssueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1471
|
+
const rows = await dbOrTx
|
|
1472
|
+
.select({
|
|
1473
|
+
currentIssueId: issueRelations.relatedIssueId,
|
|
1474
|
+
relatedId: issues.id,
|
|
1475
|
+
identifier: issues.identifier,
|
|
1476
|
+
title: issues.title,
|
|
1477
|
+
status: issues.status,
|
|
1478
|
+
priority: issues.priority,
|
|
1479
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
1480
|
+
assigneeUserId: issues.assigneeUserId,
|
|
1481
|
+
})
|
|
1482
|
+
.from(issueRelations)
|
|
1483
|
+
.innerJoin(issues, eq(issueRelations.issueId, issues.id))
|
|
1484
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.type, "blocks"), inArray(issueRelations.relatedIssueId, issueIdChunk)));
|
|
1485
|
+
for (const row of rows) {
|
|
1486
|
+
const blockedBy = map.get(row.currentIssueId);
|
|
1487
|
+
if (!blockedBy)
|
|
1488
|
+
continue;
|
|
1489
|
+
blockedBy.push({
|
|
1490
|
+
id: row.relatedId,
|
|
1491
|
+
identifier: row.identifier,
|
|
1492
|
+
title: row.title,
|
|
1493
|
+
status: row.status,
|
|
1494
|
+
priority: row.priority,
|
|
1495
|
+
assigneeAgentId: row.assigneeAgentId,
|
|
1496
|
+
assigneeUserId: row.assigneeUserId,
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
for (const blockedBy of map.values()) {
|
|
1501
|
+
blockedBy.sort((a, b) => a.title.localeCompare(b.title));
|
|
1502
|
+
}
|
|
1503
|
+
return map;
|
|
1504
|
+
}
|
|
1505
|
+
const BLOCKED_INBOX_TERMINAL_STATUSES = ["done", "cancelled"];
|
|
1506
|
+
const BLOCKED_INBOX_ACTIVE_RUN_STATUSES = ["queued", "running"];
|
|
1507
|
+
const BLOCKED_INBOX_ACTIVE_WAKE_STATUSES = ["queued", "deferred_issue_execution"];
|
|
1508
|
+
const BLOCKED_INBOX_PENDING_INTERACTION_STATUSES = ["pending"];
|
|
1509
|
+
const BLOCKED_INBOX_PENDING_APPROVAL_STATUSES = ["pending", "revision_requested"];
|
|
1510
|
+
const BLOCKED_INBOX_RECOVERY_ORIGIN_KINDS = ["harness_liveness_escalation", "stranded_issue_recovery"];
|
|
1511
|
+
const BLOCKED_INBOX_SUCCESSFUL_RUN_HANDOFF_ACTIONS = [
|
|
1512
|
+
"issue.successful_run_handoff_required",
|
|
1513
|
+
"issue.successful_run_handoff_resolved",
|
|
1514
|
+
"issue.successful_run_handoff_escalated",
|
|
1515
|
+
];
|
|
1516
|
+
function issueRef(row) {
|
|
1517
|
+
if (!row)
|
|
1518
|
+
return null;
|
|
1519
|
+
return {
|
|
1520
|
+
id: row.id,
|
|
1521
|
+
identifier: row.identifier,
|
|
1522
|
+
title: row.title,
|
|
1523
|
+
status: row.status,
|
|
1524
|
+
priority: row.priority,
|
|
1525
|
+
assigneeAgentId: row.assigneeAgentId,
|
|
1526
|
+
assigneeUserId: row.assigneeUserId,
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function isoDate(value) {
|
|
1530
|
+
if (!value)
|
|
1531
|
+
return null;
|
|
1532
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
1533
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
1534
|
+
}
|
|
1535
|
+
function attentionBase(input) {
|
|
1536
|
+
return {
|
|
1537
|
+
kind: "blocked",
|
|
1538
|
+
state: input.state,
|
|
1539
|
+
reason: input.reason,
|
|
1540
|
+
severity: input.severity,
|
|
1541
|
+
stoppedSinceAt: isoDate(input.stoppedSinceAt),
|
|
1542
|
+
owner: input.owner,
|
|
1543
|
+
action: input.action,
|
|
1544
|
+
sourceIssue: input.sourceIssue,
|
|
1545
|
+
leafIssue: input.leafIssue ?? null,
|
|
1546
|
+
recoveryIssue: input.recoveryIssue ?? null,
|
|
1547
|
+
approvalId: input.approvalId ?? null,
|
|
1548
|
+
interactionId: input.interactionId ?? null,
|
|
1549
|
+
sampleIssueIdentifier: input.sampleIssueIdentifier
|
|
1550
|
+
?? input.leafIssue?.identifier
|
|
1551
|
+
?? input.recoveryIssue?.identifier
|
|
1552
|
+
?? input.sourceIssue?.identifier
|
|
1553
|
+
?? null,
|
|
1554
|
+
redaction: {
|
|
1555
|
+
externalDetailsRedacted: input.externalDetailsRedacted ?? false,
|
|
1556
|
+
secretFieldsOmitted: true,
|
|
1557
|
+
},
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
function readSuccessfulRunHandoffFromActivity(row) {
|
|
1561
|
+
const details = row.details ?? {};
|
|
1562
|
+
const state = row.action === "issue.successful_run_handoff_required"
|
|
1563
|
+
? "required"
|
|
1564
|
+
: row.action === "issue.successful_run_handoff_resolved"
|
|
1565
|
+
? "resolved"
|
|
1566
|
+
: row.action === "issue.successful_run_handoff_escalated"
|
|
1567
|
+
? "escalated"
|
|
1568
|
+
: null;
|
|
1569
|
+
if (!state)
|
|
1570
|
+
return null;
|
|
1571
|
+
const detectedProgressSummary = readStringFromRecord(details, "detectedProgressSummary")
|
|
1572
|
+
?? readStringFromRecord(details, "detected_progress_summary")
|
|
1573
|
+
?? null;
|
|
1574
|
+
return {
|
|
1575
|
+
state,
|
|
1576
|
+
required: state === "required",
|
|
1577
|
+
sourceRunId: readStringFromRecord(details, "sourceRunId")
|
|
1578
|
+
?? readStringFromRecord(details, "source_run_id")
|
|
1579
|
+
?? readStringFromRecord(details, "resumeFromRunId")
|
|
1580
|
+
?? row.runId
|
|
1581
|
+
?? null,
|
|
1582
|
+
correctiveRunId: readStringFromRecord(details, "correctiveRunId")
|
|
1583
|
+
?? readStringFromRecord(details, "corrective_run_id")
|
|
1584
|
+
?? (state !== "required" ? row.runId : null),
|
|
1585
|
+
assigneeAgentId: readStringFromRecord(details, "assigneeAgentId")
|
|
1586
|
+
?? readStringFromRecord(details, "agentId")
|
|
1587
|
+
?? row.agentId
|
|
1588
|
+
?? null,
|
|
1589
|
+
detectedProgressSummary: detectedProgressSummary ? redactSensitiveText(detectedProgressSummary) : null,
|
|
1590
|
+
createdAt: row.createdAt,
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
async function listSuccessfulRunHandoffMapForIssues(dbOrTx, squadId, issueIds) {
|
|
1594
|
+
const uniqueIssueIds = [...new Set(issueIds)];
|
|
1595
|
+
const states = new Map();
|
|
1596
|
+
if (uniqueIssueIds.length === 0)
|
|
1597
|
+
return states;
|
|
1598
|
+
for (const issueIdChunk of chunkList(uniqueIssueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
1599
|
+
const rows = await dbOrTx
|
|
1600
|
+
.select({
|
|
1601
|
+
entityId: activityLog.entityId,
|
|
1602
|
+
action: activityLog.action,
|
|
1603
|
+
agentId: activityLog.agentId,
|
|
1604
|
+
runId: activityLog.runId,
|
|
1605
|
+
details: activityLog.details,
|
|
1606
|
+
createdAt: activityLog.createdAt,
|
|
1607
|
+
})
|
|
1608
|
+
.from(activityLog)
|
|
1609
|
+
.where(and(eq(activityLog.squadId, squadId), eq(activityLog.entityType, "issue"), inArray(activityLog.entityId, issueIdChunk), inArray(activityLog.action, [...BLOCKED_INBOX_SUCCESSFUL_RUN_HANDOFF_ACTIONS])))
|
|
1610
|
+
.orderBy(activityLog.entityId, desc(activityLog.createdAt), desc(activityLog.id));
|
|
1611
|
+
for (const row of rows) {
|
|
1612
|
+
if (states.has(row.entityId))
|
|
1613
|
+
continue;
|
|
1614
|
+
const state = readSuccessfulRunHandoffFromActivity(row);
|
|
1615
|
+
if (state)
|
|
1616
|
+
states.set(row.entityId, state);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return states;
|
|
1620
|
+
}
|
|
1621
|
+
function externalWaitFromDescription(description) {
|
|
1622
|
+
if (!description)
|
|
1623
|
+
return null;
|
|
1624
|
+
const owner = description.match(/^\s*external owner\s*:\s*(.+)$/im)?.[1]?.trim();
|
|
1625
|
+
const action = description.match(/^\s*external action\s*:\s*(.+)$/im)?.[1]?.trim();
|
|
1626
|
+
if (!owner || !action)
|
|
1627
|
+
return null;
|
|
1628
|
+
return {
|
|
1629
|
+
owner: owner.slice(0, 120),
|
|
1630
|
+
action: action.slice(0, 240),
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
function escapeRegExp(value) {
|
|
1634
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1635
|
+
}
|
|
1636
|
+
function redactExternalWaitDescription(description, external) {
|
|
1637
|
+
if (!description)
|
|
1638
|
+
return null;
|
|
1639
|
+
let redacted = description
|
|
1640
|
+
.split(/\r?\n/)
|
|
1641
|
+
.filter((line) => !/^\s*external\s+(?:owner|action)\s*:/i.test(line))
|
|
1642
|
+
.join("\n");
|
|
1643
|
+
for (const value of [external?.owner, external?.action]) {
|
|
1644
|
+
if (!value)
|
|
1645
|
+
continue;
|
|
1646
|
+
redacted = redacted.replace(new RegExp(escapeRegExp(value), "gi"), "[redacted external wait detail]");
|
|
1647
|
+
}
|
|
1648
|
+
redacted = redacted.replace(/\n{3,}/g, "\n\n").trim();
|
|
1649
|
+
return redacted.length > 0 ? redacted : null;
|
|
1650
|
+
}
|
|
1651
|
+
function blockedInboxResponseDescription(attention, row) {
|
|
1652
|
+
if (!attention.redaction.externalDetailsRedacted)
|
|
1653
|
+
return row.description;
|
|
1654
|
+
return redactExternalWaitDescription(row.description, externalWaitFromDescription(row.description));
|
|
1655
|
+
}
|
|
1656
|
+
function blockedInboxSearchText(attention, row) {
|
|
1657
|
+
return [
|
|
1658
|
+
row.identifier,
|
|
1659
|
+
row.title,
|
|
1660
|
+
blockedInboxResponseDescription(attention, row),
|
|
1661
|
+
attention.sourceIssue?.identifier,
|
|
1662
|
+
attention.sourceIssue?.title,
|
|
1663
|
+
attention.leafIssue?.identifier,
|
|
1664
|
+
attention.leafIssue?.title,
|
|
1665
|
+
attention.recoveryIssue?.identifier,
|
|
1666
|
+
attention.recoveryIssue?.title,
|
|
1667
|
+
attention.action.label,
|
|
1668
|
+
attention.action.detail,
|
|
1669
|
+
]
|
|
1670
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
1671
|
+
.join(" ")
|
|
1672
|
+
.toLowerCase();
|
|
1673
|
+
}
|
|
1674
|
+
function blockedInboxSeverityRank(severity) {
|
|
1675
|
+
switch (severity) {
|
|
1676
|
+
case "critical":
|
|
1677
|
+
return 0;
|
|
1678
|
+
case "high":
|
|
1679
|
+
return 1;
|
|
1680
|
+
case "medium":
|
|
1681
|
+
return 2;
|
|
1682
|
+
case "low":
|
|
1683
|
+
return 3;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
function issuePriorityRank(priority) {
|
|
1687
|
+
switch (priority) {
|
|
1688
|
+
case "critical":
|
|
1689
|
+
return 0;
|
|
1690
|
+
case "high":
|
|
1691
|
+
return 1;
|
|
1692
|
+
case "medium":
|
|
1693
|
+
return 2;
|
|
1694
|
+
case "low":
|
|
1695
|
+
return 3;
|
|
1696
|
+
default:
|
|
1697
|
+
return 4;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
function compareBlockedInboxRows(left, right) {
|
|
1701
|
+
const leftAttention = left.blockedInboxAttention;
|
|
1702
|
+
const rightAttention = right.blockedInboxAttention;
|
|
1703
|
+
const severity = blockedInboxSeverityRank(leftAttention.severity)
|
|
1704
|
+
- blockedInboxSeverityRank(rightAttention.severity);
|
|
1705
|
+
if (severity !== 0)
|
|
1706
|
+
return severity;
|
|
1707
|
+
const leftStopped = leftAttention.stoppedSinceAt
|
|
1708
|
+
? new Date(leftAttention.stoppedSinceAt).getTime()
|
|
1709
|
+
: Number.POSITIVE_INFINITY;
|
|
1710
|
+
const rightStopped = rightAttention.stoppedSinceAt
|
|
1711
|
+
? new Date(rightAttention.stoppedSinceAt).getTime()
|
|
1712
|
+
: Number.POSITIVE_INFINITY;
|
|
1713
|
+
if (leftStopped !== rightStopped)
|
|
1714
|
+
return leftStopped - rightStopped;
|
|
1715
|
+
const priority = issuePriorityRank(left.priority) - issuePriorityRank(right.priority);
|
|
1716
|
+
if (priority !== 0)
|
|
1717
|
+
return priority;
|
|
1718
|
+
const leftActivity = left.lastActivityAt ? new Date(left.lastActivityAt).getTime() : new Date(left.updatedAt).getTime();
|
|
1719
|
+
const rightActivity = right.lastActivityAt ? new Date(right.lastActivityAt).getTime() : new Date(right.updatedAt).getTime();
|
|
1720
|
+
if (leftActivity !== rightActivity)
|
|
1721
|
+
return rightActivity - leftActivity;
|
|
1722
|
+
return right.id.localeCompare(left.id);
|
|
1723
|
+
}
|
|
1724
|
+
async function listIssueBlockedInboxAttentionMap(dbOrTx, squadId, issueRows) {
|
|
1725
|
+
const rowIssueIds = [...new Set(issueRows.map((row) => row.id))];
|
|
1726
|
+
const result = new Map();
|
|
1727
|
+
if (rowIssueIds.length === 0)
|
|
1728
|
+
return result;
|
|
1729
|
+
const [graphIssueRows, graphRelationRows, squadAgentRows] = await Promise.all([
|
|
1730
|
+
dbOrTx
|
|
1731
|
+
.select()
|
|
1732
|
+
.from(issues)
|
|
1733
|
+
.where(and(eq(issues.squadId, squadId), isNull(issues.hiddenAt), notInArray(issues.status, [...BLOCKED_INBOX_TERMINAL_STATUSES]))),
|
|
1734
|
+
dbOrTx
|
|
1735
|
+
.select({
|
|
1736
|
+
squadId: issueRelations.squadId,
|
|
1737
|
+
blockerIssueId: issueRelations.issueId,
|
|
1738
|
+
blockedIssueId: issueRelations.relatedIssueId,
|
|
1739
|
+
})
|
|
1740
|
+
.from(issueRelations)
|
|
1741
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.type, "blocks"))),
|
|
1742
|
+
dbOrTx
|
|
1743
|
+
.select({
|
|
1744
|
+
id: agents.id,
|
|
1745
|
+
squadId: agents.squadId,
|
|
1746
|
+
name: agents.name,
|
|
1747
|
+
role: agents.role,
|
|
1748
|
+
title: agents.title,
|
|
1749
|
+
status: agents.status,
|
|
1750
|
+
reportsTo: agents.reportsTo,
|
|
1751
|
+
})
|
|
1752
|
+
.from(agents)
|
|
1753
|
+
.where(eq(agents.squadId, squadId)),
|
|
1754
|
+
]);
|
|
1755
|
+
const graphIssues = graphIssueRows;
|
|
1756
|
+
const graphRelations = graphRelationRows;
|
|
1757
|
+
const squadAgents = squadAgentRows;
|
|
1758
|
+
const graphIssueIds = graphIssues.map((issue) => issue.id);
|
|
1759
|
+
const issuesById = new Map(graphIssues.map((issue) => [issue.id, issue]));
|
|
1760
|
+
const [activeRunRows, wakeRows, scheduledRetryRows, interactionRows, approvalRows, handoffMap] = await Promise.all([
|
|
1761
|
+
graphIssueIds.length === 0
|
|
1762
|
+
? Promise.resolve([])
|
|
1763
|
+
: dbOrTx
|
|
1764
|
+
.select({
|
|
1765
|
+
squadId: heartbeatRuns.squadId,
|
|
1766
|
+
issueId: sql `${heartbeatRuns.contextSnapshot} ->> 'issueId'`,
|
|
1767
|
+
agentId: heartbeatRuns.agentId,
|
|
1768
|
+
status: heartbeatRuns.status,
|
|
1769
|
+
})
|
|
1770
|
+
.from(heartbeatRuns)
|
|
1771
|
+
.where(and(eq(heartbeatRuns.squadId, squadId), inArray(heartbeatRuns.status, [...BLOCKED_INBOX_ACTIVE_RUN_STATUSES]), inArray(sql `${heartbeatRuns.contextSnapshot} ->> 'issueId'`, graphIssueIds))),
|
|
1772
|
+
graphIssueIds.length === 0
|
|
1773
|
+
? Promise.resolve([])
|
|
1774
|
+
: dbOrTx
|
|
1775
|
+
.select({
|
|
1776
|
+
squadId: agentWakeupRequests.squadId,
|
|
1777
|
+
issueId: sql `${agentWakeupRequests.payload} ->> 'issueId'`,
|
|
1778
|
+
agentId: agentWakeupRequests.agentId,
|
|
1779
|
+
status: agentWakeupRequests.status,
|
|
1780
|
+
})
|
|
1781
|
+
.from(agentWakeupRequests)
|
|
1782
|
+
.where(and(eq(agentWakeupRequests.squadId, squadId), inArray(agentWakeupRequests.status, [...BLOCKED_INBOX_ACTIVE_WAKE_STATUSES]), sql `${agentWakeupRequests.runId} is null`, inArray(sql `${agentWakeupRequests.payload} ->> 'issueId'`, graphIssueIds))),
|
|
1783
|
+
graphIssueIds.length === 0
|
|
1784
|
+
? Promise.resolve([])
|
|
1785
|
+
: dbOrTx
|
|
1786
|
+
.select({
|
|
1787
|
+
squadId: heartbeatRuns.squadId,
|
|
1788
|
+
issueId: sql `${heartbeatRuns.contextSnapshot} ->> 'issueId'`,
|
|
1789
|
+
agentId: heartbeatRuns.agentId,
|
|
1790
|
+
status: heartbeatRuns.status,
|
|
1791
|
+
})
|
|
1792
|
+
.from(heartbeatRuns)
|
|
1793
|
+
.where(and(eq(heartbeatRuns.squadId, squadId), eq(heartbeatRuns.status, "scheduled_retry"), inArray(sql `${heartbeatRuns.contextSnapshot} ->> 'issueId'`, graphIssueIds))),
|
|
1794
|
+
graphIssueIds.length === 0
|
|
1795
|
+
? Promise.resolve([])
|
|
1796
|
+
: dbOrTx
|
|
1797
|
+
.select({
|
|
1798
|
+
id: issueThreadInteractions.id,
|
|
1799
|
+
issueId: issueThreadInteractions.issueId,
|
|
1800
|
+
kind: issueThreadInteractions.kind,
|
|
1801
|
+
createdAt: issueThreadInteractions.createdAt,
|
|
1802
|
+
})
|
|
1803
|
+
.from(issueThreadInteractions)
|
|
1804
|
+
.where(and(eq(issueThreadInteractions.squadId, squadId), inArray(issueThreadInteractions.status, [...BLOCKED_INBOX_PENDING_INTERACTION_STATUSES]), inArray(issueThreadInteractions.issueId, graphIssueIds))),
|
|
1805
|
+
graphIssueIds.length === 0
|
|
1806
|
+
? Promise.resolve([])
|
|
1807
|
+
: dbOrTx
|
|
1808
|
+
.select({
|
|
1809
|
+
approvalId: approvals.id,
|
|
1810
|
+
issueId: issueApprovals.issueId,
|
|
1811
|
+
createdAt: approvals.createdAt,
|
|
1812
|
+
})
|
|
1813
|
+
.from(issueApprovals)
|
|
1814
|
+
.innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id))
|
|
1815
|
+
.where(and(eq(issueApprovals.squadId, squadId), eq(approvals.squadId, squadId), inArray(approvals.status, [...BLOCKED_INBOX_PENDING_APPROVAL_STATUSES]), inArray(issueApprovals.issueId, graphIssueIds))),
|
|
1816
|
+
listSuccessfulRunHandoffMapForIssues(dbOrTx, squadId, rowIssueIds),
|
|
1817
|
+
]);
|
|
1818
|
+
const pendingInteractions = interactionRows.map((row) => ({
|
|
1819
|
+
squadId,
|
|
1820
|
+
issueId: row.issueId,
|
|
1821
|
+
status: "pending",
|
|
1822
|
+
}));
|
|
1823
|
+
const pendingApprovals = approvalRows.map((row) => ({
|
|
1824
|
+
squadId,
|
|
1825
|
+
issueId: row.issueId,
|
|
1826
|
+
status: "pending",
|
|
1827
|
+
}));
|
|
1828
|
+
const openRecoveryIssues = graphIssues
|
|
1829
|
+
.filter((issue) => BLOCKED_INBOX_RECOVERY_ORIGIN_KINDS.includes(issue.originKind))
|
|
1830
|
+
.flatMap((issue) => {
|
|
1831
|
+
const entries = [{ squadId, issueId: issue.id, status: issue.status }];
|
|
1832
|
+
if (issue.originKind === "harness_liveness_escalation") {
|
|
1833
|
+
const parsed = parseIssueGraphLivenessIncidentKey(issue.originId);
|
|
1834
|
+
if (parsed?.squadId === squadId) {
|
|
1835
|
+
entries.push({ squadId, issueId: parsed.issueId, status: issue.status });
|
|
1836
|
+
entries.push({ squadId, issueId: parsed.leafIssueId, status: issue.status });
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
else if (issue.originKind === "stranded_issue_recovery" && issue.originId) {
|
|
1840
|
+
entries.push({ squadId, issueId: issue.originId, status: issue.status });
|
|
1841
|
+
}
|
|
1842
|
+
return entries;
|
|
1843
|
+
});
|
|
1844
|
+
const findings = classifyIssueGraphLiveness({
|
|
1845
|
+
issues: graphIssues.map((issue) => ({
|
|
1846
|
+
id: issue.id,
|
|
1847
|
+
squadId: issue.squadId,
|
|
1848
|
+
identifier: issue.identifier,
|
|
1849
|
+
title: issue.title,
|
|
1850
|
+
status: issue.status,
|
|
1851
|
+
projectId: issue.projectId,
|
|
1852
|
+
goalId: issue.goalId,
|
|
1853
|
+
parentId: issue.parentId,
|
|
1854
|
+
assigneeAgentId: issue.assigneeAgentId,
|
|
1855
|
+
assigneeUserId: issue.assigneeUserId,
|
|
1856
|
+
createdByAgentId: issue.createdByAgentId,
|
|
1857
|
+
createdByUserId: issue.createdByUserId,
|
|
1858
|
+
executionPolicy: issue.executionPolicy,
|
|
1859
|
+
executionState: issue.executionState,
|
|
1860
|
+
monitorNextCheckAt: issue.monitorNextCheckAt,
|
|
1861
|
+
monitorAttemptCount: issue.monitorAttemptCount,
|
|
1862
|
+
})),
|
|
1863
|
+
relations: graphRelations,
|
|
1864
|
+
agents: squadAgents,
|
|
1865
|
+
activeRuns: activeRunRows
|
|
1866
|
+
.flatMap((row) => row.issueId
|
|
1867
|
+
? [{ squadId: row.squadId, issueId: row.issueId, agentId: row.agentId, status: row.status }]
|
|
1868
|
+
: []),
|
|
1869
|
+
queuedWakeRequests: [
|
|
1870
|
+
...wakeRows,
|
|
1871
|
+
...scheduledRetryRows,
|
|
1872
|
+
]
|
|
1873
|
+
.flatMap((row) => row.issueId
|
|
1874
|
+
? [{ squadId: row.squadId, issueId: row.issueId, agentId: row.agentId, status: row.status }]
|
|
1875
|
+
: []),
|
|
1876
|
+
pendingInteractions,
|
|
1877
|
+
pendingApprovals,
|
|
1878
|
+
openRecoveryIssues,
|
|
1879
|
+
now: new Date(),
|
|
1880
|
+
});
|
|
1881
|
+
const findingByIssueId = new Map();
|
|
1882
|
+
for (const finding of findings) {
|
|
1883
|
+
if (!findingByIssueId.has(finding.issueId))
|
|
1884
|
+
findingByIssueId.set(finding.issueId, finding);
|
|
1885
|
+
}
|
|
1886
|
+
const interactionByIssueId = new Map();
|
|
1887
|
+
for (const row of interactionRows) {
|
|
1888
|
+
if (!interactionByIssueId.has(row.issueId))
|
|
1889
|
+
interactionByIssueId.set(row.issueId, row);
|
|
1890
|
+
}
|
|
1891
|
+
const approvalByIssueId = new Map();
|
|
1892
|
+
for (const row of approvalRows) {
|
|
1893
|
+
if (!approvalByIssueId.has(row.issueId))
|
|
1894
|
+
approvalByIssueId.set(row.issueId, row);
|
|
1895
|
+
}
|
|
1896
|
+
for (const row of issueRows) {
|
|
1897
|
+
if (row.squadId !== squadId || BLOCKED_INBOX_TERMINAL_STATUSES.includes(row.status) || row.hiddenAt) {
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
const source = issueRef(row);
|
|
1901
|
+
const handoff = handoffMap.get(row.id);
|
|
1902
|
+
if (handoff && (handoff.required || handoff.state === "escalated")) {
|
|
1903
|
+
result.set(row.id, attentionBase({
|
|
1904
|
+
state: "missing_disposition",
|
|
1905
|
+
reason: "missing_successful_run_disposition",
|
|
1906
|
+
severity: "high",
|
|
1907
|
+
stoppedSinceAt: handoff.createdAt ?? row.updatedAt,
|
|
1908
|
+
owner: {
|
|
1909
|
+
type: row.assigneeAgentId ? "agent" : row.assigneeUserId ? "user" : "unknown",
|
|
1910
|
+
agentId: row.assigneeAgentId,
|
|
1911
|
+
userId: row.assigneeUserId,
|
|
1912
|
+
label: null,
|
|
1913
|
+
},
|
|
1914
|
+
action: {
|
|
1915
|
+
label: "Choose disposition",
|
|
1916
|
+
detail: "Choose exactly one final disposition: done, cancelled, review/input, blocked with owner, delegated follow-up, or queued continuation.",
|
|
1917
|
+
},
|
|
1918
|
+
sourceIssue: source,
|
|
1919
|
+
}));
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
if (BLOCKED_INBOX_RECOVERY_ORIGIN_KINDS.includes(row.originKind)) {
|
|
1923
|
+
let sourceIssue = null;
|
|
1924
|
+
let leafIssue = null;
|
|
1925
|
+
if (row.originKind === "harness_liveness_escalation") {
|
|
1926
|
+
const parsed = parseIssueGraphLivenessIncidentKey(row.originId);
|
|
1927
|
+
if (parsed?.squadId === squadId) {
|
|
1928
|
+
sourceIssue = issueRef(issuesById.get(parsed.issueId));
|
|
1929
|
+
leafIssue = issueRef(issuesById.get(parsed.leafIssueId));
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
else if (row.originKind === "stranded_issue_recovery" && row.originId) {
|
|
1933
|
+
sourceIssue = issueRef(issuesById.get(row.originId));
|
|
1934
|
+
}
|
|
1935
|
+
result.set(row.id, attentionBase({
|
|
1936
|
+
state: "recovery_open",
|
|
1937
|
+
reason: "open_recovery_issue",
|
|
1938
|
+
severity: "high",
|
|
1939
|
+
stoppedSinceAt: row.createdAt,
|
|
1940
|
+
owner: {
|
|
1941
|
+
type: row.assigneeAgentId ? "agent" : row.assigneeUserId ? "user" : "unknown",
|
|
1942
|
+
agentId: row.assigneeAgentId,
|
|
1943
|
+
userId: row.assigneeUserId,
|
|
1944
|
+
label: null,
|
|
1945
|
+
},
|
|
1946
|
+
action: {
|
|
1947
|
+
label: "Resolve recovery",
|
|
1948
|
+
detail: "Restore a live path for the source work or record why this recovery issue is a false positive.",
|
|
1949
|
+
},
|
|
1950
|
+
sourceIssue: sourceIssue ?? source,
|
|
1951
|
+
leafIssue,
|
|
1952
|
+
recoveryIssue: source,
|
|
1953
|
+
}));
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
const interaction = interactionByIssueId.get(row.id);
|
|
1957
|
+
if (interaction) {
|
|
1958
|
+
const isUserQuestion = interaction.kind === "ask_user_questions" && Boolean(row.assigneeUserId);
|
|
1959
|
+
result.set(row.id, attentionBase({
|
|
1960
|
+
state: "awaiting_decision",
|
|
1961
|
+
reason: isUserQuestion ? "pending_user_decision" : "pending_operator_decision",
|
|
1962
|
+
severity: "medium",
|
|
1963
|
+
stoppedSinceAt: interaction.createdAt,
|
|
1964
|
+
owner: isUserQuestion
|
|
1965
|
+
? { type: "user", agentId: null, userId: row.assigneeUserId, label: null }
|
|
1966
|
+
: { type: "operator", agentId: null, userId: null, label: "Operator" },
|
|
1967
|
+
action: {
|
|
1968
|
+
label: isUserQuestion ? "Answer question" : "Answer confirmation",
|
|
1969
|
+
detail: "Respond to the pending issue-thread interaction so the assignee has a live next action.",
|
|
1970
|
+
},
|
|
1971
|
+
sourceIssue: source,
|
|
1972
|
+
interactionId: interaction.id,
|
|
1973
|
+
}));
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
const approval = approvalByIssueId.get(row.id);
|
|
1977
|
+
if (approval) {
|
|
1978
|
+
result.set(row.id, attentionBase({
|
|
1979
|
+
state: "awaiting_decision",
|
|
1980
|
+
reason: "pending_operator_decision",
|
|
1981
|
+
severity: "medium",
|
|
1982
|
+
stoppedSinceAt: approval.createdAt,
|
|
1983
|
+
owner: { type: "operator", agentId: null, userId: null, label: "Operator" },
|
|
1984
|
+
action: {
|
|
1985
|
+
label: "Decide approval",
|
|
1986
|
+
detail: "Approve, reject, or request revision on the linked approval.",
|
|
1987
|
+
},
|
|
1988
|
+
sourceIssue: source,
|
|
1989
|
+
approvalId: approval.approvalId,
|
|
1990
|
+
}));
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
const finding = findingByIssueId.get(row.id);
|
|
1994
|
+
if (finding) {
|
|
1995
|
+
const leaf = finding.dependencyPath.length > 1
|
|
1996
|
+
? issuesById.get(finding.dependencyPath[finding.dependencyPath.length - 1].issueId)
|
|
1997
|
+
: issuesById.get(finding.recoveryIssueId);
|
|
1998
|
+
const ownerAgentId = finding.state === "blocked_by_unassigned_issue"
|
|
1999
|
+
? null
|
|
2000
|
+
: finding.recommendedOwnerAgentId ?? row.assigneeAgentId ?? leaf?.assigneeAgentId ?? null;
|
|
2001
|
+
result.set(row.id, attentionBase({
|
|
2002
|
+
state: "needs_attention",
|
|
2003
|
+
reason: finding.state,
|
|
2004
|
+
severity: finding.state === "blocked_by_assigned_backlog_issue"
|
|
2005
|
+
|| finding.state === "in_review_without_action_path"
|
|
2006
|
+
? "high"
|
|
2007
|
+
: finding.severity === "critical" ? "critical" : "high",
|
|
2008
|
+
stoppedSinceAt: leaf?.updatedAt ?? row.updatedAt,
|
|
2009
|
+
owner: {
|
|
2010
|
+
type: ownerAgentId ? "agent" : leaf?.assigneeUserId ? "user" : "unknown",
|
|
2011
|
+
agentId: ownerAgentId,
|
|
2012
|
+
userId: leaf?.assigneeUserId ?? null,
|
|
2013
|
+
label: null,
|
|
2014
|
+
},
|
|
2015
|
+
action: {
|
|
2016
|
+
label: (() => {
|
|
2017
|
+
switch (finding.state) {
|
|
2018
|
+
case "blocked_by_unassigned_issue":
|
|
2019
|
+
return "Assign blocker";
|
|
2020
|
+
case "blocked_by_assigned_backlog_issue":
|
|
2021
|
+
return "Resume parked blocker";
|
|
2022
|
+
case "blocked_by_uninvokable_assignee":
|
|
2023
|
+
return "Assign active owner";
|
|
2024
|
+
case "blocked_by_cancelled_issue":
|
|
2025
|
+
return "Replace blocker";
|
|
2026
|
+
case "invalid_review_participant":
|
|
2027
|
+
return "Repair review participant";
|
|
2028
|
+
case "in_review_without_action_path":
|
|
2029
|
+
return "Choose review path";
|
|
2030
|
+
}
|
|
2031
|
+
})(),
|
|
2032
|
+
detail: finding.recommendedAction,
|
|
2033
|
+
},
|
|
2034
|
+
sourceIssue: source,
|
|
2035
|
+
leafIssue: issueRef(leaf),
|
|
2036
|
+
recoveryIssue: issueRef(issuesById.get(finding.recoveryIssueId)),
|
|
2037
|
+
sampleIssueIdentifier: leaf?.identifier ?? finding.identifier,
|
|
2038
|
+
}));
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
const hasMonitor = Boolean(row.monitorNextCheckAt && row.monitorNextCheckAt.getTime() > Date.now());
|
|
2042
|
+
const external = row.status === "blocked" && !hasMonitor ? externalWaitFromDescription(row.description) : null;
|
|
2043
|
+
if (external) {
|
|
2044
|
+
result.set(row.id, attentionBase({
|
|
2045
|
+
state: "external_wait",
|
|
2046
|
+
reason: "external_owner_action",
|
|
2047
|
+
severity: "medium",
|
|
2048
|
+
stoppedSinceAt: row.updatedAt,
|
|
2049
|
+
owner: { type: "external", agentId: null, userId: null, label: null },
|
|
2050
|
+
action: {
|
|
2051
|
+
label: "External owner action",
|
|
2052
|
+
detail: null,
|
|
2053
|
+
},
|
|
2054
|
+
sourceIssue: source,
|
|
2055
|
+
externalDetailsRedacted: true,
|
|
2056
|
+
}));
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
const blockerAttention = await listIssueBlockerAttentionMap(dbOrTx, squadId, [row]);
|
|
2060
|
+
const blockerState = blockerAttention.get(row.id);
|
|
2061
|
+
if (row.status === "blocked" && (blockerState?.state === "needs_attention" || blockerState?.state === "stalled")) {
|
|
2062
|
+
result.set(row.id, attentionBase({
|
|
2063
|
+
state: "needs_attention",
|
|
2064
|
+
reason: "blocked_chain_stalled",
|
|
2065
|
+
severity: "high",
|
|
2066
|
+
stoppedSinceAt: row.updatedAt,
|
|
2067
|
+
owner: { type: "unknown", agentId: null, userId: null, label: null },
|
|
2068
|
+
action: {
|
|
2069
|
+
label: "Inspect blocker chain",
|
|
2070
|
+
detail: "Inspect the stalled blocker or review leaf and make the next owner/action explicit.",
|
|
2071
|
+
},
|
|
2072
|
+
sourceIssue: source,
|
|
2073
|
+
sampleIssueIdentifier: blockerState.sampleStalledBlockerIdentifier ?? blockerState.sampleBlockerIdentifier,
|
|
2074
|
+
}));
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return result;
|
|
2078
|
+
}
|
|
2079
|
+
async function blockedInboxIssueConditions(dbOrTx, squadId, filters) {
|
|
2080
|
+
const conditions = [
|
|
2081
|
+
eq(issues.squadId, squadId),
|
|
2082
|
+
isNull(issues.hiddenAt),
|
|
2083
|
+
notInArray(issues.status, [...BLOCKED_INBOX_TERMINAL_STATUSES]),
|
|
2084
|
+
];
|
|
2085
|
+
const touchedByUserId = filters?.touchedByUserId?.trim() || undefined;
|
|
2086
|
+
const inboxArchivedByUserId = filters?.inboxArchivedByUserId?.trim() || undefined;
|
|
2087
|
+
const unreadForUserId = filters?.unreadForUserId?.trim() || undefined;
|
|
2088
|
+
const contextUserId = unreadForUserId ?? touchedByUserId ?? inboxArchivedByUserId;
|
|
2089
|
+
if (filters?.descendantOf) {
|
|
2090
|
+
conditions.push(sql `
|
|
2091
|
+
${issues.id} IN (
|
|
2092
|
+
WITH RECURSIVE descendants(id) AS (
|
|
2093
|
+
SELECT ${issues.id}
|
|
2094
|
+
FROM ${issues}
|
|
2095
|
+
WHERE ${issues.squadId} = ${squadId}
|
|
2096
|
+
AND ${issues.parentId} = ${filters.descendantOf}
|
|
2097
|
+
UNION
|
|
2098
|
+
SELECT ${issues.id}
|
|
2099
|
+
FROM ${issues}
|
|
2100
|
+
JOIN descendants ON ${issues.parentId} = descendants.id
|
|
2101
|
+
WHERE ${issues.squadId} = ${squadId}
|
|
2102
|
+
)
|
|
2103
|
+
SELECT id FROM descendants
|
|
2104
|
+
)
|
|
2105
|
+
`);
|
|
2106
|
+
}
|
|
2107
|
+
if (filters?.status) {
|
|
2108
|
+
const statuses = filters.status.split(",").map((status) => status.trim()).filter(Boolean);
|
|
2109
|
+
if (statuses.length > 0) {
|
|
2110
|
+
conditions.push(statuses.length === 1 ? eq(issues.status, statuses[0]) : inArray(issues.status, statuses));
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (filters?.assigneeAgentId)
|
|
2114
|
+
conditions.push(eq(issues.assigneeAgentId, filters.assigneeAgentId));
|
|
2115
|
+
if (filters?.participantAgentId)
|
|
2116
|
+
conditions.push(participatedByAgentCondition(squadId, filters.participantAgentId));
|
|
2117
|
+
if (filters?.assigneeUserId)
|
|
2118
|
+
conditions.push(eq(issues.assigneeUserId, filters.assigneeUserId));
|
|
2119
|
+
if (touchedByUserId)
|
|
2120
|
+
conditions.push(touchedByUserCondition(squadId, touchedByUserId));
|
|
2121
|
+
if (inboxArchivedByUserId)
|
|
2122
|
+
conditions.push(inboxVisibleForUserCondition(squadId, inboxArchivedByUserId));
|
|
2123
|
+
if (unreadForUserId)
|
|
2124
|
+
conditions.push(unreadForUserCondition(squadId, unreadForUserId));
|
|
2125
|
+
if (filters?.projectId)
|
|
2126
|
+
conditions.push(eq(issues.projectId, filters.projectId));
|
|
2127
|
+
if (filters?.workspaceId) {
|
|
2128
|
+
conditions.push(or(eq(issues.executionWorkspaceId, filters.workspaceId), eq(issues.projectWorkspaceId, filters.workspaceId)));
|
|
2129
|
+
}
|
|
2130
|
+
if (filters?.executionWorkspaceId)
|
|
2131
|
+
conditions.push(eq(issues.executionWorkspaceId, filters.executionWorkspaceId));
|
|
2132
|
+
if (filters?.parentId)
|
|
2133
|
+
conditions.push(eq(issues.parentId, filters.parentId));
|
|
2134
|
+
if (filters?.originKind)
|
|
2135
|
+
conditions.push(eq(issues.originKind, filters.originKind));
|
|
2136
|
+
if (filters?.originKindPrefix)
|
|
2137
|
+
conditions.push(like(issues.originKind, `${filters.originKindPrefix}%`));
|
|
2138
|
+
if (filters?.originId)
|
|
2139
|
+
conditions.push(eq(issues.originId, filters.originId));
|
|
2140
|
+
if (!shouldIncludePluginOperationIssues(filters))
|
|
2141
|
+
conditions.push(nonPluginOperationIssueCondition());
|
|
2142
|
+
if (filters?.labelId) {
|
|
2143
|
+
const labeledIssueIds = await dbOrTx
|
|
2144
|
+
.select({ issueId: issueLabels.issueId })
|
|
2145
|
+
.from(issueLabels)
|
|
2146
|
+
.where(and(eq(issueLabels.squadId, squadId), eq(issueLabels.labelId, filters.labelId)));
|
|
2147
|
+
if (labeledIssueIds.length === 0)
|
|
2148
|
+
return { conditions: [sql `false`], contextUserId };
|
|
2149
|
+
conditions.push(inArray(issues.id, labeledIssueIds.map((row) => row.issueId)));
|
|
2150
|
+
}
|
|
2151
|
+
if (filters?.excludeRoutineExecutions && !filters?.originKind && !filters?.originId) {
|
|
2152
|
+
conditions.push(ne(issues.originKind, "routine_execution"));
|
|
2153
|
+
}
|
|
2154
|
+
return { conditions, contextUserId };
|
|
2155
|
+
}
|
|
2156
|
+
async function listBlockedInboxIssues(dbOrTx, squadId, filters) {
|
|
2157
|
+
const { conditions, contextUserId } = await blockedInboxIssueConditions(dbOrTx, squadId, filters);
|
|
2158
|
+
const rows = (await dbOrTx
|
|
2159
|
+
.select(issueListSelect)
|
|
2160
|
+
.from(issues)
|
|
2161
|
+
.where(and(...conditions))
|
|
2162
|
+
.orderBy(desc(issueCanonicalLastActivityAtExpr(squadId)), desc(issues.updatedAt), desc(issues.id)))
|
|
2163
|
+
.map((row) => ({
|
|
2164
|
+
...row,
|
|
2165
|
+
description: decodeDatabaseTextPreview(row.description, ISSUE_LIST_DESCRIPTION_MAX_CHARS),
|
|
2166
|
+
}));
|
|
2167
|
+
const withLabels = await withIssueLabels(dbOrTx, rows);
|
|
2168
|
+
const withRuns = withActiveRuns(withLabels, await activeRunMapForIssues(dbOrTx, withLabels));
|
|
2169
|
+
if (withRuns.length === 0)
|
|
2170
|
+
return [];
|
|
2171
|
+
const issueIds = withRuns.map((row) => row.id);
|
|
2172
|
+
const [statsRows, readRows, lastActivityRows, blockedByMap, blockerAttentionByIssueId, productivityReviewByIssueId, blockedInboxAttentionByIssueId,] = await Promise.all([
|
|
2173
|
+
contextUserId ? userCommentStatsForIssues(dbOrTx, squadId, contextUserId, issueIds) : Promise.resolve([]),
|
|
2174
|
+
contextUserId ? userReadStatsForIssues(dbOrTx, squadId, contextUserId, issueIds) : Promise.resolve([]),
|
|
2175
|
+
lastActivityStatsForIssues(dbOrTx, squadId, issueIds),
|
|
2176
|
+
blockedByMapForIssues(dbOrTx, squadId, issueIds),
|
|
2177
|
+
listIssueBlockerAttentionMap(dbOrTx, squadId, withRuns),
|
|
2178
|
+
listIssueProductivityReviewMap(dbOrTx, squadId, issueIds),
|
|
2179
|
+
listIssueBlockedInboxAttentionMap(dbOrTx, squadId, withRuns),
|
|
2180
|
+
]);
|
|
2181
|
+
const rawSearchInput = filters?.q?.trim() ?? "";
|
|
2182
|
+
const rawSearch = rawSearchInput.toLowerCase();
|
|
2183
|
+
const commentSearchMatchIssueIds = new Set();
|
|
2184
|
+
if (rawSearchInput) {
|
|
2185
|
+
const containsPattern = `%${escapeLikePattern(rawSearchInput)}%`;
|
|
2186
|
+
for (const issueIdChunk of chunkList(issueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
2187
|
+
const rows = await dbOrTx
|
|
2188
|
+
.select({ issueId: issueComments.issueId })
|
|
2189
|
+
.from(issueComments)
|
|
2190
|
+
.where(and(eq(issueComments.squadId, squadId), inArray(issueComments.issueId, issueIdChunk), sql `${issueComments.body} ILIKE ${containsPattern} ESCAPE '\\'`));
|
|
2191
|
+
for (const row of rows)
|
|
2192
|
+
commentSearchMatchIssueIds.add(row.issueId);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
const statsByIssueId = new Map(statsRows.map((row) => [row.issueId, row]));
|
|
2196
|
+
const readByIssueId = new Map(readRows.map((row) => [row.issueId, row.myLastReadAt]));
|
|
2197
|
+
const lastActivityByIssueId = new Map(lastActivityRows.map((row) => [row.issueId, row]));
|
|
2198
|
+
const enriched = withRuns.flatMap((row) => {
|
|
2199
|
+
const blockedInboxAttention = blockedInboxAttentionByIssueId.get(row.id);
|
|
2200
|
+
if (!blockedInboxAttention)
|
|
2201
|
+
return [];
|
|
2202
|
+
if (rawSearch
|
|
2203
|
+
&& !blockedInboxSearchText(blockedInboxAttention, row).includes(rawSearch)
|
|
2204
|
+
&& !commentSearchMatchIssueIds.has(row.id))
|
|
2205
|
+
return [];
|
|
2206
|
+
const activity = lastActivityByIssueId.get(row.id);
|
|
2207
|
+
const lastActivityAt = latestIssueActivityAt(row.updatedAt, activity?.latestCommentAt ?? null, activity?.latestLogAt ?? null) ?? row.updatedAt;
|
|
2208
|
+
return [{
|
|
2209
|
+
...row,
|
|
2210
|
+
description: blockedInboxResponseDescription(blockedInboxAttention, row),
|
|
2211
|
+
blockedBy: blockedByMap.get(row.id) ?? [],
|
|
2212
|
+
lastActivityAt,
|
|
2213
|
+
...(blockerAttentionByIssueId.has(row.id) ? { blockerAttention: blockerAttentionByIssueId.get(row.id) } : {}),
|
|
2214
|
+
blockedInboxAttention,
|
|
2215
|
+
...(productivityReviewByIssueId.has(row.id)
|
|
2216
|
+
? { productivityReview: productivityReviewByIssueId.get(row.id) }
|
|
2217
|
+
: {}),
|
|
2218
|
+
...(contextUserId
|
|
2219
|
+
? deriveIssueUserContext(row, contextUserId, {
|
|
2220
|
+
myLastCommentAt: statsByIssueId.get(row.id)?.myLastCommentAt ?? null,
|
|
2221
|
+
myLastReadAt: readByIssueId.get(row.id) ?? null,
|
|
2222
|
+
lastExternalCommentAt: statsByIssueId.get(row.id)?.lastExternalCommentAt ?? null,
|
|
2223
|
+
})
|
|
2224
|
+
: {}),
|
|
2225
|
+
}];
|
|
2226
|
+
}).sort(compareBlockedInboxRows);
|
|
2227
|
+
const offset = typeof filters?.offset === "number" && Number.isFinite(filters.offset)
|
|
2228
|
+
? Math.max(0, Math.floor(filters.offset))
|
|
2229
|
+
: 0;
|
|
2230
|
+
const limit = typeof filters?.limit === "number" && Number.isFinite(filters.limit)
|
|
2231
|
+
? Math.max(1, Math.floor(filters.limit))
|
|
2232
|
+
: undefined;
|
|
2233
|
+
return limit === undefined ? enriched.slice(offset) : enriched.slice(offset, offset + limit);
|
|
2234
|
+
}
|
|
2235
|
+
async function countBlockedInboxIssues(dbOrTx, squadId, filters) {
|
|
2236
|
+
const { conditions } = await blockedInboxIssueConditions(dbOrTx, squadId, filters);
|
|
2237
|
+
const rows = (await dbOrTx
|
|
2238
|
+
.select()
|
|
2239
|
+
.from(issues)
|
|
2240
|
+
.where(and(...conditions)));
|
|
2241
|
+
if (rows.length === 0)
|
|
2242
|
+
return 0;
|
|
2243
|
+
const blockedInboxAttentionByIssueId = await listIssueBlockedInboxAttentionMap(dbOrTx, squadId, rows);
|
|
2244
|
+
const rawSearchInput = filters?.q?.trim() ?? "";
|
|
2245
|
+
const rawSearch = rawSearchInput.toLowerCase();
|
|
2246
|
+
const commentSearchMatchIssueIds = new Set();
|
|
2247
|
+
if (rawSearchInput) {
|
|
2248
|
+
const issueIds = rows.map((row) => row.id);
|
|
2249
|
+
const containsPattern = `%${escapeLikePattern(rawSearchInput)}%`;
|
|
2250
|
+
for (const issueIdChunk of chunkList(issueIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
|
2251
|
+
const commentRows = await dbOrTx
|
|
2252
|
+
.select({ issueId: issueComments.issueId })
|
|
2253
|
+
.from(issueComments)
|
|
2254
|
+
.where(and(eq(issueComments.squadId, squadId), inArray(issueComments.issueId, issueIdChunk), sql `${issueComments.body} ILIKE ${containsPattern} ESCAPE '\\'`));
|
|
2255
|
+
for (const row of commentRows)
|
|
2256
|
+
commentSearchMatchIssueIds.add(row.issueId);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
return rows.reduce((count, row) => {
|
|
2260
|
+
const attention = blockedInboxAttentionByIssueId.get(row.id);
|
|
2261
|
+
if (!attention)
|
|
2262
|
+
return count;
|
|
2263
|
+
if (rawSearch
|
|
2264
|
+
&& !blockedInboxSearchText(attention, row).includes(rawSearch)
|
|
2265
|
+
&& !commentSearchMatchIssueIds.has(row.id))
|
|
2266
|
+
return count;
|
|
2267
|
+
return count + 1;
|
|
2268
|
+
}, 0);
|
|
2269
|
+
}
|
|
2270
|
+
export function issueService(db) {
|
|
2271
|
+
const instanceSettings = instanceSettingsService(db);
|
|
2272
|
+
const treeControlSvc = issueTreeControlService(db);
|
|
2273
|
+
async function getIssueByUuid(id) {
|
|
2274
|
+
const row = await db
|
|
2275
|
+
.select()
|
|
2276
|
+
.from(issues)
|
|
2277
|
+
.where(eq(issues.id, id))
|
|
2278
|
+
.then((rows) => rows[0] ?? null);
|
|
2279
|
+
if (!row)
|
|
2280
|
+
return null;
|
|
2281
|
+
const [enriched] = await withIssueLabels(db, [row]);
|
|
2282
|
+
return enriched;
|
|
2283
|
+
}
|
|
2284
|
+
async function getIssueByIdentifier(identifier) {
|
|
2285
|
+
const row = await db
|
|
2286
|
+
.select()
|
|
2287
|
+
.from(issues)
|
|
2288
|
+
.where(eq(issues.identifier, identifier.toUpperCase()))
|
|
2289
|
+
.then((rows) => rows[0] ?? null);
|
|
2290
|
+
if (!row)
|
|
2291
|
+
return null;
|
|
2292
|
+
const [enriched] = await withIssueLabels(db, [row]);
|
|
2293
|
+
return enriched;
|
|
2294
|
+
}
|
|
2295
|
+
async function getCurrentScheduledRetryForIssue(issueId, squadId) {
|
|
2296
|
+
const row = await db
|
|
2297
|
+
.select({
|
|
2298
|
+
runId: heartbeatRuns.id,
|
|
2299
|
+
status: heartbeatRuns.status,
|
|
2300
|
+
agentId: heartbeatRuns.agentId,
|
|
2301
|
+
agentName: agents.name,
|
|
2302
|
+
retryOfRunId: heartbeatRuns.retryOfRunId,
|
|
2303
|
+
scheduledRetryAt: heartbeatRuns.scheduledRetryAt,
|
|
2304
|
+
scheduledRetryAttempt: heartbeatRuns.scheduledRetryAttempt,
|
|
2305
|
+
scheduledRetryReason: heartbeatRuns.scheduledRetryReason,
|
|
2306
|
+
error: heartbeatRuns.error,
|
|
2307
|
+
errorCode: heartbeatRuns.errorCode,
|
|
2308
|
+
})
|
|
2309
|
+
.from(heartbeatRuns)
|
|
2310
|
+
.innerJoin(agents, eq(heartbeatRuns.agentId, agents.id))
|
|
2311
|
+
.where(and(eq(heartbeatRuns.squadId, squadId), eq(heartbeatRuns.status, "scheduled_retry"), sql `${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}`))
|
|
2312
|
+
.orderBy(asc(heartbeatRuns.scheduledRetryAt), asc(heartbeatRuns.createdAt), asc(heartbeatRuns.id))
|
|
2313
|
+
.limit(1)
|
|
2314
|
+
.then((rows) => rows[0] ?? null);
|
|
2315
|
+
return row ? { ...row, status: "scheduled_retry" } : null;
|
|
2316
|
+
}
|
|
2317
|
+
function deriveIssueCommentAuthorType(comment) {
|
|
2318
|
+
const explicit = issueCommentAuthorTypeSchema.safeParse(comment.authorType);
|
|
2319
|
+
if (explicit.success)
|
|
2320
|
+
return explicit.data;
|
|
2321
|
+
if (comment.authorAgentId)
|
|
2322
|
+
return "agent";
|
|
2323
|
+
if (comment.authorUserId)
|
|
2324
|
+
return "user";
|
|
2325
|
+
return "system";
|
|
2326
|
+
}
|
|
2327
|
+
function assertIssueCommentAuthorTypeAllowed(actor, authorType) {
|
|
2328
|
+
if (actor.agentId && authorType !== "agent") {
|
|
2329
|
+
throw unprocessable("Comment authorType must match authenticated actor");
|
|
2330
|
+
}
|
|
2331
|
+
if (actor.userId && authorType !== "user") {
|
|
2332
|
+
throw unprocessable("Comment authorType must match authenticated actor");
|
|
2333
|
+
}
|
|
2334
|
+
if (!actor.agentId && !actor.userId && authorType !== "system") {
|
|
2335
|
+
throw unprocessable("System comments cannot use user or agent authorType without an author id");
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function redactIssueComment(comment, censorUsernameInLogs) {
|
|
2339
|
+
return {
|
|
2340
|
+
...comment,
|
|
2341
|
+
authorType: deriveIssueCommentAuthorType(comment),
|
|
2342
|
+
body: redactCurrentUserText(comment.body, { enabled: censorUsernameInLogs }),
|
|
2343
|
+
presentation: issueCommentPresentationSchema.nullable().catch(null).parse(comment.presentation ?? null),
|
|
2344
|
+
metadata: issueCommentMetadataSchema.nullable().catch(null).parse(comment.metadata ?? null),
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
async function readRunLogText(run) {
|
|
2348
|
+
if (run.logStore !== "local_file" || !run.logRef)
|
|
2349
|
+
return "";
|
|
2350
|
+
const logBytes = Number(run.logBytes ?? 0);
|
|
2351
|
+
if (!Number.isFinite(logBytes) || logBytes <= 0)
|
|
2352
|
+
return "";
|
|
2353
|
+
const store = getRunLogStore();
|
|
2354
|
+
let offset = 0;
|
|
2355
|
+
let content = "";
|
|
2356
|
+
let nextOffset = 0;
|
|
2357
|
+
try {
|
|
2358
|
+
while (nextOffset !== undefined) {
|
|
2359
|
+
const remainingBytes = ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_LOG_BYTES - Buffer.byteLength(content, "utf8");
|
|
2360
|
+
if (remainingBytes <= 0)
|
|
2361
|
+
break;
|
|
2362
|
+
const chunk = await store.read({ store: "local_file", logRef: run.logRef }, {
|
|
2363
|
+
offset,
|
|
2364
|
+
limitBytes: Math.min(ISSUE_COMMENT_RUN_LOG_DERIVATION_CHUNK_BYTES, remainingBytes),
|
|
2365
|
+
});
|
|
2366
|
+
content += chunk.content;
|
|
2367
|
+
nextOffset = chunk.nextOffset;
|
|
2368
|
+
offset = chunk.nextOffset ?? 0;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
catch (err) {
|
|
2372
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
2373
|
+
logger.warn({ err, runId: run.runId ?? undefined, logRef: run.logRef }, "missing heartbeat run log while deriving issue comment metadata");
|
|
2374
|
+
return content;
|
|
2375
|
+
}
|
|
2376
|
+
throw err;
|
|
2377
|
+
}
|
|
2378
|
+
return content;
|
|
2379
|
+
}
|
|
2380
|
+
async function enrichCommentsWithDerivedAgentAttribution(comments) {
|
|
2381
|
+
const candidates = comments.filter((comment) => !comment.authorAgentId
|
|
2382
|
+
&& !!comment.authorUserId
|
|
2383
|
+
&& !comment.createdByRunId);
|
|
2384
|
+
if (candidates.length === 0)
|
|
2385
|
+
return comments;
|
|
2386
|
+
const squadId = comments[0]?.squadId ?? null;
|
|
2387
|
+
const issueId = comments[0]?.issueId ?? null;
|
|
2388
|
+
if (!squadId || !issueId)
|
|
2389
|
+
return comments;
|
|
2390
|
+
const minCommentCreatedAtMs = candidates.reduce((min, comment) => {
|
|
2391
|
+
const timestamp = toTimestampMs(comment.createdAt);
|
|
2392
|
+
if (timestamp === null)
|
|
2393
|
+
return min;
|
|
2394
|
+
return min === null ? timestamp : Math.min(min, timestamp);
|
|
2395
|
+
}, null);
|
|
2396
|
+
const maxCommentCreatedAtMs = candidates.reduce((max, comment) => {
|
|
2397
|
+
const timestamp = toTimestampMs(comment.createdAt);
|
|
2398
|
+
if (timestamp === null)
|
|
2399
|
+
return max;
|
|
2400
|
+
return max === null ? timestamp : Math.max(max, timestamp);
|
|
2401
|
+
}, null);
|
|
2402
|
+
if (minCommentCreatedAtMs === null || maxCommentCreatedAtMs === null)
|
|
2403
|
+
return comments;
|
|
2404
|
+
const minCommentCreatedAt = new Date(minCommentCreatedAtMs).toISOString();
|
|
2405
|
+
const maxCommentCreatedAt = new Date(maxCommentCreatedAtMs + ISSUE_COMMENT_RUN_LOG_DERIVATION_END_SLACK_MS).toISOString();
|
|
2406
|
+
const runs = await db
|
|
2407
|
+
.select({
|
|
2408
|
+
runId: heartbeatRuns.id,
|
|
2409
|
+
agentId: heartbeatRuns.agentId,
|
|
2410
|
+
createdAt: heartbeatRuns.createdAt,
|
|
2411
|
+
startedAt: heartbeatRuns.startedAt,
|
|
2412
|
+
finishedAt: heartbeatRuns.finishedAt,
|
|
2413
|
+
logStore: heartbeatRuns.logStore,
|
|
2414
|
+
logRef: heartbeatRuns.logRef,
|
|
2415
|
+
logBytes: heartbeatRuns.logBytes,
|
|
2416
|
+
})
|
|
2417
|
+
.from(heartbeatRuns)
|
|
2418
|
+
.where(and(eq(heartbeatRuns.squadId, squadId), or(sql `${heartbeatRuns.contextSnapshot} ->> 'issueId' = ${issueId}`, sql `exists (
|
|
2419
|
+
select 1
|
|
2420
|
+
from ${activityLog}
|
|
2421
|
+
where ${activityLog.squadId} = ${squadId}
|
|
2422
|
+
and ${activityLog.entityType} = 'issue'
|
|
2423
|
+
and ${activityLog.entityId} = ${issueId}
|
|
2424
|
+
and ${activityLog.runId} = ${heartbeatRuns.id}
|
|
2425
|
+
)`), sql `coalesce(${heartbeatRuns.finishedAt}, ${heartbeatRuns.createdAt}) >= ${minCommentCreatedAt}::timestamptz`, sql `coalesce(${heartbeatRuns.startedAt}, ${heartbeatRuns.createdAt}) <= ${maxCommentCreatedAt}::timestamptz`))
|
|
2426
|
+
.orderBy(desc(heartbeatRuns.createdAt));
|
|
2427
|
+
if (runs.length === 0)
|
|
2428
|
+
return comments;
|
|
2429
|
+
const runsWithLogs = [];
|
|
2430
|
+
for (let index = 0; index < runs.length; index += ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_PARALLEL_READS) {
|
|
2431
|
+
const batch = runs.slice(index, index + ISSUE_COMMENT_RUN_LOG_DERIVATION_MAX_PARALLEL_READS);
|
|
2432
|
+
const batchWithLogs = await Promise.all(batch.map(async (run) => ({
|
|
2433
|
+
...run,
|
|
2434
|
+
logContent: await readRunLogText(run),
|
|
2435
|
+
})));
|
|
2436
|
+
runsWithLogs.push(...batchWithLogs);
|
|
2437
|
+
}
|
|
2438
|
+
const derivedByCommentId = deriveIssueCommentRunLogAttribution(candidates, runsWithLogs);
|
|
2439
|
+
if (derivedByCommentId.size === 0)
|
|
2440
|
+
return comments;
|
|
2441
|
+
return comments.map((comment) => {
|
|
2442
|
+
const derived = derivedByCommentId.get(comment.id);
|
|
2443
|
+
return derived ? { ...comment, ...derived } : comment;
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
async function assertAssignableAgent(squadId, agentId) {
|
|
2447
|
+
const assignee = await db
|
|
2448
|
+
.select({
|
|
2449
|
+
id: agents.id,
|
|
2450
|
+
squadId: agents.squadId,
|
|
2451
|
+
status: agents.status,
|
|
2452
|
+
})
|
|
2453
|
+
.from(agents)
|
|
2454
|
+
.where(eq(agents.id, agentId))
|
|
2455
|
+
.then((rows) => rows[0] ?? null);
|
|
2456
|
+
if (!assignee)
|
|
2457
|
+
throw notFound("Assignee agent not found");
|
|
2458
|
+
if (assignee.squadId !== squadId) {
|
|
2459
|
+
throw unprocessable("Assignee must belong to same squad");
|
|
2460
|
+
}
|
|
2461
|
+
if (assignee.status === "pending_approval") {
|
|
2462
|
+
throw conflict("Cannot assign work to pending approval agents");
|
|
2463
|
+
}
|
|
2464
|
+
if (assignee.status === "terminated") {
|
|
2465
|
+
throw conflict("Cannot assign work to terminated agents");
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
async function isTreeHoldInteractionCheckoutAllowed(squadId, checkoutRunId, _gate) {
|
|
2469
|
+
if (!checkoutRunId)
|
|
2470
|
+
return false;
|
|
2471
|
+
const run = await db
|
|
2472
|
+
.select({
|
|
2473
|
+
id: heartbeatRuns.id,
|
|
2474
|
+
agentId: heartbeatRuns.agentId,
|
|
2475
|
+
wakeupRequestId: heartbeatRuns.wakeupRequestId,
|
|
2476
|
+
contextSnapshot: heartbeatRuns.contextSnapshot,
|
|
2477
|
+
})
|
|
2478
|
+
.from(heartbeatRuns)
|
|
2479
|
+
.where(and(eq(heartbeatRuns.id, checkoutRunId), eq(heartbeatRuns.squadId, squadId)))
|
|
2480
|
+
.then((rows) => rows[0] ?? null);
|
|
2481
|
+
const issueId = readStringFromRecord(run?.contextSnapshot, "issueId");
|
|
2482
|
+
if (!run || !issueId)
|
|
2483
|
+
return false;
|
|
2484
|
+
return isVerifiedIssueTreeControlInteractionWake(db, {
|
|
2485
|
+
squadId,
|
|
2486
|
+
issueId,
|
|
2487
|
+
agentId: run.agentId,
|
|
2488
|
+
runId: run.id,
|
|
2489
|
+
wakeupRequestId: run.wakeupRequestId,
|
|
2490
|
+
contextSnapshot: run.contextSnapshot,
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
async function assertAssignableUser(squadId, userId) {
|
|
2494
|
+
const membership = await db
|
|
2495
|
+
.select({ id: squadMemberships.id })
|
|
2496
|
+
.from(squadMemberships)
|
|
2497
|
+
.where(and(eq(squadMemberships.squadId, squadId), eq(squadMemberships.principalType, "user"), eq(squadMemberships.principalId, userId), eq(squadMemberships.status, "active")))
|
|
2498
|
+
.then((rows) => rows[0] ?? null);
|
|
2499
|
+
if (!membership) {
|
|
2500
|
+
throw notFound("Assignee user not found");
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
async function assertValidProjectWorkspace(squadId, projectId, projectWorkspaceId, dbOrTx = db) {
|
|
2504
|
+
const workspace = await dbOrTx
|
|
2505
|
+
.select({
|
|
2506
|
+
id: projectWorkspaces.id,
|
|
2507
|
+
squadId: projectWorkspaces.squadId,
|
|
2508
|
+
projectId: projectWorkspaces.projectId,
|
|
2509
|
+
})
|
|
2510
|
+
.from(projectWorkspaces)
|
|
2511
|
+
.where(eq(projectWorkspaces.id, projectWorkspaceId))
|
|
2512
|
+
.then((rows) => rows[0] ?? null);
|
|
2513
|
+
if (!workspace)
|
|
2514
|
+
throw notFound("Project workspace not found");
|
|
2515
|
+
if (workspace.squadId !== squadId)
|
|
2516
|
+
throw unprocessable("Project workspace must belong to same squad");
|
|
2517
|
+
if (projectId && workspace.projectId !== projectId) {
|
|
2518
|
+
throw unprocessable("Project workspace must belong to the selected project");
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
async function assertValidExecutionWorkspace(squadId, projectId, executionWorkspaceId, dbOrTx = db) {
|
|
2522
|
+
const workspace = await dbOrTx
|
|
2523
|
+
.select({
|
|
2524
|
+
id: executionWorkspaces.id,
|
|
2525
|
+
squadId: executionWorkspaces.squadId,
|
|
2526
|
+
projectId: executionWorkspaces.projectId,
|
|
2527
|
+
})
|
|
2528
|
+
.from(executionWorkspaces)
|
|
2529
|
+
.where(eq(executionWorkspaces.id, executionWorkspaceId))
|
|
2530
|
+
.then((rows) => rows[0] ?? null);
|
|
2531
|
+
if (!workspace)
|
|
2532
|
+
throw notFound("Execution workspace not found");
|
|
2533
|
+
if (workspace.squadId !== squadId)
|
|
2534
|
+
throw unprocessable("Execution workspace must belong to same squad");
|
|
2535
|
+
if (projectId && workspace.projectId !== projectId) {
|
|
2536
|
+
throw unprocessable("Execution workspace must belong to the selected project");
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
async function assertValidLabelIds(squadId, labelIds, dbOrTx = db) {
|
|
2540
|
+
if (labelIds.length === 0)
|
|
2541
|
+
return;
|
|
2542
|
+
const existing = await dbOrTx
|
|
2543
|
+
.select({ id: labels.id })
|
|
2544
|
+
.from(labels)
|
|
2545
|
+
.where(and(eq(labels.squadId, squadId), inArray(labels.id, labelIds)));
|
|
2546
|
+
if (existing.length !== new Set(labelIds).size) {
|
|
2547
|
+
throw unprocessable("One or more labels are invalid for this squad");
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
async function syncIssueLabels(issueId, squadId, labelIds, dbOrTx = db) {
|
|
2551
|
+
const deduped = [...new Set(labelIds)];
|
|
2552
|
+
await assertValidLabelIds(squadId, deduped, dbOrTx);
|
|
2553
|
+
await dbOrTx.delete(issueLabels).where(eq(issueLabels.issueId, issueId));
|
|
2554
|
+
if (deduped.length === 0)
|
|
2555
|
+
return;
|
|
2556
|
+
await dbOrTx.insert(issueLabels).values(deduped.map((labelId) => ({
|
|
2557
|
+
issueId,
|
|
2558
|
+
labelId,
|
|
2559
|
+
squadId,
|
|
2560
|
+
})));
|
|
2561
|
+
}
|
|
2562
|
+
async function getIssueRelationSummaryMap(squadId, issueIds, dbOrTx = db) {
|
|
2563
|
+
const uniqueIssueIds = [...new Set(issueIds)];
|
|
2564
|
+
const empty = new Map();
|
|
2565
|
+
for (const issueId of uniqueIssueIds) {
|
|
2566
|
+
empty.set(issueId, { blockedBy: [], blocks: [] });
|
|
2567
|
+
}
|
|
2568
|
+
if (uniqueIssueIds.length === 0)
|
|
2569
|
+
return empty;
|
|
2570
|
+
const [blockedByRows, blockingRows] = await Promise.all([
|
|
2571
|
+
dbOrTx
|
|
2572
|
+
.select({
|
|
2573
|
+
currentIssueId: issueRelations.relatedIssueId,
|
|
2574
|
+
relatedId: issues.id,
|
|
2575
|
+
identifier: issues.identifier,
|
|
2576
|
+
title: issues.title,
|
|
2577
|
+
status: issues.status,
|
|
2578
|
+
priority: issues.priority,
|
|
2579
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
2580
|
+
assigneeUserId: issues.assigneeUserId,
|
|
2581
|
+
})
|
|
2582
|
+
.from(issueRelations)
|
|
2583
|
+
.innerJoin(issues, eq(issueRelations.issueId, issues.id))
|
|
2584
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.type, "blocks"), inArray(issueRelations.relatedIssueId, uniqueIssueIds))),
|
|
2585
|
+
dbOrTx
|
|
2586
|
+
.select({
|
|
2587
|
+
currentIssueId: issueRelations.issueId,
|
|
2588
|
+
relatedId: issues.id,
|
|
2589
|
+
identifier: issues.identifier,
|
|
2590
|
+
title: issues.title,
|
|
2591
|
+
status: issues.status,
|
|
2592
|
+
priority: issues.priority,
|
|
2593
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
2594
|
+
assigneeUserId: issues.assigneeUserId,
|
|
2595
|
+
})
|
|
2596
|
+
.from(issueRelations)
|
|
2597
|
+
.innerJoin(issues, eq(issueRelations.relatedIssueId, issues.id))
|
|
2598
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.type, "blocks"), inArray(issueRelations.issueId, uniqueIssueIds))),
|
|
2599
|
+
]);
|
|
2600
|
+
for (const row of blockedByRows) {
|
|
2601
|
+
empty.get(row.currentIssueId)?.blockedBy.push(summarizeIssueRelationRow(row));
|
|
2602
|
+
}
|
|
2603
|
+
for (const row of blockingRows) {
|
|
2604
|
+
empty.get(row.currentIssueId)?.blocks.push(summarizeIssueRelationRow(row));
|
|
2605
|
+
}
|
|
2606
|
+
const terminalByRoot = await terminalExplicitBlockersByRoot(squadId, [...empty.values()].flatMap((relations) => relations.blockedBy), dbOrTx);
|
|
2607
|
+
for (const relations of empty.values()) {
|
|
2608
|
+
relations.blockedBy.sort((a, b) => a.title.localeCompare(b.title));
|
|
2609
|
+
for (const blocker of relations.blockedBy) {
|
|
2610
|
+
const terminalBlockers = terminalByRoot.get(blocker.id);
|
|
2611
|
+
if (terminalBlockers && terminalBlockers.length > 0) {
|
|
2612
|
+
blocker.terminalBlockers = terminalBlockers;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
relations.blocks.sort((a, b) => a.title.localeCompare(b.title));
|
|
2616
|
+
}
|
|
2617
|
+
return empty;
|
|
2618
|
+
}
|
|
2619
|
+
async function assertNoBlockingCycles(squadId, issueId, blockerIssueIds, dbOrTx = db) {
|
|
2620
|
+
if (blockerIssueIds.length === 0)
|
|
2621
|
+
return;
|
|
2622
|
+
const rows = await dbOrTx
|
|
2623
|
+
.select({
|
|
2624
|
+
blockerIssueId: issueRelations.issueId,
|
|
2625
|
+
blockedIssueId: issueRelations.relatedIssueId,
|
|
2626
|
+
})
|
|
2627
|
+
.from(issueRelations)
|
|
2628
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.type, "blocks")));
|
|
2629
|
+
const adjacency = new Map();
|
|
2630
|
+
for (const row of rows) {
|
|
2631
|
+
const list = adjacency.get(row.blockerIssueId) ?? [];
|
|
2632
|
+
list.push(row.blockedIssueId);
|
|
2633
|
+
adjacency.set(row.blockerIssueId, list);
|
|
2634
|
+
}
|
|
2635
|
+
for (const blockerIssueId of blockerIssueIds) {
|
|
2636
|
+
const queue = [...(adjacency.get(issueId) ?? [])];
|
|
2637
|
+
const visited = new Set([issueId]);
|
|
2638
|
+
while (queue.length > 0) {
|
|
2639
|
+
const current = queue.shift();
|
|
2640
|
+
if (current === blockerIssueId) {
|
|
2641
|
+
throw unprocessable("Blocking relations cannot contain cycles");
|
|
2642
|
+
}
|
|
2643
|
+
if (visited.has(current))
|
|
2644
|
+
continue;
|
|
2645
|
+
visited.add(current);
|
|
2646
|
+
queue.push(...(adjacency.get(current) ?? []));
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
async function syncBlockedByIssueIds(issueId, squadId, blockedByIssueIds, actor = {}, dbOrTx = db) {
|
|
2651
|
+
const deduped = [...new Set(blockedByIssueIds)];
|
|
2652
|
+
if (deduped.some((candidate) => candidate === issueId)) {
|
|
2653
|
+
throw unprocessable("Issue cannot be blocked by itself");
|
|
2654
|
+
}
|
|
2655
|
+
if (deduped.length > 0) {
|
|
2656
|
+
const lockedIssueIds = [issueId, ...deduped].sort();
|
|
2657
|
+
await dbOrTx.execute(sql `SELECT ${issues.id} FROM ${issues}
|
|
2658
|
+
WHERE ${and(eq(issues.squadId, squadId), inArray(issues.id, lockedIssueIds))}
|
|
2659
|
+
ORDER BY ${issues.id}
|
|
2660
|
+
FOR UPDATE`);
|
|
2661
|
+
const relatedIssues = await dbOrTx
|
|
2662
|
+
.select({ id: issues.id })
|
|
2663
|
+
.from(issues)
|
|
2664
|
+
.where(and(eq(issues.squadId, squadId), inArray(issues.id, deduped)));
|
|
2665
|
+
if (relatedIssues.length !== deduped.length) {
|
|
2666
|
+
throw unprocessable("Blocked-by issues must belong to the same squad");
|
|
2667
|
+
}
|
|
2668
|
+
await assertNoBlockingCycles(squadId, issueId, deduped, dbOrTx);
|
|
2669
|
+
}
|
|
2670
|
+
await dbOrTx
|
|
2671
|
+
.delete(issueRelations)
|
|
2672
|
+
.where(and(eq(issueRelations.squadId, squadId), eq(issueRelations.relatedIssueId, issueId), eq(issueRelations.type, "blocks")));
|
|
2673
|
+
if (deduped.length === 0)
|
|
2674
|
+
return;
|
|
2675
|
+
await dbOrTx.insert(issueRelations).values(deduped.map((blockerIssueId) => ({
|
|
2676
|
+
squadId,
|
|
2677
|
+
issueId: blockerIssueId,
|
|
2678
|
+
relatedIssueId: issueId,
|
|
2679
|
+
type: "blocks",
|
|
2680
|
+
createdByAgentId: actor.agentId ?? null,
|
|
2681
|
+
createdByUserId: actor.userId ?? null,
|
|
2682
|
+
})));
|
|
2683
|
+
}
|
|
2684
|
+
async function isTerminalOrMissingHeartbeatRun(runId) {
|
|
2685
|
+
const run = await db
|
|
2686
|
+
.select({ status: heartbeatRuns.status })
|
|
2687
|
+
.from(heartbeatRuns)
|
|
2688
|
+
.where(eq(heartbeatRuns.id, runId))
|
|
2689
|
+
.then((rows) => rows[0] ?? null);
|
|
2690
|
+
if (!run)
|
|
2691
|
+
return true;
|
|
2692
|
+
return TERMINAL_HEARTBEAT_RUN_STATUSES.has(run.status);
|
|
2693
|
+
}
|
|
2694
|
+
async function adoptStaleCheckoutRun(input) {
|
|
2695
|
+
const stale = await isTerminalOrMissingHeartbeatRun(input.expectedCheckoutRunId);
|
|
2696
|
+
if (!stale)
|
|
2697
|
+
return null;
|
|
2698
|
+
const now = new Date();
|
|
2699
|
+
const adopted = await db
|
|
2700
|
+
.update(issues)
|
|
2701
|
+
.set({
|
|
2702
|
+
checkoutRunId: input.actorRunId,
|
|
2703
|
+
executionRunId: input.actorRunId,
|
|
2704
|
+
executionLockedAt: now,
|
|
2705
|
+
updatedAt: now,
|
|
2706
|
+
})
|
|
2707
|
+
.where(and(eq(issues.id, input.issueId), eq(issues.status, "in_progress"), eq(issues.assigneeAgentId, input.actorAgentId), eq(issues.checkoutRunId, input.expectedCheckoutRunId)))
|
|
2708
|
+
.returning({
|
|
2709
|
+
id: issues.id,
|
|
2710
|
+
status: issues.status,
|
|
2711
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
2712
|
+
checkoutRunId: issues.checkoutRunId,
|
|
2713
|
+
executionRunId: issues.executionRunId,
|
|
2714
|
+
})
|
|
2715
|
+
.then((rows) => rows[0] ?? null);
|
|
2716
|
+
return adopted;
|
|
2717
|
+
}
|
|
2718
|
+
async function adoptUnownedCheckoutRun(input) {
|
|
2719
|
+
const now = new Date();
|
|
2720
|
+
const adopted = await db
|
|
2721
|
+
.update(issues)
|
|
2722
|
+
.set({
|
|
2723
|
+
checkoutRunId: input.actorRunId,
|
|
2724
|
+
executionRunId: input.actorRunId,
|
|
2725
|
+
executionLockedAt: now,
|
|
2726
|
+
updatedAt: now,
|
|
2727
|
+
})
|
|
2728
|
+
.where(and(eq(issues.id, input.issueId), eq(issues.status, "in_progress"), eq(issues.assigneeAgentId, input.actorAgentId), isNull(issues.checkoutRunId), or(isNull(issues.executionRunId), eq(issues.executionRunId, input.actorRunId))))
|
|
2729
|
+
.returning({
|
|
2730
|
+
id: issues.id,
|
|
2731
|
+
status: issues.status,
|
|
2732
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
2733
|
+
checkoutRunId: issues.checkoutRunId,
|
|
2734
|
+
executionRunId: issues.executionRunId,
|
|
2735
|
+
})
|
|
2736
|
+
.then((rows) => rows[0] ?? null);
|
|
2737
|
+
return adopted;
|
|
2738
|
+
}
|
|
2739
|
+
async function clearExecutionRunIfTerminal(issueId) {
|
|
2740
|
+
return db.transaction(async (tx) => {
|
|
2741
|
+
await tx.execute(sql `select ${issues.id} from ${issues} where ${issues.id} = ${issueId} for update`);
|
|
2742
|
+
const issue = await tx
|
|
2743
|
+
.select({ executionRunId: issues.executionRunId })
|
|
2744
|
+
.from(issues)
|
|
2745
|
+
.where(eq(issues.id, issueId))
|
|
2746
|
+
.then((rows) => rows[0] ?? null);
|
|
2747
|
+
if (!issue?.executionRunId)
|
|
2748
|
+
return false;
|
|
2749
|
+
await tx.execute(sql `select ${heartbeatRuns.id} from ${heartbeatRuns} where ${heartbeatRuns.id} = ${issue.executionRunId} for update`);
|
|
2750
|
+
const run = await tx
|
|
2751
|
+
.select({ status: heartbeatRuns.status })
|
|
2752
|
+
.from(heartbeatRuns)
|
|
2753
|
+
.where(eq(heartbeatRuns.id, issue.executionRunId))
|
|
2754
|
+
.then((rows) => rows[0] ?? null);
|
|
2755
|
+
if (run && !TERMINAL_HEARTBEAT_RUN_STATUSES.has(run.status))
|
|
2756
|
+
return false;
|
|
2757
|
+
const updated = await tx
|
|
2758
|
+
.update(issues)
|
|
2759
|
+
.set({
|
|
2760
|
+
executionRunId: null,
|
|
2761
|
+
executionAgentNameKey: null,
|
|
2762
|
+
executionLockedAt: null,
|
|
2763
|
+
updatedAt: new Date(),
|
|
2764
|
+
})
|
|
2765
|
+
.where(and(eq(issues.id, issueId), eq(issues.executionRunId, issue.executionRunId)))
|
|
2766
|
+
.returning({ id: issues.id })
|
|
2767
|
+
.then((rows) => rows[0] ?? null);
|
|
2768
|
+
return Boolean(updated);
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
return {
|
|
2772
|
+
clearExecutionRunIfTerminal,
|
|
2773
|
+
list: async (squadId, filters) => {
|
|
2774
|
+
if (filters?.attention === "blocked") {
|
|
2775
|
+
return listBlockedInboxIssues(db, squadId, {
|
|
2776
|
+
...filters,
|
|
2777
|
+
includeBlockedBy: true,
|
|
2778
|
+
includeBlockedInboxAttention: true,
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
const conditions = [eq(issues.squadId, squadId)];
|
|
2782
|
+
const limit = typeof filters?.limit === "number" && Number.isFinite(filters.limit)
|
|
2783
|
+
? Math.max(1, Math.floor(filters.limit))
|
|
2784
|
+
: undefined;
|
|
2785
|
+
const offset = typeof filters?.offset === "number" && Number.isFinite(filters.offset)
|
|
2786
|
+
? Math.max(0, Math.floor(filters.offset))
|
|
2787
|
+
: 0;
|
|
2788
|
+
const touchedByUserId = filters?.touchedByUserId?.trim() || undefined;
|
|
2789
|
+
const inboxArchivedByUserId = filters?.inboxArchivedByUserId?.trim() || undefined;
|
|
2790
|
+
const unreadForUserId = filters?.unreadForUserId?.trim() || undefined;
|
|
2791
|
+
const contextUserId = unreadForUserId ?? touchedByUserId ?? inboxArchivedByUserId;
|
|
2792
|
+
const includeBlockedBy = filters?.includeBlockedBy === true;
|
|
2793
|
+
const includeBlockedInboxAttention = filters?.includeBlockedInboxAttention === true;
|
|
2794
|
+
const rawSearch = filters?.q?.trim() ?? "";
|
|
2795
|
+
const hasSearch = rawSearch.length > 0;
|
|
2796
|
+
const escapedSearch = hasSearch ? escapeLikePattern(rawSearch) : "";
|
|
2797
|
+
const startsWithPattern = `${escapedSearch}%`;
|
|
2798
|
+
const containsPattern = `%${escapedSearch}%`;
|
|
2799
|
+
const titleStartsWithMatch = sql `${issues.title} ILIKE ${startsWithPattern} ESCAPE '\\'`;
|
|
2800
|
+
const titleContainsMatch = sql `${issues.title} ILIKE ${containsPattern} ESCAPE '\\'`;
|
|
2801
|
+
const identifierStartsWithMatch = sql `${issues.identifier} ILIKE ${startsWithPattern} ESCAPE '\\'`;
|
|
2802
|
+
const identifierContainsMatch = sql `${issues.identifier} ILIKE ${containsPattern} ESCAPE '\\'`;
|
|
2803
|
+
const descriptionContainsMatch = sql `${issues.description} ILIKE ${containsPattern} ESCAPE '\\'`;
|
|
2804
|
+
const commentContainsMatch = sql `
|
|
2805
|
+
EXISTS (
|
|
2806
|
+
SELECT 1
|
|
2807
|
+
FROM ${issueComments}
|
|
2808
|
+
WHERE ${issueComments.issueId} = ${issues.id}
|
|
2809
|
+
AND ${issueComments.squadId} = ${squadId}
|
|
2810
|
+
AND ${issueComments.body} ILIKE ${containsPattern} ESCAPE '\\'
|
|
2811
|
+
)
|
|
2812
|
+
`;
|
|
2813
|
+
if (filters?.descendantOf) {
|
|
2814
|
+
conditions.push(sql `
|
|
2815
|
+
${issues.id} IN (
|
|
2816
|
+
WITH RECURSIVE descendants(id) AS (
|
|
2817
|
+
SELECT ${issues.id}
|
|
2818
|
+
FROM ${issues}
|
|
2819
|
+
WHERE ${issues.squadId} = ${squadId}
|
|
2820
|
+
AND ${issues.parentId} = ${filters.descendantOf}
|
|
2821
|
+
UNION
|
|
2822
|
+
SELECT ${issues.id}
|
|
2823
|
+
FROM ${issues}
|
|
2824
|
+
JOIN descendants ON ${issues.parentId} = descendants.id
|
|
2825
|
+
WHERE ${issues.squadId} = ${squadId}
|
|
2826
|
+
)
|
|
2827
|
+
SELECT id FROM descendants
|
|
2828
|
+
)
|
|
2829
|
+
`);
|
|
2830
|
+
}
|
|
2831
|
+
if (filters?.status) {
|
|
2832
|
+
const statuses = filters.status.split(",").map((s) => s.trim());
|
|
2833
|
+
conditions.push(statuses.length === 1 ? eq(issues.status, statuses[0]) : inArray(issues.status, statuses));
|
|
2834
|
+
}
|
|
2835
|
+
if (filters?.assigneeAgentId) {
|
|
2836
|
+
conditions.push(eq(issues.assigneeAgentId, filters.assigneeAgentId));
|
|
2837
|
+
}
|
|
2838
|
+
if (filters?.participantAgentId) {
|
|
2839
|
+
conditions.push(participatedByAgentCondition(squadId, filters.participantAgentId));
|
|
2840
|
+
}
|
|
2841
|
+
if (filters?.assigneeUserId) {
|
|
2842
|
+
conditions.push(eq(issues.assigneeUserId, filters.assigneeUserId));
|
|
2843
|
+
}
|
|
2844
|
+
if (touchedByUserId) {
|
|
2845
|
+
conditions.push(touchedByUserCondition(squadId, touchedByUserId));
|
|
2846
|
+
}
|
|
2847
|
+
if (inboxArchivedByUserId) {
|
|
2848
|
+
conditions.push(inboxVisibleForUserCondition(squadId, inboxArchivedByUserId));
|
|
2849
|
+
}
|
|
2850
|
+
if (unreadForUserId) {
|
|
2851
|
+
conditions.push(unreadForUserCondition(squadId, unreadForUserId));
|
|
2852
|
+
}
|
|
2853
|
+
if (filters?.projectId)
|
|
2854
|
+
conditions.push(eq(issues.projectId, filters.projectId));
|
|
2855
|
+
if (filters?.workspaceId) {
|
|
2856
|
+
conditions.push(or(eq(issues.executionWorkspaceId, filters.workspaceId), eq(issues.projectWorkspaceId, filters.workspaceId)));
|
|
2857
|
+
}
|
|
2858
|
+
if (filters?.executionWorkspaceId) {
|
|
2859
|
+
conditions.push(eq(issues.executionWorkspaceId, filters.executionWorkspaceId));
|
|
2860
|
+
}
|
|
2861
|
+
if (filters?.parentId)
|
|
2862
|
+
conditions.push(eq(issues.parentId, filters.parentId));
|
|
2863
|
+
if (filters?.originKind)
|
|
2864
|
+
conditions.push(eq(issues.originKind, filters.originKind));
|
|
2865
|
+
if (filters?.originKindPrefix)
|
|
2866
|
+
conditions.push(like(issues.originKind, `${filters.originKindPrefix}%`));
|
|
2867
|
+
if (filters?.originId)
|
|
2868
|
+
conditions.push(eq(issues.originId, filters.originId));
|
|
2869
|
+
if (!shouldIncludePluginOperationIssues(filters)) {
|
|
2870
|
+
conditions.push(nonPluginOperationIssueCondition());
|
|
2871
|
+
}
|
|
2872
|
+
if (filters?.labelId) {
|
|
2873
|
+
const labeledIssueIds = await db
|
|
2874
|
+
.select({ issueId: issueLabels.issueId })
|
|
2875
|
+
.from(issueLabels)
|
|
2876
|
+
.where(and(eq(issueLabels.squadId, squadId), eq(issueLabels.labelId, filters.labelId)));
|
|
2877
|
+
if (labeledIssueIds.length === 0)
|
|
2878
|
+
return [];
|
|
2879
|
+
conditions.push(inArray(issues.id, labeledIssueIds.map((row) => row.issueId)));
|
|
2880
|
+
}
|
|
2881
|
+
if (hasSearch) {
|
|
2882
|
+
conditions.push(or(titleContainsMatch, identifierContainsMatch, descriptionContainsMatch, commentContainsMatch));
|
|
2883
|
+
}
|
|
2884
|
+
if (filters?.excludeRoutineExecutions && !filters?.originKind && !filters?.originId) {
|
|
2885
|
+
conditions.push(ne(issues.originKind, "routine_execution"));
|
|
2886
|
+
}
|
|
2887
|
+
conditions.push(isNull(issues.hiddenAt));
|
|
2888
|
+
const priorityOrder = sql `CASE ${issues.priority} WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END`;
|
|
2889
|
+
const searchOrder = sql `
|
|
2890
|
+
CASE
|
|
2891
|
+
WHEN ${titleStartsWithMatch} THEN 0
|
|
2892
|
+
WHEN ${titleContainsMatch} THEN 1
|
|
2893
|
+
WHEN ${identifierStartsWithMatch} THEN 2
|
|
2894
|
+
WHEN ${identifierContainsMatch} THEN 3
|
|
2895
|
+
WHEN ${commentContainsMatch} THEN 4
|
|
2896
|
+
WHEN ${descriptionContainsMatch} THEN 5
|
|
2897
|
+
ELSE 6
|
|
2898
|
+
END
|
|
2899
|
+
`;
|
|
2900
|
+
const baseQuery = db
|
|
2901
|
+
.select(issueListSelect)
|
|
2902
|
+
.from(issues)
|
|
2903
|
+
.where(and(...conditions))
|
|
2904
|
+
.orderBy(...issueListOrderBy(squadId, {
|
|
2905
|
+
hasSearch,
|
|
2906
|
+
priorityOrder,
|
|
2907
|
+
searchOrder,
|
|
2908
|
+
sortField: filters?.sortField,
|
|
2909
|
+
sortDir: filters?.sortDir,
|
|
2910
|
+
}));
|
|
2911
|
+
const pageQuery = offset > 0
|
|
2912
|
+
? (limit === undefined ? baseQuery.offset(offset) : baseQuery.limit(limit).offset(offset))
|
|
2913
|
+
: (limit === undefined ? baseQuery : baseQuery.limit(limit));
|
|
2914
|
+
const rows = (await pageQuery).map((row) => ({
|
|
2915
|
+
...row,
|
|
2916
|
+
description: decodeDatabaseTextPreview(row.description, ISSUE_LIST_DESCRIPTION_MAX_CHARS),
|
|
2917
|
+
}));
|
|
2918
|
+
const withLabels = await withIssueLabels(db, rows);
|
|
2919
|
+
const runMap = await activeRunMapForIssues(db, withLabels);
|
|
2920
|
+
const withRuns = withActiveRuns(withLabels, runMap);
|
|
2921
|
+
if (withRuns.length === 0) {
|
|
2922
|
+
return withRuns;
|
|
2923
|
+
}
|
|
2924
|
+
const issueIds = withRuns.map((row) => row.id);
|
|
2925
|
+
const [statsRows, readRows, lastActivityRows, blockedByMap] = await Promise.all([
|
|
2926
|
+
contextUserId
|
|
2927
|
+
? userCommentStatsForIssues(db, squadId, contextUserId, issueIds)
|
|
2928
|
+
: Promise.resolve([]),
|
|
2929
|
+
contextUserId
|
|
2930
|
+
? userReadStatsForIssues(db, squadId, contextUserId, issueIds)
|
|
2931
|
+
: Promise.resolve([]),
|
|
2932
|
+
lastActivityStatsForIssues(db, squadId, issueIds),
|
|
2933
|
+
includeBlockedBy
|
|
2934
|
+
? blockedByMapForIssues(db, squadId, issueIds)
|
|
2935
|
+
: Promise.resolve(new Map()),
|
|
2936
|
+
]);
|
|
2937
|
+
const statsByIssueId = new Map(statsRows.map((row) => [row.issueId, row]));
|
|
2938
|
+
const lastActivityByIssueId = new Map(lastActivityRows.map((row) => [row.issueId, row]));
|
|
2939
|
+
const [blockerAttentionByIssueId, productivityReviewByIssueId, blockedInboxAttentionByIssueId,] = await Promise.all([
|
|
2940
|
+
listIssueBlockerAttentionMap(db, squadId, withRuns),
|
|
2941
|
+
listIssueProductivityReviewMap(db, squadId, issueIds),
|
|
2942
|
+
includeBlockedInboxAttention
|
|
2943
|
+
? listIssueBlockedInboxAttentionMap(db, squadId, withRuns)
|
|
2944
|
+
: Promise.resolve(new Map()),
|
|
2945
|
+
]);
|
|
2946
|
+
if (!contextUserId) {
|
|
2947
|
+
return withRuns.map((row) => {
|
|
2948
|
+
const activity = lastActivityByIssueId.get(row.id);
|
|
2949
|
+
const lastActivityAt = latestIssueActivityAt(row.updatedAt, activity?.latestCommentAt ?? null, activity?.latestLogAt ?? null) ?? row.updatedAt;
|
|
2950
|
+
return {
|
|
2951
|
+
...row,
|
|
2952
|
+
...(includeBlockedBy ? { blockedBy: blockedByMap.get(row.id) ?? [] } : {}),
|
|
2953
|
+
lastActivityAt,
|
|
2954
|
+
...(blockerAttentionByIssueId.has(row.id) ? { blockerAttention: blockerAttentionByIssueId.get(row.id) } : {}),
|
|
2955
|
+
...(includeBlockedInboxAttention ? { blockedInboxAttention: blockedInboxAttentionByIssueId.get(row.id) ?? null } : {}),
|
|
2956
|
+
...(productivityReviewByIssueId.has(row.id)
|
|
2957
|
+
? { productivityReview: productivityReviewByIssueId.get(row.id) }
|
|
2958
|
+
: {}),
|
|
2959
|
+
};
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
const readByIssueId = new Map(readRows.map((row) => [row.issueId, row.myLastReadAt]));
|
|
2963
|
+
return withRuns.map((row) => {
|
|
2964
|
+
const activity = lastActivityByIssueId.get(row.id);
|
|
2965
|
+
const lastActivityAt = latestIssueActivityAt(row.updatedAt, activity?.latestCommentAt ?? null, activity?.latestLogAt ?? null) ?? row.updatedAt;
|
|
2966
|
+
return {
|
|
2967
|
+
...row,
|
|
2968
|
+
...(includeBlockedBy ? { blockedBy: blockedByMap.get(row.id) ?? [] } : {}),
|
|
2969
|
+
lastActivityAt,
|
|
2970
|
+
...(blockerAttentionByIssueId.has(row.id) ? { blockerAttention: blockerAttentionByIssueId.get(row.id) } : {}),
|
|
2971
|
+
...(includeBlockedInboxAttention ? { blockedInboxAttention: blockedInboxAttentionByIssueId.get(row.id) ?? null } : {}),
|
|
2972
|
+
...(productivityReviewByIssueId.has(row.id)
|
|
2973
|
+
? { productivityReview: productivityReviewByIssueId.get(row.id) }
|
|
2974
|
+
: {}),
|
|
2975
|
+
...deriveIssueUserContext(row, contextUserId, {
|
|
2976
|
+
myLastCommentAt: statsByIssueId.get(row.id)?.myLastCommentAt ?? null,
|
|
2977
|
+
myLastReadAt: readByIssueId.get(row.id) ?? null,
|
|
2978
|
+
lastExternalCommentAt: statsByIssueId.get(row.id)?.lastExternalCommentAt ?? null,
|
|
2979
|
+
}),
|
|
2980
|
+
};
|
|
2981
|
+
});
|
|
2982
|
+
},
|
|
2983
|
+
count: async (squadId, filters) => {
|
|
2984
|
+
if (filters?.attention === "blocked") {
|
|
2985
|
+
return countBlockedInboxIssues(db, squadId, filters);
|
|
2986
|
+
}
|
|
2987
|
+
const conditions = [eq(issues.squadId, squadId), isNull(issues.hiddenAt)];
|
|
2988
|
+
if (filters?.status) {
|
|
2989
|
+
const statuses = filters.status.split(",").map((status) => status.trim()).filter(Boolean);
|
|
2990
|
+
if (statuses.length === 1)
|
|
2991
|
+
conditions.push(eq(issues.status, statuses[0]));
|
|
2992
|
+
else if (statuses.length > 1)
|
|
2993
|
+
conditions.push(inArray(issues.status, statuses));
|
|
2994
|
+
}
|
|
2995
|
+
if (filters?.assigneeAgentId)
|
|
2996
|
+
conditions.push(eq(issues.assigneeAgentId, filters.assigneeAgentId));
|
|
2997
|
+
if (filters?.assigneeUserId)
|
|
2998
|
+
conditions.push(eq(issues.assigneeUserId, filters.assigneeUserId));
|
|
2999
|
+
if (filters?.projectId)
|
|
3000
|
+
conditions.push(eq(issues.projectId, filters.projectId));
|
|
3001
|
+
if (filters?.workspaceId) {
|
|
3002
|
+
conditions.push(or(eq(issues.executionWorkspaceId, filters.workspaceId), eq(issues.projectWorkspaceId, filters.workspaceId)));
|
|
3003
|
+
}
|
|
3004
|
+
if (filters?.executionWorkspaceId)
|
|
3005
|
+
conditions.push(eq(issues.executionWorkspaceId, filters.executionWorkspaceId));
|
|
3006
|
+
if (filters?.parentId)
|
|
3007
|
+
conditions.push(eq(issues.parentId, filters.parentId));
|
|
3008
|
+
if (filters?.originKind)
|
|
3009
|
+
conditions.push(eq(issues.originKind, filters.originKind));
|
|
3010
|
+
if (filters?.originKindPrefix)
|
|
3011
|
+
conditions.push(like(issues.originKind, `${filters.originKindPrefix}%`));
|
|
3012
|
+
if (filters?.originId)
|
|
3013
|
+
conditions.push(eq(issues.originId, filters.originId));
|
|
3014
|
+
if (!shouldIncludePluginOperationIssues(filters))
|
|
3015
|
+
conditions.push(nonPluginOperationIssueCondition());
|
|
3016
|
+
const [row] = await db
|
|
3017
|
+
.select({ count: sql `count(*)` })
|
|
3018
|
+
.from(issues)
|
|
3019
|
+
.where(and(...conditions));
|
|
3020
|
+
return Number(row?.count ?? 0);
|
|
3021
|
+
},
|
|
3022
|
+
countUnreadTouchedByUser: async (squadId, userId, status) => {
|
|
3023
|
+
const conditions = [
|
|
3024
|
+
eq(issues.squadId, squadId),
|
|
3025
|
+
isNull(issues.hiddenAt),
|
|
3026
|
+
nonPluginOperationIssueCondition(),
|
|
3027
|
+
unreadForUserCondition(squadId, userId),
|
|
3028
|
+
];
|
|
3029
|
+
if (status) {
|
|
3030
|
+
const statuses = status.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3031
|
+
if (statuses.length === 1) {
|
|
3032
|
+
conditions.push(eq(issues.status, statuses[0]));
|
|
3033
|
+
}
|
|
3034
|
+
else if (statuses.length > 1) {
|
|
3035
|
+
conditions.push(inArray(issues.status, statuses));
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
const [row] = await db
|
|
3039
|
+
.select({ count: sql `count(*)` })
|
|
3040
|
+
.from(issues)
|
|
3041
|
+
.where(and(...conditions));
|
|
3042
|
+
return Number(row?.count ?? 0);
|
|
3043
|
+
},
|
|
3044
|
+
markRead: async (squadId, issueId, userId, readAt = new Date()) => {
|
|
3045
|
+
const now = new Date();
|
|
3046
|
+
const [row] = await db
|
|
3047
|
+
.insert(issueReadStates)
|
|
3048
|
+
.values({
|
|
3049
|
+
squadId,
|
|
3050
|
+
issueId,
|
|
3051
|
+
userId,
|
|
3052
|
+
lastReadAt: readAt,
|
|
3053
|
+
updatedAt: now,
|
|
3054
|
+
})
|
|
3055
|
+
.onConflictDoUpdate({
|
|
3056
|
+
target: [issueReadStates.squadId, issueReadStates.issueId, issueReadStates.userId],
|
|
3057
|
+
set: {
|
|
3058
|
+
lastReadAt: readAt,
|
|
3059
|
+
updatedAt: now,
|
|
3060
|
+
},
|
|
3061
|
+
})
|
|
3062
|
+
.returning();
|
|
3063
|
+
return row;
|
|
3064
|
+
},
|
|
3065
|
+
markUnread: async (squadId, issueId, userId) => {
|
|
3066
|
+
const deleted = await db
|
|
3067
|
+
.delete(issueReadStates)
|
|
3068
|
+
.where(and(eq(issueReadStates.squadId, squadId), eq(issueReadStates.issueId, issueId), eq(issueReadStates.userId, userId)))
|
|
3069
|
+
.returning();
|
|
3070
|
+
return deleted.length > 0;
|
|
3071
|
+
},
|
|
3072
|
+
archiveInbox: async (squadId, issueId, userId, archivedAt = new Date()) => {
|
|
3073
|
+
const now = new Date();
|
|
3074
|
+
const [row] = await db
|
|
3075
|
+
.insert(issueInboxArchives)
|
|
3076
|
+
.values({
|
|
3077
|
+
squadId,
|
|
3078
|
+
issueId,
|
|
3079
|
+
userId,
|
|
3080
|
+
archivedAt,
|
|
3081
|
+
updatedAt: now,
|
|
3082
|
+
})
|
|
3083
|
+
.onConflictDoUpdate({
|
|
3084
|
+
target: [issueInboxArchives.squadId, issueInboxArchives.issueId, issueInboxArchives.userId],
|
|
3085
|
+
set: {
|
|
3086
|
+
archivedAt,
|
|
3087
|
+
updatedAt: now,
|
|
3088
|
+
},
|
|
3089
|
+
})
|
|
3090
|
+
.returning();
|
|
3091
|
+
return row;
|
|
3092
|
+
},
|
|
3093
|
+
unarchiveInbox: async (squadId, issueId, userId) => {
|
|
3094
|
+
const [row] = await db
|
|
3095
|
+
.delete(issueInboxArchives)
|
|
3096
|
+
.where(and(eq(issueInboxArchives.squadId, squadId), eq(issueInboxArchives.issueId, issueId), eq(issueInboxArchives.userId, userId)))
|
|
3097
|
+
.returning();
|
|
3098
|
+
return row ?? null;
|
|
3099
|
+
},
|
|
3100
|
+
getById: async (raw) => {
|
|
3101
|
+
const id = raw.trim();
|
|
3102
|
+
const identifier = normalizeIssueReferenceIdentifier(id);
|
|
3103
|
+
if (identifier) {
|
|
3104
|
+
return getIssueByIdentifier(identifier);
|
|
3105
|
+
}
|
|
3106
|
+
if (!isUuidLike(id)) {
|
|
3107
|
+
return null;
|
|
3108
|
+
}
|
|
3109
|
+
return getIssueByUuid(id);
|
|
3110
|
+
},
|
|
3111
|
+
getByIdentifier: async (identifier) => {
|
|
3112
|
+
return getIssueByIdentifier(identifier);
|
|
3113
|
+
},
|
|
3114
|
+
getCurrentScheduledRetry: async (issueId) => {
|
|
3115
|
+
const issue = await db
|
|
3116
|
+
.select({ id: issues.id, squadId: issues.squadId })
|
|
3117
|
+
.from(issues)
|
|
3118
|
+
.where(eq(issues.id, issueId))
|
|
3119
|
+
.then((rows) => rows[0] ?? null);
|
|
3120
|
+
if (!issue)
|
|
3121
|
+
throw notFound("Issue not found");
|
|
3122
|
+
return getCurrentScheduledRetryForIssue(issue.id, issue.squadId);
|
|
3123
|
+
},
|
|
3124
|
+
getRelationSummaries: async (issueId) => {
|
|
3125
|
+
const issue = await db
|
|
3126
|
+
.select({ id: issues.id, squadId: issues.squadId })
|
|
3127
|
+
.from(issues)
|
|
3128
|
+
.where(eq(issues.id, issueId))
|
|
3129
|
+
.then((rows) => rows[0] ?? null);
|
|
3130
|
+
if (!issue)
|
|
3131
|
+
throw notFound("Issue not found");
|
|
3132
|
+
const relations = await getIssueRelationSummaryMap(issue.squadId, [issueId], db);
|
|
3133
|
+
return relations.get(issueId) ?? { blockedBy: [], blocks: [] };
|
|
3134
|
+
},
|
|
3135
|
+
getDependencyReadiness: async (issueId, dbOrTx = db) => {
|
|
3136
|
+
const issue = await dbOrTx
|
|
3137
|
+
.select({ id: issues.id, squadId: issues.squadId })
|
|
3138
|
+
.from(issues)
|
|
3139
|
+
.where(eq(issues.id, issueId))
|
|
3140
|
+
.then((rows) => rows[0] ?? null);
|
|
3141
|
+
if (!issue)
|
|
3142
|
+
throw notFound("Issue not found");
|
|
3143
|
+
const readiness = await listIssueDependencyReadinessMap(dbOrTx, issue.squadId, [issueId]);
|
|
3144
|
+
return readiness.get(issueId) ?? createIssueDependencyReadiness(issueId);
|
|
3145
|
+
},
|
|
3146
|
+
listDependencyReadiness: async (squadId, issueIds, dbOrTx = db) => {
|
|
3147
|
+
return listIssueDependencyReadinessMap(dbOrTx, squadId, issueIds);
|
|
3148
|
+
},
|
|
3149
|
+
listBlockerAttention: async (squadId, issueRows, dbOrTx = db) => {
|
|
3150
|
+
return listIssueBlockerAttentionMap(dbOrTx, squadId, issueRows);
|
|
3151
|
+
},
|
|
3152
|
+
listProductivityReviews: async (squadId, sourceIssueIds, dbOrTx = db) => {
|
|
3153
|
+
return listIssueProductivityReviewMap(dbOrTx, squadId, sourceIssueIds);
|
|
3154
|
+
},
|
|
3155
|
+
listWakeableBlockedDependents: async (blockerIssueId) => {
|
|
3156
|
+
const blockerIssue = await db
|
|
3157
|
+
.select({ id: issues.id, squadId: issues.squadId })
|
|
3158
|
+
.from(issues)
|
|
3159
|
+
.where(eq(issues.id, blockerIssueId))
|
|
3160
|
+
.then((rows) => rows[0] ?? null);
|
|
3161
|
+
if (!blockerIssue)
|
|
3162
|
+
return [];
|
|
3163
|
+
const candidates = await db
|
|
3164
|
+
.select({
|
|
3165
|
+
id: issues.id,
|
|
3166
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
3167
|
+
status: issues.status,
|
|
3168
|
+
})
|
|
3169
|
+
.from(issueRelations)
|
|
3170
|
+
.innerJoin(issues, eq(issueRelations.relatedIssueId, issues.id))
|
|
3171
|
+
.where(and(eq(issueRelations.squadId, blockerIssue.squadId), eq(issueRelations.type, "blocks"), eq(issueRelations.issueId, blockerIssueId)));
|
|
3172
|
+
if (candidates.length === 0)
|
|
3173
|
+
return [];
|
|
3174
|
+
const wakeableCandidates = candidates.filter((candidate) => candidate.assigneeAgentId && !["backlog", "done", "cancelled"].includes(candidate.status));
|
|
3175
|
+
if (wakeableCandidates.length === 0)
|
|
3176
|
+
return [];
|
|
3177
|
+
// Defer to the unified readiness check so that a dependent only fires when
|
|
3178
|
+
// (a) every blocker is done AND (b) every done blocker's workspace has
|
|
3179
|
+
// recorded a successful workspace_finalize. The finalize hook also calls
|
|
3180
|
+
// this function on completion, so a wake initially gated by an in-flight
|
|
3181
|
+
// sync-back will re-fire once the restore lands locally.
|
|
3182
|
+
const readinessMap = await listIssueDependencyReadinessMap(db, blockerIssue.squadId, wakeableCandidates.map((candidate) => candidate.id));
|
|
3183
|
+
return wakeableCandidates
|
|
3184
|
+
.map((candidate) => {
|
|
3185
|
+
const readiness = readinessMap.get(candidate.id) ?? createIssueDependencyReadiness(candidate.id);
|
|
3186
|
+
return { candidate, readiness };
|
|
3187
|
+
})
|
|
3188
|
+
.filter(({ readiness }) => readiness.isDependencyReady && readiness.blockerIssueIds.length > 0)
|
|
3189
|
+
.map(({ candidate, readiness }) => ({
|
|
3190
|
+
id: candidate.id,
|
|
3191
|
+
assigneeAgentId: candidate.assigneeAgentId,
|
|
3192
|
+
blockerIssueIds: readiness.blockerIssueIds,
|
|
3193
|
+
}));
|
|
3194
|
+
},
|
|
3195
|
+
getWakeableParentAfterChildCompletion: async (parentIssueId) => {
|
|
3196
|
+
const parent = await db
|
|
3197
|
+
.select({
|
|
3198
|
+
id: issues.id,
|
|
3199
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
3200
|
+
status: issues.status,
|
|
3201
|
+
squadId: issues.squadId,
|
|
3202
|
+
})
|
|
3203
|
+
.from(issues)
|
|
3204
|
+
.where(eq(issues.id, parentIssueId))
|
|
3205
|
+
.then((rows) => rows[0] ?? null);
|
|
3206
|
+
if (!parent || !parent.assigneeAgentId || ["backlog", "done", "cancelled"].includes(parent.status)) {
|
|
3207
|
+
return null;
|
|
3208
|
+
}
|
|
3209
|
+
const children = await db
|
|
3210
|
+
.select({
|
|
3211
|
+
id: issues.id,
|
|
3212
|
+
identifier: issues.identifier,
|
|
3213
|
+
title: issues.title,
|
|
3214
|
+
status: issues.status,
|
|
3215
|
+
priority: issues.priority,
|
|
3216
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
3217
|
+
assigneeUserId: issues.assigneeUserId,
|
|
3218
|
+
updatedAt: issues.updatedAt,
|
|
3219
|
+
})
|
|
3220
|
+
.from(issues)
|
|
3221
|
+
.where(and(eq(issues.squadId, parent.squadId), eq(issues.parentId, parentIssueId)))
|
|
3222
|
+
.orderBy(asc(issues.issueNumber), asc(issues.createdAt));
|
|
3223
|
+
if (children.length === 0)
|
|
3224
|
+
return null;
|
|
3225
|
+
if (!children.every((child) => child.status === "done" || child.status === "cancelled")) {
|
|
3226
|
+
return null;
|
|
3227
|
+
}
|
|
3228
|
+
const childIdsForSummaries = children.slice(0, MAX_CHILD_COMPLETION_SUMMARIES).map((child) => child.id);
|
|
3229
|
+
const commentRows = childIdsForSummaries.length > 0
|
|
3230
|
+
? await db
|
|
3231
|
+
.select({
|
|
3232
|
+
issueId: issueComments.issueId,
|
|
3233
|
+
body: issueComments.body,
|
|
3234
|
+
createdAt: issueComments.createdAt,
|
|
3235
|
+
})
|
|
3236
|
+
.from(issueComments)
|
|
3237
|
+
.where(and(eq(issueComments.squadId, parent.squadId), inArray(issueComments.issueId, childIdsForSummaries)))
|
|
3238
|
+
.orderBy(desc(issueComments.createdAt), desc(issueComments.id))
|
|
3239
|
+
: [];
|
|
3240
|
+
const latestCommentByIssueId = new Map();
|
|
3241
|
+
for (const comment of commentRows) {
|
|
3242
|
+
if (!latestCommentByIssueId.has(comment.issueId)) {
|
|
3243
|
+
latestCommentByIssueId.set(comment.issueId, comment.body);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
const childIssueSummaries = children
|
|
3247
|
+
.slice(0, MAX_CHILD_COMPLETION_SUMMARIES)
|
|
3248
|
+
.map((child) => ({
|
|
3249
|
+
...child,
|
|
3250
|
+
summary: truncateInlineSummary(latestCommentByIssueId.get(child.id)),
|
|
3251
|
+
}));
|
|
3252
|
+
return {
|
|
3253
|
+
id: parent.id,
|
|
3254
|
+
assigneeAgentId: parent.assigneeAgentId,
|
|
3255
|
+
childIssueIds: children.map((child) => child.id),
|
|
3256
|
+
childIssueSummaries,
|
|
3257
|
+
childIssueSummaryTruncated: children.length > childIssueSummaries.length,
|
|
3258
|
+
};
|
|
3259
|
+
},
|
|
3260
|
+
createChild: async (parentIssueId, data) => {
|
|
3261
|
+
const parent = await db
|
|
3262
|
+
.select()
|
|
3263
|
+
.from(issues)
|
|
3264
|
+
.where(eq(issues.id, parentIssueId))
|
|
3265
|
+
.then((rows) => rows[0] ?? null);
|
|
3266
|
+
if (!parent)
|
|
3267
|
+
throw notFound("Parent issue not found");
|
|
3268
|
+
const [{ childCount }] = await db
|
|
3269
|
+
.select({ childCount: sql `count(*)::int` })
|
|
3270
|
+
.from(issues)
|
|
3271
|
+
.where(and(eq(issues.squadId, parent.squadId), eq(issues.parentId, parent.id)));
|
|
3272
|
+
if (childCount >= MAX_CHILD_ISSUES_CREATED_BY_HELPER) {
|
|
3273
|
+
throw unprocessable(`Parent issue already has the maximum ${MAX_CHILD_ISSUES_CREATED_BY_HELPER} child issues for this helper`);
|
|
3274
|
+
}
|
|
3275
|
+
const { acceptanceCriteria, blockParentUntilDone, actorAgentId, actorUserId, ...issueData } = data;
|
|
3276
|
+
const child = await issueService(db).create(parent.squadId, {
|
|
3277
|
+
...issueData,
|
|
3278
|
+
parentId: parent.id,
|
|
3279
|
+
projectId: issueData.projectId ?? parent.projectId,
|
|
3280
|
+
goalId: issueData.goalId ?? parent.goalId,
|
|
3281
|
+
requestDepth: clampIssueRequestDepth(Math.max(clampIssueRequestDepth(parent.requestDepth) + 1, issueData.requestDepth ?? 0)),
|
|
3282
|
+
description: appendAcceptanceCriteriaToDescription(issueData.description, acceptanceCriteria),
|
|
3283
|
+
inheritExecutionWorkspaceFromIssueId: parent.id,
|
|
3284
|
+
});
|
|
3285
|
+
if (blockParentUntilDone) {
|
|
3286
|
+
const existingBlockers = await db
|
|
3287
|
+
.select({ blockerIssueId: issueRelations.issueId })
|
|
3288
|
+
.from(issueRelations)
|
|
3289
|
+
.where(and(eq(issueRelations.squadId, parent.squadId), eq(issueRelations.relatedIssueId, parent.id), eq(issueRelations.type, "blocks")));
|
|
3290
|
+
await syncBlockedByIssueIds(parent.id, parent.squadId, [...new Set([...existingBlockers.map((row) => row.blockerIssueId), child.id])], { agentId: actorAgentId ?? null, userId: actorUserId ?? null });
|
|
3291
|
+
}
|
|
3292
|
+
return {
|
|
3293
|
+
issue: child,
|
|
3294
|
+
parentBlockerAdded: Boolean(blockParentUntilDone),
|
|
3295
|
+
};
|
|
3296
|
+
},
|
|
3297
|
+
decomposeAcceptedPlan: async (sourceIssueId, data) => {
|
|
3298
|
+
const sourceIssue = await db
|
|
3299
|
+
.select({
|
|
3300
|
+
id: issues.id,
|
|
3301
|
+
squadId: issues.squadId,
|
|
3302
|
+
projectId: issues.projectId,
|
|
3303
|
+
goalId: issues.goalId,
|
|
3304
|
+
})
|
|
3305
|
+
.from(issues)
|
|
3306
|
+
.where(eq(issues.id, sourceIssueId))
|
|
3307
|
+
.then((rows) => rows[0] ?? null);
|
|
3308
|
+
if (!sourceIssue)
|
|
3309
|
+
throw notFound("Source issue not found");
|
|
3310
|
+
const requestFingerprint = createAcceptedPlanDecompositionRequestFingerprint({
|
|
3311
|
+
acceptedPlanRevisionId: data.acceptedPlanRevisionId,
|
|
3312
|
+
children: data.children,
|
|
3313
|
+
});
|
|
3314
|
+
const initialClaim = await db.transaction(async (tx) => {
|
|
3315
|
+
await tx.execute(sql `select ${issues.id} from ${issues} where ${issues.id} = ${sourceIssue.id} for update`);
|
|
3316
|
+
const belongsToPlanDocument = await tx
|
|
3317
|
+
.select({ revisionId: documentRevisions.id })
|
|
3318
|
+
.from(issueDocuments)
|
|
3319
|
+
.innerJoin(documentRevisions, eq(issueDocuments.documentId, documentRevisions.documentId))
|
|
3320
|
+
.where(and(eq(issueDocuments.squadId, sourceIssue.squadId), eq(issueDocuments.issueId, sourceIssue.id), eq(issueDocuments.key, "plan"), eq(documentRevisions.id, data.acceptedPlanRevisionId)))
|
|
3321
|
+
.then((rows) => rows[0] ?? null);
|
|
3322
|
+
if (!belongsToPlanDocument) {
|
|
3323
|
+
throw unprocessable("acceptedPlanRevisionId must belong to the source issue's plan document");
|
|
3324
|
+
}
|
|
3325
|
+
const acceptedInteraction = await findAcceptedPlanDocumentInteraction(tx, {
|
|
3326
|
+
squadId: sourceIssue.squadId,
|
|
3327
|
+
sourceIssueId: sourceIssue.id,
|
|
3328
|
+
acceptedPlanRevisionId: data.acceptedPlanRevisionId,
|
|
3329
|
+
});
|
|
3330
|
+
if (!acceptedInteraction) {
|
|
3331
|
+
throw unprocessable("acceptedPlanRevisionId must have an accepted plan confirmation");
|
|
3332
|
+
}
|
|
3333
|
+
const existing = await tx
|
|
3334
|
+
.select()
|
|
3335
|
+
.from(issuePlanDecompositions)
|
|
3336
|
+
.where(and(eq(issuePlanDecompositions.squadId, sourceIssue.squadId), eq(issuePlanDecompositions.sourceIssueId, sourceIssue.id), eq(issuePlanDecompositions.acceptedPlanRevisionId, data.acceptedPlanRevisionId)))
|
|
3337
|
+
.then((rows) => rows[0] ?? null);
|
|
3338
|
+
const now = new Date();
|
|
3339
|
+
if (!existing) {
|
|
3340
|
+
const [created] = await tx
|
|
3341
|
+
.insert(issuePlanDecompositions)
|
|
3342
|
+
.values({
|
|
3343
|
+
squadId: sourceIssue.squadId,
|
|
3344
|
+
sourceIssueId: sourceIssue.id,
|
|
3345
|
+
acceptedPlanRevisionId: data.acceptedPlanRevisionId,
|
|
3346
|
+
acceptedInteractionId: acceptedInteraction.id,
|
|
3347
|
+
status: "in_flight",
|
|
3348
|
+
requestFingerprint,
|
|
3349
|
+
requestedChildCount: data.children.length,
|
|
3350
|
+
requestedChildren: data.children,
|
|
3351
|
+
childIssueIds: [],
|
|
3352
|
+
ownerAgentId: data.actorAgentId ?? null,
|
|
3353
|
+
ownerUserId: data.actorUserId ?? null,
|
|
3354
|
+
ownerRunId: data.actorRunId ?? null,
|
|
3355
|
+
updatedAt: now,
|
|
3356
|
+
})
|
|
3357
|
+
.returning();
|
|
3358
|
+
if (!created)
|
|
3359
|
+
throw new Error("Failed to create accepted-plan decomposition claim");
|
|
3360
|
+
return created;
|
|
3361
|
+
}
|
|
3362
|
+
if (existing.requestFingerprint !== requestFingerprint) {
|
|
3363
|
+
throw conflict("Accepted-plan decomposition already exists for this revision with a different child set");
|
|
3364
|
+
}
|
|
3365
|
+
return existing;
|
|
3366
|
+
});
|
|
3367
|
+
let currentClaim = initialClaim;
|
|
3368
|
+
const newlyCreatedIssues = [];
|
|
3369
|
+
while (true) {
|
|
3370
|
+
const step = await db.transaction(async (tx) => {
|
|
3371
|
+
await tx.execute(sql `select ${issuePlanDecompositions.id}
|
|
3372
|
+
from ${issuePlanDecompositions}
|
|
3373
|
+
where ${issuePlanDecompositions.id} = ${currentClaim.id}
|
|
3374
|
+
for update`);
|
|
3375
|
+
const claim = await tx
|
|
3376
|
+
.select()
|
|
3377
|
+
.from(issuePlanDecompositions)
|
|
3378
|
+
.where(eq(issuePlanDecompositions.id, currentClaim.id))
|
|
3379
|
+
.then((rows) => rows[0] ?? null);
|
|
3380
|
+
if (!claim)
|
|
3381
|
+
throw notFound("Accepted-plan decomposition claim not found");
|
|
3382
|
+
if (claim.requestFingerprint !== requestFingerprint) {
|
|
3383
|
+
throw conflict("Accepted-plan decomposition already exists for this revision with a different child set");
|
|
3384
|
+
}
|
|
3385
|
+
const existingChildIssueIds = normalizeIssuePlanDecompositionChildIds(claim.childIssueIds);
|
|
3386
|
+
if (claim.status === "completed" || existingChildIssueIds.length >= data.children.length) {
|
|
3387
|
+
const nextIds = existingChildIssueIds.slice(0, data.children.length);
|
|
3388
|
+
if (claim.status === "completed" && nextIds.length === data.children.length) {
|
|
3389
|
+
return {
|
|
3390
|
+
claim,
|
|
3391
|
+
createdIssue: null,
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
const completedAt = claim.completedAt ?? new Date();
|
|
3395
|
+
const ownerPatch = await resolveAcceptedPlanClaimOwner({
|
|
3396
|
+
dbOrTx: tx,
|
|
3397
|
+
claim,
|
|
3398
|
+
actorAgentId: data.actorAgentId,
|
|
3399
|
+
actorUserId: data.actorUserId,
|
|
3400
|
+
actorRunId: data.actorRunId,
|
|
3401
|
+
});
|
|
3402
|
+
const [completed] = await tx
|
|
3403
|
+
.update(issuePlanDecompositions)
|
|
3404
|
+
.set({
|
|
3405
|
+
status: "completed",
|
|
3406
|
+
childIssueIds: nextIds,
|
|
3407
|
+
completedAt,
|
|
3408
|
+
...ownerPatch,
|
|
3409
|
+
updatedAt: completedAt,
|
|
3410
|
+
})
|
|
3411
|
+
.where(eq(issuePlanDecompositions.id, claim.id))
|
|
3412
|
+
.returning();
|
|
3413
|
+
if (!completed)
|
|
3414
|
+
throw new Error("Failed to complete accepted-plan decomposition claim");
|
|
3415
|
+
return {
|
|
3416
|
+
claim: completed,
|
|
3417
|
+
createdIssue: null,
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
const nextChildInput = data.children[existingChildIssueIds.length];
|
|
3421
|
+
if (!nextChildInput) {
|
|
3422
|
+
throw new Error("Accepted-plan decomposition child cursor moved past the requested children");
|
|
3423
|
+
}
|
|
3424
|
+
const createdChild = await issueService(tx).createChild(sourceIssue.id, nextChildInput);
|
|
3425
|
+
const nextIds = [...existingChildIssueIds, createdChild.issue.id];
|
|
3426
|
+
const now = new Date();
|
|
3427
|
+
const nextStatus = nextIds.length === data.children.length ? "completed" : "in_flight";
|
|
3428
|
+
const ownerPatch = await resolveAcceptedPlanClaimOwner({
|
|
3429
|
+
dbOrTx: tx,
|
|
3430
|
+
claim,
|
|
3431
|
+
actorAgentId: data.actorAgentId,
|
|
3432
|
+
actorUserId: data.actorUserId,
|
|
3433
|
+
actorRunId: data.actorRunId,
|
|
3434
|
+
});
|
|
3435
|
+
const [updatedClaim] = await tx
|
|
3436
|
+
.update(issuePlanDecompositions)
|
|
3437
|
+
.set({
|
|
3438
|
+
status: nextStatus,
|
|
3439
|
+
childIssueIds: nextIds,
|
|
3440
|
+
completedAt: nextStatus === "completed" ? now : null,
|
|
3441
|
+
...ownerPatch,
|
|
3442
|
+
updatedAt: now,
|
|
3443
|
+
})
|
|
3444
|
+
.where(eq(issuePlanDecompositions.id, claim.id))
|
|
3445
|
+
.returning();
|
|
3446
|
+
if (!updatedClaim)
|
|
3447
|
+
throw new Error("Failed to persist accepted-plan decomposition progress");
|
|
3448
|
+
return {
|
|
3449
|
+
claim: updatedClaim,
|
|
3450
|
+
createdIssue: createdChild.issue,
|
|
3451
|
+
};
|
|
3452
|
+
});
|
|
3453
|
+
currentClaim = step.claim;
|
|
3454
|
+
if (step.createdIssue) {
|
|
3455
|
+
newlyCreatedIssues.push(step.createdIssue);
|
|
3456
|
+
}
|
|
3457
|
+
if (step.claim.status === "completed")
|
|
3458
|
+
break;
|
|
3459
|
+
}
|
|
3460
|
+
const childIssueIds = normalizeIssuePlanDecompositionChildIds(currentClaim.childIssueIds);
|
|
3461
|
+
const childIssueRows = childIssueIds.length > 0
|
|
3462
|
+
? await db
|
|
3463
|
+
.select()
|
|
3464
|
+
.from(issues)
|
|
3465
|
+
.where(and(eq(issues.squadId, sourceIssue.squadId), inArray(issues.id, childIssueIds)))
|
|
3466
|
+
: [];
|
|
3467
|
+
const childIssueMap = new Map(childIssueRows.map((row) => [row.id, row]));
|
|
3468
|
+
const orderedChildIssues = childIssueIds
|
|
3469
|
+
.map((childIssueId) => childIssueMap.get(childIssueId))
|
|
3470
|
+
.filter((row) => Boolean(row));
|
|
3471
|
+
const decomposition = serializeAcceptedPlanDecomposition(currentClaim);
|
|
3472
|
+
return {
|
|
3473
|
+
decomposition,
|
|
3474
|
+
childIssueIds: decomposition.childIssueIds,
|
|
3475
|
+
childIssues: orderedChildIssues,
|
|
3476
|
+
newlyCreatedIssues,
|
|
3477
|
+
};
|
|
3478
|
+
},
|
|
3479
|
+
listAcceptedPlanDecompositions: async (sourceIssueId) => {
|
|
3480
|
+
const sourceIssue = await db
|
|
3481
|
+
.select({ id: issues.id, squadId: issues.squadId })
|
|
3482
|
+
.from(issues)
|
|
3483
|
+
.where(eq(issues.id, sourceIssueId))
|
|
3484
|
+
.then((rows) => rows[0] ?? null);
|
|
3485
|
+
if (!sourceIssue)
|
|
3486
|
+
return [];
|
|
3487
|
+
const rows = await db
|
|
3488
|
+
.select({
|
|
3489
|
+
decomposition: issuePlanDecompositions,
|
|
3490
|
+
revisionNumber: documentRevisions.revisionNumber,
|
|
3491
|
+
})
|
|
3492
|
+
.from(issuePlanDecompositions)
|
|
3493
|
+
.leftJoin(documentRevisions, eq(documentRevisions.id, issuePlanDecompositions.acceptedPlanRevisionId))
|
|
3494
|
+
.where(and(eq(issuePlanDecompositions.squadId, sourceIssue.squadId), eq(issuePlanDecompositions.sourceIssueId, sourceIssue.id)))
|
|
3495
|
+
.orderBy(desc(issuePlanDecompositions.createdAt));
|
|
3496
|
+
if (rows.length === 0)
|
|
3497
|
+
return [];
|
|
3498
|
+
const allChildIds = new Set();
|
|
3499
|
+
for (const row of rows) {
|
|
3500
|
+
for (const childId of normalizeIssuePlanDecompositionChildIds(row.decomposition.childIssueIds)) {
|
|
3501
|
+
allChildIds.add(childId);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
const childIssueRows = allChildIds.size > 0
|
|
3505
|
+
? await db
|
|
3506
|
+
.select({
|
|
3507
|
+
id: issues.id,
|
|
3508
|
+
identifier: issues.identifier,
|
|
3509
|
+
title: issues.title,
|
|
3510
|
+
status: issues.status,
|
|
3511
|
+
priority: issues.priority,
|
|
3512
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
3513
|
+
assigneeUserId: issues.assigneeUserId,
|
|
3514
|
+
})
|
|
3515
|
+
.from(issues)
|
|
3516
|
+
.where(and(eq(issues.squadId, sourceIssue.squadId), inArray(issues.id, Array.from(allChildIds))))
|
|
3517
|
+
: [];
|
|
3518
|
+
const childIssueMap = new Map(childIssueRows.map((row) => [row.id, row]));
|
|
3519
|
+
return rows.map((row) => {
|
|
3520
|
+
const decomposition = serializeAcceptedPlanDecomposition(row.decomposition);
|
|
3521
|
+
const childIds = decomposition.childIssueIds;
|
|
3522
|
+
return {
|
|
3523
|
+
...decomposition,
|
|
3524
|
+
acceptedPlanRevisionNumber: row.revisionNumber ?? null,
|
|
3525
|
+
childIssues: childIds
|
|
3526
|
+
.map((childId) => childIssueMap.get(childId) ?? null)
|
|
3527
|
+
.filter((entry) => entry !== null),
|
|
3528
|
+
};
|
|
3529
|
+
});
|
|
3530
|
+
},
|
|
3531
|
+
create: async (squadId, data) => {
|
|
3532
|
+
const { labelIds: inputLabelIds, blockedByIssueIds, inheritExecutionWorkspaceFromIssueId, ...issueData } = data;
|
|
3533
|
+
const isolatedWorkspacesEnabled = (await instanceSettings.getExperimental()).enableIsolatedWorkspaces;
|
|
3534
|
+
if (!isolatedWorkspacesEnabled) {
|
|
3535
|
+
delete issueData.executionWorkspaceId;
|
|
3536
|
+
delete issueData.executionWorkspacePreference;
|
|
3537
|
+
delete issueData.executionWorkspaceSettings;
|
|
3538
|
+
}
|
|
3539
|
+
if (data.assigneeAgentId && data.assigneeUserId) {
|
|
3540
|
+
throw unprocessable("Issue can only have one assignee");
|
|
3541
|
+
}
|
|
3542
|
+
if (data.assigneeAgentId) {
|
|
3543
|
+
await assertAssignableAgent(squadId, data.assigneeAgentId);
|
|
3544
|
+
}
|
|
3545
|
+
if (data.assigneeUserId) {
|
|
3546
|
+
await assertAssignableUser(squadId, data.assigneeUserId);
|
|
3547
|
+
}
|
|
3548
|
+
if (data.status === "in_progress" && !data.assigneeAgentId && !data.assigneeUserId) {
|
|
3549
|
+
throw unprocessable("in_progress issues require an assignee");
|
|
3550
|
+
}
|
|
3551
|
+
return db.transaction(async (tx) => {
|
|
3552
|
+
const defaultSquadGoal = await getDefaultSquadGoal(tx, squadId);
|
|
3553
|
+
const projectGoalId = await getProjectDefaultGoalId(tx, squadId, issueData.projectId);
|
|
3554
|
+
let projectWorkspaceId = issueData.projectWorkspaceId ?? null;
|
|
3555
|
+
let executionWorkspaceId = issueData.executionWorkspaceId ?? null;
|
|
3556
|
+
let executionWorkspacePreference = issueData.executionWorkspacePreference ?? null;
|
|
3557
|
+
let executionWorkspaceSettings = issueData.executionWorkspaceSettings ?? null;
|
|
3558
|
+
const workspaceInheritanceIssueId = inheritExecutionWorkspaceFromIssueId ?? issueData.parentId ?? null;
|
|
3559
|
+
const hasExplicitExecutionWorkspaceOverride = issueData.executionWorkspaceId !== undefined ||
|
|
3560
|
+
issueData.executionWorkspacePreference !== undefined ||
|
|
3561
|
+
issueData.executionWorkspaceSettings !== undefined;
|
|
3562
|
+
if (workspaceInheritanceIssueId) {
|
|
3563
|
+
const workspaceSource = await getWorkspaceInheritanceIssue(tx, squadId, workspaceInheritanceIssueId);
|
|
3564
|
+
if (projectWorkspaceId == null && workspaceSource.projectWorkspaceId) {
|
|
3565
|
+
projectWorkspaceId = workspaceSource.projectWorkspaceId;
|
|
3566
|
+
}
|
|
3567
|
+
if (isolatedWorkspacesEnabled &&
|
|
3568
|
+
!hasExplicitExecutionWorkspaceOverride &&
|
|
3569
|
+
workspaceSource.executionWorkspaceId) {
|
|
3570
|
+
const sourceWorkspace = await tx
|
|
3571
|
+
.select({
|
|
3572
|
+
id: executionWorkspaces.id,
|
|
3573
|
+
mode: executionWorkspaces.mode,
|
|
3574
|
+
})
|
|
3575
|
+
.from(executionWorkspaces)
|
|
3576
|
+
.where(eq(executionWorkspaces.id, workspaceSource.executionWorkspaceId))
|
|
3577
|
+
.then((rows) => rows[0] ?? null);
|
|
3578
|
+
if (sourceWorkspace) {
|
|
3579
|
+
executionWorkspaceId = sourceWorkspace.id;
|
|
3580
|
+
executionWorkspacePreference = "reuse_existing";
|
|
3581
|
+
executionWorkspaceSettings = {
|
|
3582
|
+
...(workspaceSource.executionWorkspaceSettings ?? {}),
|
|
3583
|
+
mode: issueExecutionWorkspaceModeForPersistedWorkspace(sourceWorkspace.mode),
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
// Cache the project policy lookup for this insert. Both the
|
|
3589
|
+
// default-settings block and the assignee-environment-promotion block
|
|
3590
|
+
// need the same row; without caching they'd issue two round-trips.
|
|
3591
|
+
let projectPolicyCached = null;
|
|
3592
|
+
let projectPolicyLoaded = false;
|
|
3593
|
+
const loadProjectPolicyOnce = async () => {
|
|
3594
|
+
if (projectPolicyLoaded)
|
|
3595
|
+
return projectPolicyCached;
|
|
3596
|
+
projectPolicyLoaded = true;
|
|
3597
|
+
if (!issueData.projectId)
|
|
3598
|
+
return null;
|
|
3599
|
+
const projectRow = await tx
|
|
3600
|
+
.select({ executionWorkspacePolicy: projects.executionWorkspacePolicy })
|
|
3601
|
+
.from(projects)
|
|
3602
|
+
.where(and(eq(projects.id, issueData.projectId), eq(projects.squadId, squadId)))
|
|
3603
|
+
.then((rows) => rows[0] ?? null);
|
|
3604
|
+
projectPolicyCached = parseProjectExecutionWorkspacePolicy(projectRow?.executionWorkspacePolicy);
|
|
3605
|
+
return projectPolicyCached;
|
|
3606
|
+
};
|
|
3607
|
+
if (executionWorkspaceSettings == null &&
|
|
3608
|
+
executionWorkspaceId == null &&
|
|
3609
|
+
issueData.projectId) {
|
|
3610
|
+
executionWorkspaceSettings =
|
|
3611
|
+
defaultIssueExecutionWorkspaceSettingsForProject(gateProjectExecutionWorkspacePolicy(await loadProjectPolicyOnce(), isolatedWorkspacesEnabled));
|
|
3612
|
+
}
|
|
3613
|
+
if (data.assigneeAgentId && isolatedWorkspacesEnabled) {
|
|
3614
|
+
const currentWorkspaceSettings = executionWorkspaceSettings == null
|
|
3615
|
+
? {}
|
|
3616
|
+
: parseObject(executionWorkspaceSettings);
|
|
3617
|
+
const issueHasEnvironmentSelection = Object.prototype.hasOwnProperty.call(currentWorkspaceSettings, "environmentId");
|
|
3618
|
+
// Don't promote the assignee agent's defaultEnvironmentId if either
|
|
3619
|
+
// the issue or the project policy already specifies an environment.
|
|
3620
|
+
// resolveExecutionWorkspaceEnvironmentId treats issue settings as
|
|
3621
|
+
// higher priority than project policy, so promoting the agent's
|
|
3622
|
+
// default to issue settings would invert the documented priority
|
|
3623
|
+
// (project policy must win over agent default when explicitly set).
|
|
3624
|
+
let projectHasEnvironmentSelection = false;
|
|
3625
|
+
if (!issueHasEnvironmentSelection && issueData.projectId) {
|
|
3626
|
+
const projectPolicy = await loadProjectPolicyOnce();
|
|
3627
|
+
projectHasEnvironmentSelection = projectPolicy?.environmentId !== undefined;
|
|
3628
|
+
}
|
|
3629
|
+
if (!issueHasEnvironmentSelection && !projectHasEnvironmentSelection) {
|
|
3630
|
+
const assigneeAgent = await tx
|
|
3631
|
+
.select({ defaultEnvironmentId: agents.defaultEnvironmentId })
|
|
3632
|
+
.from(agents)
|
|
3633
|
+
.where(and(eq(agents.id, data.assigneeAgentId), eq(agents.squadId, squadId)))
|
|
3634
|
+
.then((rows) => rows[0] ?? null);
|
|
3635
|
+
if (typeof assigneeAgent?.defaultEnvironmentId === "string" && assigneeAgent.defaultEnvironmentId.length > 0) {
|
|
3636
|
+
executionWorkspaceSettings = {
|
|
3637
|
+
...currentWorkspaceSettings,
|
|
3638
|
+
environmentId: assigneeAgent.defaultEnvironmentId,
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
if (!projectWorkspaceId && issueData.projectId) {
|
|
3644
|
+
const project = await tx
|
|
3645
|
+
.select({
|
|
3646
|
+
executionWorkspacePolicy: projects.executionWorkspacePolicy,
|
|
3647
|
+
})
|
|
3648
|
+
.from(projects)
|
|
3649
|
+
.where(and(eq(projects.id, issueData.projectId), eq(projects.squadId, squadId)))
|
|
3650
|
+
.then((rows) => rows[0] ?? null);
|
|
3651
|
+
const projectPolicy = parseProjectExecutionWorkspacePolicy(project?.executionWorkspacePolicy);
|
|
3652
|
+
projectWorkspaceId = projectPolicy?.defaultProjectWorkspaceId ?? null;
|
|
3653
|
+
if (!projectWorkspaceId) {
|
|
3654
|
+
projectWorkspaceId = await tx
|
|
3655
|
+
.select({ id: projectWorkspaces.id })
|
|
3656
|
+
.from(projectWorkspaces)
|
|
3657
|
+
.where(and(eq(projectWorkspaces.projectId, issueData.projectId), eq(projectWorkspaces.squadId, squadId)))
|
|
3658
|
+
.orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id))
|
|
3659
|
+
.then((rows) => rows[0]?.id ?? null);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
if (projectWorkspaceId) {
|
|
3663
|
+
await assertValidProjectWorkspace(squadId, issueData.projectId, projectWorkspaceId, tx);
|
|
3664
|
+
}
|
|
3665
|
+
if (executionWorkspaceId) {
|
|
3666
|
+
await assertValidExecutionWorkspace(squadId, issueData.projectId, executionWorkspaceId, tx);
|
|
3667
|
+
}
|
|
3668
|
+
// Self-correcting counter: use MAX(issue_number) + 1 if the counter
|
|
3669
|
+
// has drifted below the actual max, preventing identifier collisions.
|
|
3670
|
+
const [maxRow] = await tx
|
|
3671
|
+
.select({ maxNum: sql `coalesce(max(${issues.issueNumber}), 0)` })
|
|
3672
|
+
.from(issues)
|
|
3673
|
+
.where(eq(issues.squadId, squadId));
|
|
3674
|
+
const currentMax = maxRow?.maxNum ?? 0;
|
|
3675
|
+
const [squad] = await tx
|
|
3676
|
+
.update(squads)
|
|
3677
|
+
.set({
|
|
3678
|
+
issueCounter: sql `greatest(${squads.issueCounter}, ${currentMax}) + 1`,
|
|
3679
|
+
})
|
|
3680
|
+
.where(eq(squads.id, squadId))
|
|
3681
|
+
.returning({ issueCounter: squads.issueCounter, issuePrefix: squads.issuePrefix });
|
|
3682
|
+
const issueNumber = squad.issueCounter;
|
|
3683
|
+
const identifier = `${squad.issuePrefix}-${issueNumber}`;
|
|
3684
|
+
const values = {
|
|
3685
|
+
...issueData,
|
|
3686
|
+
requestDepth: clampIssueRequestDepth(issueData.requestDepth),
|
|
3687
|
+
originKind: issueData.originKind ?? "manual",
|
|
3688
|
+
goalId: resolveIssueGoalId({
|
|
3689
|
+
projectId: issueData.projectId,
|
|
3690
|
+
goalId: issueData.goalId,
|
|
3691
|
+
projectGoalId,
|
|
3692
|
+
defaultGoalId: defaultSquadGoal?.id ?? null,
|
|
3693
|
+
}),
|
|
3694
|
+
...(projectWorkspaceId ? { projectWorkspaceId } : {}),
|
|
3695
|
+
...(executionWorkspaceId ? { executionWorkspaceId } : {}),
|
|
3696
|
+
...(executionWorkspacePreference ? { executionWorkspacePreference } : {}),
|
|
3697
|
+
...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}),
|
|
3698
|
+
squadId,
|
|
3699
|
+
issueNumber,
|
|
3700
|
+
identifier,
|
|
3701
|
+
};
|
|
3702
|
+
if (values.status === "in_progress" && !values.startedAt) {
|
|
3703
|
+
values.startedAt = new Date();
|
|
3704
|
+
}
|
|
3705
|
+
if (values.status === "done") {
|
|
3706
|
+
values.completedAt = new Date();
|
|
3707
|
+
}
|
|
3708
|
+
if (values.status === "cancelled") {
|
|
3709
|
+
values.cancelledAt = new Date();
|
|
3710
|
+
}
|
|
3711
|
+
Object.assign(values, buildInitialIssueMonitorFields({
|
|
3712
|
+
policy: normalizeIssueExecutionPolicy(issueData.executionPolicy ?? null),
|
|
3713
|
+
status: values.status ?? "backlog",
|
|
3714
|
+
assigneeAgentId: values.assigneeAgentId ?? null,
|
|
3715
|
+
assigneeUserId: values.assigneeUserId ?? null,
|
|
3716
|
+
}));
|
|
3717
|
+
const [issue] = await tx.insert(issues).values(values).returning();
|
|
3718
|
+
if (inputLabelIds) {
|
|
3719
|
+
await syncIssueLabels(issue.id, squadId, inputLabelIds, tx);
|
|
3720
|
+
}
|
|
3721
|
+
if (blockedByIssueIds !== undefined) {
|
|
3722
|
+
await syncBlockedByIssueIds(issue.id, squadId, blockedByIssueIds, {
|
|
3723
|
+
agentId: issueData.createdByAgentId ?? null,
|
|
3724
|
+
userId: issueData.createdByUserId ?? null,
|
|
3725
|
+
}, tx);
|
|
3726
|
+
}
|
|
3727
|
+
const [enriched] = await withIssueLabels(tx, [issue]);
|
|
3728
|
+
return enriched;
|
|
3729
|
+
});
|
|
3730
|
+
},
|
|
3731
|
+
update: async (id, data, dbOrTx = db) => {
|
|
3732
|
+
const existing = await dbOrTx
|
|
3733
|
+
.select()
|
|
3734
|
+
.from(issues)
|
|
3735
|
+
.where(eq(issues.id, id))
|
|
3736
|
+
.then((rows) => rows[0] ?? null);
|
|
3737
|
+
if (!existing)
|
|
3738
|
+
return null;
|
|
3739
|
+
const { labelIds: nextLabelIds, blockedByIssueIds, actorAgentId, actorUserId, ...issueData } = data;
|
|
3740
|
+
const isolatedWorkspacesEnabled = (await instanceSettings.getExperimental()).enableIsolatedWorkspaces;
|
|
3741
|
+
if (!isolatedWorkspacesEnabled) {
|
|
3742
|
+
delete issueData.executionWorkspaceId;
|
|
3743
|
+
delete issueData.executionWorkspacePreference;
|
|
3744
|
+
delete issueData.executionWorkspaceSettings;
|
|
3745
|
+
}
|
|
3746
|
+
if (issueData.status) {
|
|
3747
|
+
assertTransition(existing.status, issueData.status);
|
|
3748
|
+
}
|
|
3749
|
+
const patch = {
|
|
3750
|
+
...issueData,
|
|
3751
|
+
updatedAt: new Date(),
|
|
3752
|
+
};
|
|
3753
|
+
if (issueData.requestDepth !== undefined) {
|
|
3754
|
+
patch.requestDepth = clampIssueRequestDepth(issueData.requestDepth);
|
|
3755
|
+
}
|
|
3756
|
+
const nextAssigneeAgentId = issueData.assigneeAgentId !== undefined ? issueData.assigneeAgentId : existing.assigneeAgentId;
|
|
3757
|
+
const nextAssigneeUserId = issueData.assigneeUserId !== undefined ? issueData.assigneeUserId : existing.assigneeUserId;
|
|
3758
|
+
if (nextAssigneeAgentId && nextAssigneeUserId) {
|
|
3759
|
+
throw unprocessable("Issue can only have one assignee");
|
|
3760
|
+
}
|
|
3761
|
+
if (patch.status === "in_progress" && !nextAssigneeAgentId && !nextAssigneeUserId) {
|
|
3762
|
+
throw unprocessable("in_progress issues require an assignee");
|
|
3763
|
+
}
|
|
3764
|
+
if (patch.status === "in_progress") {
|
|
3765
|
+
const unresolvedBlockerIssueIds = blockedByIssueIds !== undefined
|
|
3766
|
+
? await listUnresolvedBlockerIssueIds(dbOrTx, existing.squadId, blockedByIssueIds)
|
|
3767
|
+
: (await listIssueDependencyReadinessMap(dbOrTx, existing.squadId, [id])).get(id)?.unresolvedBlockerIssueIds ?? [];
|
|
3768
|
+
if (unresolvedBlockerIssueIds.length > 0) {
|
|
3769
|
+
throw unprocessable("Issue is blocked by unresolved blockers", { unresolvedBlockerIssueIds });
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
if (issueData.assigneeAgentId) {
|
|
3773
|
+
await assertAssignableAgent(existing.squadId, issueData.assigneeAgentId);
|
|
3774
|
+
}
|
|
3775
|
+
if (issueData.assigneeUserId) {
|
|
3776
|
+
await assertAssignableUser(existing.squadId, issueData.assigneeUserId);
|
|
3777
|
+
}
|
|
3778
|
+
const nextProjectId = issueData.projectId !== undefined ? issueData.projectId : existing.projectId;
|
|
3779
|
+
const nextProjectWorkspaceId = issueData.projectWorkspaceId !== undefined ? issueData.projectWorkspaceId : existing.projectWorkspaceId;
|
|
3780
|
+
const nextExecutionWorkspaceId = issueData.executionWorkspaceId !== undefined ? issueData.executionWorkspaceId : existing.executionWorkspaceId;
|
|
3781
|
+
const nextExecutionWorkspacePreference = issueData.executionWorkspacePreference !== undefined
|
|
3782
|
+
? issueData.executionWorkspacePreference
|
|
3783
|
+
: existing.executionWorkspacePreference;
|
|
3784
|
+
const nextExecutionWorkspaceSettings = issueData.executionWorkspaceSettings !== undefined
|
|
3785
|
+
? parseIssueExecutionWorkspaceSettings(issueData.executionWorkspaceSettings)
|
|
3786
|
+
: parseIssueExecutionWorkspaceSettings(existing.executionWorkspaceSettings);
|
|
3787
|
+
if (nextProjectWorkspaceId) {
|
|
3788
|
+
await assertValidProjectWorkspace(existing.squadId, nextProjectId, nextProjectWorkspaceId);
|
|
3789
|
+
}
|
|
3790
|
+
if (nextExecutionWorkspaceId) {
|
|
3791
|
+
await assertValidExecutionWorkspace(existing.squadId, nextProjectId, nextExecutionWorkspaceId);
|
|
3792
|
+
}
|
|
3793
|
+
applyStatusSideEffects(issueData.status, patch);
|
|
3794
|
+
if (issueData.status && issueData.status !== "done") {
|
|
3795
|
+
patch.completedAt = null;
|
|
3796
|
+
}
|
|
3797
|
+
if (issueData.status && issueData.status !== "cancelled") {
|
|
3798
|
+
patch.cancelledAt = null;
|
|
3799
|
+
}
|
|
3800
|
+
if (issueData.status && issueData.status !== "in_progress") {
|
|
3801
|
+
patch.checkoutRunId = null;
|
|
3802
|
+
// Fix B: also clear the execution lock when leaving in_progress
|
|
3803
|
+
patch.executionRunId = null;
|
|
3804
|
+
patch.executionAgentNameKey = null;
|
|
3805
|
+
patch.executionLockedAt = null;
|
|
3806
|
+
}
|
|
3807
|
+
if ((issueData.assigneeAgentId !== undefined && issueData.assigneeAgentId !== existing.assigneeAgentId) ||
|
|
3808
|
+
(issueData.assigneeUserId !== undefined && issueData.assigneeUserId !== existing.assigneeUserId)) {
|
|
3809
|
+
patch.checkoutRunId = null;
|
|
3810
|
+
// Fix B: clear execution lock on reassignment, matching checkoutRunId clear
|
|
3811
|
+
patch.executionRunId = null;
|
|
3812
|
+
patch.executionAgentNameKey = null;
|
|
3813
|
+
patch.executionLockedAt = null;
|
|
3814
|
+
}
|
|
3815
|
+
const runUpdate = async (tx) => {
|
|
3816
|
+
const defaultSquadGoal = await getDefaultSquadGoal(tx, existing.squadId);
|
|
3817
|
+
const [currentProjectGoalId, nextProjectGoalId] = await Promise.all([
|
|
3818
|
+
getProjectDefaultGoalId(tx, existing.squadId, existing.projectId),
|
|
3819
|
+
getProjectDefaultGoalId(tx, existing.squadId, issueData.projectId !== undefined ? issueData.projectId : existing.projectId),
|
|
3820
|
+
]);
|
|
3821
|
+
// Mirror the create() path: when the assignee changes to a non-null
|
|
3822
|
+
// agent, default the issue's executionWorkspaceSettings.environmentId
|
|
3823
|
+
// to the new agent's defaultEnvironmentId. Skip when:
|
|
3824
|
+
// - this update explicitly sets executionWorkspaceSettings.environmentId
|
|
3825
|
+
// (caller is making a deliberate override; respect it), OR
|
|
3826
|
+
// - the project policy already specifies an environmentId (project
|
|
3827
|
+
// policy must win over agent default per the documented priority
|
|
3828
|
+
// order in resolveExecutionWorkspaceEnvironmentId), OR
|
|
3829
|
+
// - the issue already has an environmentId that was *not* the prior
|
|
3830
|
+
// assignee's default (i.e., the operator set it explicitly in an
|
|
3831
|
+
// earlier update; preserve their choice). When the existing
|
|
3832
|
+
// environmentId matches the prior assignee's default, treat it as
|
|
3833
|
+
// auto-promoted and refresh it to the new assignee's default.
|
|
3834
|
+
const assigneeChanged = issueData.assigneeAgentId !== undefined &&
|
|
3835
|
+
issueData.assigneeAgentId !== null &&
|
|
3836
|
+
issueData.assigneeAgentId !== existing.assigneeAgentId;
|
|
3837
|
+
const explicitEnvInThisUpdate = issueData.executionWorkspaceSettings !== undefined &&
|
|
3838
|
+
Object.prototype.hasOwnProperty.call(parseObject(issueData.executionWorkspaceSettings), "environmentId");
|
|
3839
|
+
if (assigneeChanged && isolatedWorkspacesEnabled && !explicitEnvInThisUpdate) {
|
|
3840
|
+
let projectHasEnvironmentSelection = false;
|
|
3841
|
+
if (nextProjectId) {
|
|
3842
|
+
const projectRow = await tx
|
|
3843
|
+
.select({ executionWorkspacePolicy: projects.executionWorkspacePolicy })
|
|
3844
|
+
.from(projects)
|
|
3845
|
+
.where(and(eq(projects.id, nextProjectId), eq(projects.squadId, existing.squadId)))
|
|
3846
|
+
.then((rows) => rows[0] ?? null);
|
|
3847
|
+
const projectPolicy = parseProjectExecutionWorkspacePolicy(projectRow?.executionWorkspacePolicy);
|
|
3848
|
+
projectHasEnvironmentSelection = projectPolicy?.environmentId !== undefined;
|
|
3849
|
+
}
|
|
3850
|
+
if (!projectHasEnvironmentSelection) {
|
|
3851
|
+
const baseSettings = nextExecutionWorkspaceSettings == null
|
|
3852
|
+
? {}
|
|
3853
|
+
: parseObject(nextExecutionWorkspaceSettings);
|
|
3854
|
+
const existingEnvId = typeof baseSettings.environmentId === "string"
|
|
3855
|
+
? baseSettings.environmentId
|
|
3856
|
+
: null;
|
|
3857
|
+
const agentRows = await tx
|
|
3858
|
+
.select({ id: agents.id, defaultEnvironmentId: agents.defaultEnvironmentId })
|
|
3859
|
+
.from(agents)
|
|
3860
|
+
.where(and(eq(agents.squadId, existing.squadId), inArray(agents.id, [issueData.assigneeAgentId, existing.assigneeAgentId].filter((value) => typeof value === "string"))));
|
|
3861
|
+
const newAssignee = agentRows.find((row) => row.id === issueData.assigneeAgentId);
|
|
3862
|
+
const previousAssignee = existing.assigneeAgentId
|
|
3863
|
+
? agentRows.find((row) => row.id === existing.assigneeAgentId)
|
|
3864
|
+
: null;
|
|
3865
|
+
const newDefaultEnvId = typeof newAssignee?.defaultEnvironmentId === "string" && newAssignee.defaultEnvironmentId.length > 0
|
|
3866
|
+
? newAssignee.defaultEnvironmentId
|
|
3867
|
+
: null;
|
|
3868
|
+
const previousDefaultEnvId = typeof previousAssignee?.defaultEnvironmentId === "string" && previousAssignee.defaultEnvironmentId.length > 0
|
|
3869
|
+
? previousAssignee.defaultEnvironmentId
|
|
3870
|
+
: null;
|
|
3871
|
+
const existingEnvWasAutoPromoted = existingEnvId === null ||
|
|
3872
|
+
(previousDefaultEnvId !== null && existingEnvId === previousDefaultEnvId);
|
|
3873
|
+
if (newDefaultEnvId && existingEnvWasAutoPromoted) {
|
|
3874
|
+
patch.executionWorkspaceSettings = {
|
|
3875
|
+
...baseSettings,
|
|
3876
|
+
environmentId: newDefaultEnvId,
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
patch.goalId = resolveNextIssueGoalId({
|
|
3882
|
+
currentProjectId: existing.projectId,
|
|
3883
|
+
currentGoalId: existing.goalId,
|
|
3884
|
+
currentProjectGoalId,
|
|
3885
|
+
projectId: issueData.projectId,
|
|
3886
|
+
goalId: issueData.goalId,
|
|
3887
|
+
projectGoalId: nextProjectGoalId,
|
|
3888
|
+
defaultGoalId: defaultSquadGoal?.id ?? null,
|
|
3889
|
+
});
|
|
3890
|
+
const updated = await tx
|
|
3891
|
+
.update(issues)
|
|
3892
|
+
.set(patch)
|
|
3893
|
+
.where(eq(issues.id, id))
|
|
3894
|
+
.returning()
|
|
3895
|
+
.then((rows) => rows[0] ?? null);
|
|
3896
|
+
if (!updated)
|
|
3897
|
+
return null;
|
|
3898
|
+
if (nextLabelIds !== undefined) {
|
|
3899
|
+
await syncIssueLabels(updated.id, existing.squadId, nextLabelIds, tx);
|
|
3900
|
+
}
|
|
3901
|
+
if (blockedByIssueIds !== undefined) {
|
|
3902
|
+
await syncBlockedByIssueIds(updated.id, existing.squadId, blockedByIssueIds, {
|
|
3903
|
+
agentId: actorAgentId ?? null,
|
|
3904
|
+
userId: actorUserId ?? null,
|
|
3905
|
+
}, tx);
|
|
3906
|
+
}
|
|
3907
|
+
if (issueData.executionWorkspaceSettings !== undefined &&
|
|
3908
|
+
nextExecutionWorkspaceId &&
|
|
3909
|
+
nextExecutionWorkspacePreference === "reuse_existing") {
|
|
3910
|
+
const workspace = await tx
|
|
3911
|
+
.select({
|
|
3912
|
+
id: executionWorkspaces.id,
|
|
3913
|
+
metadata: executionWorkspaces.metadata,
|
|
3914
|
+
})
|
|
3915
|
+
.from(executionWorkspaces)
|
|
3916
|
+
.where(and(eq(executionWorkspaces.id, nextExecutionWorkspaceId), eq(executionWorkspaces.squadId, existing.squadId)))
|
|
3917
|
+
.then((rows) => rows[0] ?? null);
|
|
3918
|
+
if (workspace) {
|
|
3919
|
+
await tx
|
|
3920
|
+
.update(executionWorkspaces)
|
|
3921
|
+
.set({
|
|
3922
|
+
metadata: mergeExecutionWorkspaceConfig(workspace.metadata ?? null, buildReusedExecutionWorkspaceConfigPatchFromIssueSettings(nextExecutionWorkspaceSettings)),
|
|
3923
|
+
updatedAt: new Date(),
|
|
3924
|
+
})
|
|
3925
|
+
.where(eq(executionWorkspaces.id, workspace.id));
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
const [enriched] = await withIssueLabels(tx, [updated]);
|
|
3929
|
+
if ((issueData.status === "done" || issueData.status === "cancelled") &&
|
|
3930
|
+
existing.status !== issueData.status &&
|
|
3931
|
+
existing.originKind === RECOVERY_ORIGIN_KINDS.issueGraphLivenessEscalation) {
|
|
3932
|
+
const parsedIncident = parseIssueGraphLivenessIncidentKey(existing.originId);
|
|
3933
|
+
if (parsedIncident?.issueId && parsedIncident.squadId === existing.squadId) {
|
|
3934
|
+
await tx
|
|
3935
|
+
.delete(issueRelations)
|
|
3936
|
+
.where(and(eq(issueRelations.squadId, existing.squadId), eq(issueRelations.issueId, existing.id), eq(issueRelations.relatedIssueId, parsedIncident.issueId), eq(issueRelations.type, "blocks")));
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
return enriched;
|
|
3940
|
+
};
|
|
3941
|
+
return dbOrTx === db ? db.transaction(runUpdate) : runUpdate(dbOrTx);
|
|
3942
|
+
},
|
|
3943
|
+
clearExecutionWorkspaceEnvironmentSelection: async (squadId, environmentId) => {
|
|
3944
|
+
const rows = await db
|
|
3945
|
+
.select({
|
|
3946
|
+
id: issues.id,
|
|
3947
|
+
executionWorkspaceSettings: issues.executionWorkspaceSettings,
|
|
3948
|
+
})
|
|
3949
|
+
.from(issues)
|
|
3950
|
+
.where(eq(issues.squadId, squadId));
|
|
3951
|
+
let cleared = 0;
|
|
3952
|
+
for (const row of rows) {
|
|
3953
|
+
const settings = parseIssueExecutionWorkspaceSettings(row.executionWorkspaceSettings);
|
|
3954
|
+
if (settings?.environmentId !== environmentId)
|
|
3955
|
+
continue;
|
|
3956
|
+
await db
|
|
3957
|
+
.update(issues)
|
|
3958
|
+
.set({
|
|
3959
|
+
executionWorkspaceSettings: {
|
|
3960
|
+
...settings,
|
|
3961
|
+
environmentId: null,
|
|
3962
|
+
},
|
|
3963
|
+
updatedAt: new Date(),
|
|
3964
|
+
})
|
|
3965
|
+
.where(eq(issues.id, row.id));
|
|
3966
|
+
cleared += 1;
|
|
3967
|
+
}
|
|
3968
|
+
return cleared;
|
|
3969
|
+
},
|
|
3970
|
+
remove: (id) => db.transaction(async (tx) => {
|
|
3971
|
+
const attachmentAssetIds = await tx
|
|
3972
|
+
.select({ assetId: issueAttachments.assetId })
|
|
3973
|
+
.from(issueAttachments)
|
|
3974
|
+
.where(eq(issueAttachments.issueId, id));
|
|
3975
|
+
const issueDocumentIds = await tx
|
|
3976
|
+
.select({ documentId: issueDocuments.documentId })
|
|
3977
|
+
.from(issueDocuments)
|
|
3978
|
+
.where(eq(issueDocuments.issueId, id));
|
|
3979
|
+
const removedIssue = await tx
|
|
3980
|
+
.delete(issues)
|
|
3981
|
+
.where(eq(issues.id, id))
|
|
3982
|
+
.returning()
|
|
3983
|
+
.then((rows) => rows[0] ?? null);
|
|
3984
|
+
if (removedIssue && attachmentAssetIds.length > 0) {
|
|
3985
|
+
await tx
|
|
3986
|
+
.delete(assets)
|
|
3987
|
+
.where(inArray(assets.id, attachmentAssetIds.map((row) => row.assetId)));
|
|
3988
|
+
}
|
|
3989
|
+
if (removedIssue && issueDocumentIds.length > 0) {
|
|
3990
|
+
await tx
|
|
3991
|
+
.delete(documents)
|
|
3992
|
+
.where(inArray(documents.id, issueDocumentIds.map((row) => row.documentId)));
|
|
3993
|
+
}
|
|
3994
|
+
if (!removedIssue)
|
|
3995
|
+
return null;
|
|
3996
|
+
const [enriched] = await withIssueLabels(tx, [removedIssue]);
|
|
3997
|
+
return enriched;
|
|
3998
|
+
}),
|
|
3999
|
+
checkout: async (id, agentId, expectedStatuses, checkoutRunId) => {
|
|
4000
|
+
const issueSquad = await db
|
|
4001
|
+
.select({ squadId: issues.squadId })
|
|
4002
|
+
.from(issues)
|
|
4003
|
+
.where(eq(issues.id, id))
|
|
4004
|
+
.then((rows) => rows[0] ?? null);
|
|
4005
|
+
if (!issueSquad)
|
|
4006
|
+
throw notFound("Issue not found");
|
|
4007
|
+
await assertAssignableAgent(issueSquad.squadId, agentId);
|
|
4008
|
+
const now = new Date();
|
|
4009
|
+
const activePauseHold = await treeControlSvc.getActivePauseHoldGate(issueSquad.squadId, id);
|
|
4010
|
+
if (activePauseHold &&
|
|
4011
|
+
!(await isTreeHoldInteractionCheckoutAllowed(issueSquad.squadId, checkoutRunId, activePauseHold))) {
|
|
4012
|
+
throw conflict("Issue checkout blocked by active subtree pause hold", {
|
|
4013
|
+
issueId: id,
|
|
4014
|
+
holdId: activePauseHold.holdId,
|
|
4015
|
+
rootIssueId: activePauseHold.rootIssueId,
|
|
4016
|
+
mode: activePauseHold.mode,
|
|
4017
|
+
securityPrinciples: ["Complete Mediation", "Fail Securely", "Secure Defaults"],
|
|
4018
|
+
});
|
|
4019
|
+
}
|
|
4020
|
+
await clearExecutionRunIfTerminal(id);
|
|
4021
|
+
const dependencyReadiness = await listIssueDependencyReadinessMap(db, issueSquad.squadId, [id]);
|
|
4022
|
+
const unresolvedBlockerIssueIds = dependencyReadiness.get(id)?.unresolvedBlockerIssueIds ?? [];
|
|
4023
|
+
if (unresolvedBlockerIssueIds.length > 0) {
|
|
4024
|
+
throw unprocessable("Issue is blocked by unresolved blockers", { unresolvedBlockerIssueIds });
|
|
4025
|
+
}
|
|
4026
|
+
const sameRunAssigneeCondition = checkoutRunId
|
|
4027
|
+
? and(eq(issues.assigneeAgentId, agentId), or(isNull(issues.checkoutRunId), eq(issues.checkoutRunId, checkoutRunId)))
|
|
4028
|
+
: and(eq(issues.assigneeAgentId, agentId), isNull(issues.checkoutRunId));
|
|
4029
|
+
const executionLockCondition = checkoutRunId
|
|
4030
|
+
? or(isNull(issues.executionRunId), eq(issues.executionRunId, checkoutRunId))
|
|
4031
|
+
: isNull(issues.executionRunId);
|
|
4032
|
+
const updated = await db
|
|
4033
|
+
.update(issues)
|
|
4034
|
+
.set({
|
|
4035
|
+
assigneeAgentId: agentId,
|
|
4036
|
+
assigneeUserId: null,
|
|
4037
|
+
checkoutRunId,
|
|
4038
|
+
executionRunId: checkoutRunId,
|
|
4039
|
+
status: "in_progress",
|
|
4040
|
+
startedAt: now,
|
|
4041
|
+
updatedAt: now,
|
|
4042
|
+
})
|
|
4043
|
+
.where(and(eq(issues.id, id), inArray(issues.status, expectedStatuses), or(isNull(issues.assigneeAgentId), sameRunAssigneeCondition), executionLockCondition))
|
|
4044
|
+
.returning()
|
|
4045
|
+
.then((rows) => rows[0] ?? null);
|
|
4046
|
+
if (updated) {
|
|
4047
|
+
const [enriched] = await withIssueLabels(db, [updated]);
|
|
4048
|
+
return enriched;
|
|
4049
|
+
}
|
|
4050
|
+
const current = await db
|
|
4051
|
+
.select({
|
|
4052
|
+
id: issues.id,
|
|
4053
|
+
status: issues.status,
|
|
4054
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
4055
|
+
checkoutRunId: issues.checkoutRunId,
|
|
4056
|
+
executionRunId: issues.executionRunId,
|
|
4057
|
+
})
|
|
4058
|
+
.from(issues)
|
|
4059
|
+
.where(eq(issues.id, id))
|
|
4060
|
+
.then((rows) => rows[0] ?? null);
|
|
4061
|
+
if (!current)
|
|
4062
|
+
throw notFound("Issue not found");
|
|
4063
|
+
if (current.assigneeAgentId === agentId &&
|
|
4064
|
+
current.status === "in_progress" &&
|
|
4065
|
+
current.checkoutRunId == null &&
|
|
4066
|
+
(current.executionRunId == null || current.executionRunId === checkoutRunId) &&
|
|
4067
|
+
checkoutRunId) {
|
|
4068
|
+
const adopted = await db
|
|
4069
|
+
.update(issues)
|
|
4070
|
+
.set({
|
|
4071
|
+
checkoutRunId,
|
|
4072
|
+
executionRunId: checkoutRunId,
|
|
4073
|
+
updatedAt: new Date(),
|
|
4074
|
+
})
|
|
4075
|
+
.where(and(eq(issues.id, id), eq(issues.status, "in_progress"), eq(issues.assigneeAgentId, agentId), isNull(issues.checkoutRunId), or(isNull(issues.executionRunId), eq(issues.executionRunId, checkoutRunId))))
|
|
4076
|
+
.returning()
|
|
4077
|
+
.then((rows) => rows[0] ?? null);
|
|
4078
|
+
if (adopted)
|
|
4079
|
+
return adopted;
|
|
4080
|
+
}
|
|
4081
|
+
if (checkoutRunId &&
|
|
4082
|
+
current.assigneeAgentId === agentId &&
|
|
4083
|
+
current.status === "in_progress" &&
|
|
4084
|
+
current.checkoutRunId &&
|
|
4085
|
+
current.checkoutRunId !== checkoutRunId) {
|
|
4086
|
+
const adopted = await adoptStaleCheckoutRun({
|
|
4087
|
+
issueId: id,
|
|
4088
|
+
actorAgentId: agentId,
|
|
4089
|
+
actorRunId: checkoutRunId,
|
|
4090
|
+
expectedCheckoutRunId: current.checkoutRunId,
|
|
4091
|
+
});
|
|
4092
|
+
if (adopted) {
|
|
4093
|
+
const row = await db.select().from(issues).where(eq(issues.id, id)).then((rows) => rows[0] ?? null);
|
|
4094
|
+
if (!row)
|
|
4095
|
+
throw notFound("Issue not found");
|
|
4096
|
+
const [enriched] = await withIssueLabels(db, [row]);
|
|
4097
|
+
return enriched;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
// If this run already owns it and it's in_progress, return it (no self-409)
|
|
4101
|
+
if (current.assigneeAgentId === agentId &&
|
|
4102
|
+
current.status === "in_progress" &&
|
|
4103
|
+
sameRunLock(current.checkoutRunId, checkoutRunId)) {
|
|
4104
|
+
const row = await db.select().from(issues).where(eq(issues.id, id)).then((rows) => rows[0] ?? null);
|
|
4105
|
+
if (!row)
|
|
4106
|
+
throw notFound("Issue not found");
|
|
4107
|
+
const [enriched] = await withIssueLabels(db, [row]);
|
|
4108
|
+
return enriched;
|
|
4109
|
+
}
|
|
4110
|
+
throw conflict("Issue checkout conflict", {
|
|
4111
|
+
issueId: current.id,
|
|
4112
|
+
status: current.status,
|
|
4113
|
+
assigneeAgentId: current.assigneeAgentId,
|
|
4114
|
+
checkoutRunId: current.checkoutRunId,
|
|
4115
|
+
executionRunId: current.executionRunId,
|
|
4116
|
+
});
|
|
4117
|
+
},
|
|
4118
|
+
assertCheckoutOwner: async (id, actorAgentId, actorRunId) => {
|
|
4119
|
+
await clearExecutionRunIfTerminal(id);
|
|
4120
|
+
const current = await db
|
|
4121
|
+
.select({
|
|
4122
|
+
id: issues.id,
|
|
4123
|
+
status: issues.status,
|
|
4124
|
+
assigneeAgentId: issues.assigneeAgentId,
|
|
4125
|
+
checkoutRunId: issues.checkoutRunId,
|
|
4126
|
+
executionRunId: issues.executionRunId,
|
|
4127
|
+
})
|
|
4128
|
+
.from(issues)
|
|
4129
|
+
.where(eq(issues.id, id))
|
|
4130
|
+
.then((rows) => rows[0] ?? null);
|
|
4131
|
+
if (!current)
|
|
4132
|
+
throw notFound("Issue not found");
|
|
4133
|
+
if (current.status === "in_progress" &&
|
|
4134
|
+
current.assigneeAgentId === actorAgentId &&
|
|
4135
|
+
sameRunLock(current.checkoutRunId, actorRunId)) {
|
|
4136
|
+
return { ...current, adoptedFromRunId: null };
|
|
4137
|
+
}
|
|
4138
|
+
if (actorRunId &&
|
|
4139
|
+
current.status === "in_progress" &&
|
|
4140
|
+
current.assigneeAgentId === actorAgentId &&
|
|
4141
|
+
current.checkoutRunId == null &&
|
|
4142
|
+
(current.executionRunId == null || current.executionRunId === actorRunId)) {
|
|
4143
|
+
const adopted = await adoptUnownedCheckoutRun({
|
|
4144
|
+
issueId: id,
|
|
4145
|
+
actorAgentId,
|
|
4146
|
+
actorRunId,
|
|
4147
|
+
});
|
|
4148
|
+
if (adopted) {
|
|
4149
|
+
return {
|
|
4150
|
+
...adopted,
|
|
4151
|
+
adoptedFromRunId: null,
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
if (actorRunId &&
|
|
4156
|
+
current.status === "in_progress" &&
|
|
4157
|
+
current.assigneeAgentId === actorAgentId &&
|
|
4158
|
+
current.checkoutRunId &&
|
|
4159
|
+
current.checkoutRunId !== actorRunId) {
|
|
4160
|
+
const adopted = await adoptStaleCheckoutRun({
|
|
4161
|
+
issueId: id,
|
|
4162
|
+
actorAgentId,
|
|
4163
|
+
actorRunId,
|
|
4164
|
+
expectedCheckoutRunId: current.checkoutRunId,
|
|
4165
|
+
});
|
|
4166
|
+
if (adopted) {
|
|
4167
|
+
return {
|
|
4168
|
+
...adopted,
|
|
4169
|
+
adoptedFromRunId: current.checkoutRunId,
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
throw conflict("Issue run ownership conflict", {
|
|
4174
|
+
issueId: current.id,
|
|
4175
|
+
status: current.status,
|
|
4176
|
+
assigneeAgentId: current.assigneeAgentId,
|
|
4177
|
+
checkoutRunId: current.checkoutRunId,
|
|
4178
|
+
executionRunId: current.executionRunId,
|
|
4179
|
+
actorAgentId,
|
|
4180
|
+
actorRunId,
|
|
4181
|
+
});
|
|
4182
|
+
},
|
|
4183
|
+
release: async (id, actorAgentId, actorRunId) => {
|
|
4184
|
+
await clearExecutionRunIfTerminal(id);
|
|
4185
|
+
const existing = await db
|
|
4186
|
+
.select()
|
|
4187
|
+
.from(issues)
|
|
4188
|
+
.where(eq(issues.id, id))
|
|
4189
|
+
.then((rows) => rows[0] ?? null);
|
|
4190
|
+
if (!existing)
|
|
4191
|
+
return null;
|
|
4192
|
+
if (actorAgentId && existing.assigneeAgentId && existing.assigneeAgentId !== actorAgentId) {
|
|
4193
|
+
throw conflict("Only assignee can release issue");
|
|
4194
|
+
}
|
|
4195
|
+
if (actorAgentId &&
|
|
4196
|
+
existing.status === "in_progress" &&
|
|
4197
|
+
existing.assigneeAgentId === actorAgentId &&
|
|
4198
|
+
existing.checkoutRunId &&
|
|
4199
|
+
!sameRunLock(existing.checkoutRunId, actorRunId ?? null)) {
|
|
4200
|
+
const stale = await isTerminalOrMissingHeartbeatRun(existing.checkoutRunId);
|
|
4201
|
+
if (!stale) {
|
|
4202
|
+
throw conflict("Only checkout run can release issue", {
|
|
4203
|
+
issueId: existing.id,
|
|
4204
|
+
assigneeAgentId: existing.assigneeAgentId,
|
|
4205
|
+
checkoutRunId: existing.checkoutRunId,
|
|
4206
|
+
actorRunId: actorRunId ?? null,
|
|
4207
|
+
});
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
const updated = await db
|
|
4211
|
+
.update(issues)
|
|
4212
|
+
.set({
|
|
4213
|
+
status: "todo",
|
|
4214
|
+
assigneeAgentId: null,
|
|
4215
|
+
checkoutRunId: null,
|
|
4216
|
+
executionRunId: null,
|
|
4217
|
+
executionAgentNameKey: null,
|
|
4218
|
+
executionLockedAt: null,
|
|
4219
|
+
updatedAt: new Date(),
|
|
4220
|
+
})
|
|
4221
|
+
.where(eq(issues.id, id))
|
|
4222
|
+
.returning()
|
|
4223
|
+
.then((rows) => rows[0] ?? null);
|
|
4224
|
+
if (!updated)
|
|
4225
|
+
return null;
|
|
4226
|
+
const [enriched] = await withIssueLabels(db, [updated]);
|
|
4227
|
+
return enriched;
|
|
4228
|
+
},
|
|
4229
|
+
adminForceRelease: async (id, options = {}) => db.transaction(async (tx) => {
|
|
4230
|
+
await tx.execute(sql `select ${issues.id} from ${issues} where ${issues.id} = ${id} for update`);
|
|
4231
|
+
const existing = await tx
|
|
4232
|
+
.select({
|
|
4233
|
+
id: issues.id,
|
|
4234
|
+
checkoutRunId: issues.checkoutRunId,
|
|
4235
|
+
executionRunId: issues.executionRunId,
|
|
4236
|
+
})
|
|
4237
|
+
.from(issues)
|
|
4238
|
+
.where(eq(issues.id, id))
|
|
4239
|
+
.then((rows) => rows[0] ?? null);
|
|
4240
|
+
if (!existing)
|
|
4241
|
+
return null;
|
|
4242
|
+
const patch = {
|
|
4243
|
+
checkoutRunId: null,
|
|
4244
|
+
executionRunId: null,
|
|
4245
|
+
executionAgentNameKey: null,
|
|
4246
|
+
executionLockedAt: null,
|
|
4247
|
+
updatedAt: new Date(),
|
|
4248
|
+
};
|
|
4249
|
+
if (options.clearAssignee) {
|
|
4250
|
+
patch.assigneeAgentId = null;
|
|
4251
|
+
}
|
|
4252
|
+
const updated = await tx
|
|
4253
|
+
.update(issues)
|
|
4254
|
+
.set(patch)
|
|
4255
|
+
.where(eq(issues.id, id))
|
|
4256
|
+
.returning()
|
|
4257
|
+
.then((rows) => rows[0] ?? null);
|
|
4258
|
+
if (!updated)
|
|
4259
|
+
return null;
|
|
4260
|
+
const [enriched] = await withIssueLabels(tx, [updated]);
|
|
4261
|
+
return {
|
|
4262
|
+
issue: enriched,
|
|
4263
|
+
previous: {
|
|
4264
|
+
checkoutRunId: existing.checkoutRunId,
|
|
4265
|
+
executionRunId: existing.executionRunId,
|
|
4266
|
+
},
|
|
4267
|
+
};
|
|
4268
|
+
}),
|
|
4269
|
+
listLabels: (squadId) => db.select().from(labels).where(eq(labels.squadId, squadId)).orderBy(asc(labels.name), asc(labels.id)),
|
|
4270
|
+
getLabelById: (id) => db
|
|
4271
|
+
.select()
|
|
4272
|
+
.from(labels)
|
|
4273
|
+
.where(eq(labels.id, id))
|
|
4274
|
+
.then((rows) => rows[0] ?? null),
|
|
4275
|
+
createLabel: async (squadId, data) => {
|
|
4276
|
+
const [created] = await db
|
|
4277
|
+
.insert(labels)
|
|
4278
|
+
.values({
|
|
4279
|
+
squadId,
|
|
4280
|
+
name: data.name.trim(),
|
|
4281
|
+
color: data.color,
|
|
4282
|
+
})
|
|
4283
|
+
.returning();
|
|
4284
|
+
return created;
|
|
4285
|
+
},
|
|
4286
|
+
deleteLabel: async (id) => db
|
|
4287
|
+
.delete(labels)
|
|
4288
|
+
.where(eq(labels.id, id))
|
|
4289
|
+
.returning()
|
|
4290
|
+
.then((rows) => rows[0] ?? null),
|
|
4291
|
+
listComments: async (issueId, opts) => {
|
|
4292
|
+
const order = opts?.order === "asc" ? "asc" : "desc";
|
|
4293
|
+
const afterCommentId = opts?.afterCommentId?.trim() || null;
|
|
4294
|
+
const limit = opts?.limit && opts.limit > 0
|
|
4295
|
+
? Math.min(Math.floor(opts.limit), MAX_ISSUE_COMMENT_PAGE_LIMIT)
|
|
4296
|
+
: null;
|
|
4297
|
+
const conditions = [eq(issueComments.issueId, issueId)];
|
|
4298
|
+
if (afterCommentId) {
|
|
4299
|
+
const anchor = await db
|
|
4300
|
+
.select({
|
|
4301
|
+
id: issueComments.id,
|
|
4302
|
+
createdAt: issueComments.createdAt,
|
|
4303
|
+
})
|
|
4304
|
+
.from(issueComments)
|
|
4305
|
+
.where(and(eq(issueComments.issueId, issueId), eq(issueComments.id, afterCommentId)))
|
|
4306
|
+
.then((rows) => rows[0] ?? null);
|
|
4307
|
+
if (!anchor)
|
|
4308
|
+
return [];
|
|
4309
|
+
const anchorCreatedAt = anchor.createdAt instanceof Date
|
|
4310
|
+
? anchor.createdAt
|
|
4311
|
+
: new Date(String(anchor.createdAt));
|
|
4312
|
+
conditions.push(order === "asc"
|
|
4313
|
+
? or(gt(issueComments.createdAt, anchorCreatedAt), and(eq(issueComments.createdAt, anchorCreatedAt), gt(issueComments.id, anchor.id)))
|
|
4314
|
+
: or(lt(issueComments.createdAt, anchorCreatedAt), and(eq(issueComments.createdAt, anchorCreatedAt), lt(issueComments.id, anchor.id))));
|
|
4315
|
+
}
|
|
4316
|
+
const query = db
|
|
4317
|
+
.select()
|
|
4318
|
+
.from(issueComments)
|
|
4319
|
+
.where(and(...conditions))
|
|
4320
|
+
.orderBy(order === "asc" ? asc(issueComments.createdAt) : desc(issueComments.createdAt), order === "asc" ? asc(issueComments.id) : desc(issueComments.id));
|
|
4321
|
+
const comments = limit ? await query.limit(limit) : await query;
|
|
4322
|
+
const { censorUsernameInLogs } = await instanceSettings.getGeneral();
|
|
4323
|
+
const enrichedComments = await enrichCommentsWithDerivedAgentAttribution(comments);
|
|
4324
|
+
return enrichedComments.map((comment) => redactIssueComment(comment, censorUsernameInLogs));
|
|
4325
|
+
},
|
|
4326
|
+
getCommentCursor: async (issueId) => {
|
|
4327
|
+
const [latest, countRow] = await Promise.all([
|
|
4328
|
+
db
|
|
4329
|
+
.select({
|
|
4330
|
+
latestCommentId: issueComments.id,
|
|
4331
|
+
latestCommentAt: issueComments.createdAt,
|
|
4332
|
+
})
|
|
4333
|
+
.from(issueComments)
|
|
4334
|
+
.where(eq(issueComments.issueId, issueId))
|
|
4335
|
+
.orderBy(desc(issueComments.createdAt), desc(issueComments.id))
|
|
4336
|
+
.limit(1)
|
|
4337
|
+
.then((rows) => rows[0] ?? null),
|
|
4338
|
+
db
|
|
4339
|
+
.select({
|
|
4340
|
+
totalComments: sql `count(*)::int`,
|
|
4341
|
+
})
|
|
4342
|
+
.from(issueComments)
|
|
4343
|
+
.where(eq(issueComments.issueId, issueId))
|
|
4344
|
+
.then((rows) => rows[0] ?? null),
|
|
4345
|
+
]);
|
|
4346
|
+
return {
|
|
4347
|
+
totalComments: Number(countRow?.totalComments ?? 0),
|
|
4348
|
+
latestCommentId: latest?.latestCommentId ?? null,
|
|
4349
|
+
latestCommentAt: latest?.latestCommentAt ?? null,
|
|
4350
|
+
};
|
|
4351
|
+
},
|
|
4352
|
+
getComment: async (commentId) => {
|
|
4353
|
+
const { censorUsernameInLogs } = await instanceSettings.getGeneral();
|
|
4354
|
+
const comment = await db
|
|
4355
|
+
.select()
|
|
4356
|
+
.from(issueComments)
|
|
4357
|
+
.where(eq(issueComments.id, commentId))
|
|
4358
|
+
.then((rows) => rows[0] ?? null);
|
|
4359
|
+
if (!comment)
|
|
4360
|
+
return null;
|
|
4361
|
+
const [enrichedComment] = await enrichCommentsWithDerivedAgentAttribution([comment]);
|
|
4362
|
+
return redactIssueComment(enrichedComment ?? comment, censorUsernameInLogs);
|
|
4363
|
+
},
|
|
4364
|
+
removeComment: async (commentId) => {
|
|
4365
|
+
const currentUserRedactionOptions = {
|
|
4366
|
+
enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs,
|
|
4367
|
+
};
|
|
4368
|
+
return db.transaction(async (tx) => {
|
|
4369
|
+
const [comment] = await tx
|
|
4370
|
+
.delete(issueComments)
|
|
4371
|
+
.where(eq(issueComments.id, commentId))
|
|
4372
|
+
.returning();
|
|
4373
|
+
if (!comment)
|
|
4374
|
+
return null;
|
|
4375
|
+
await tx
|
|
4376
|
+
.update(issues)
|
|
4377
|
+
.set({ updatedAt: new Date() })
|
|
4378
|
+
.where(eq(issues.id, comment.issueId));
|
|
4379
|
+
return redactIssueComment(comment, currentUserRedactionOptions.enabled);
|
|
4380
|
+
});
|
|
4381
|
+
},
|
|
4382
|
+
addComment: async (issueId, body, actor, options) => {
|
|
4383
|
+
const issue = await db
|
|
4384
|
+
.select({ squadId: issues.squadId })
|
|
4385
|
+
.from(issues)
|
|
4386
|
+
.where(eq(issues.id, issueId))
|
|
4387
|
+
.then((rows) => rows[0] ?? null);
|
|
4388
|
+
if (!issue)
|
|
4389
|
+
throw notFound("Issue not found");
|
|
4390
|
+
const currentUserRedactionOptions = {
|
|
4391
|
+
enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs,
|
|
4392
|
+
};
|
|
4393
|
+
const redactedBody = redactCurrentUserText(body, currentUserRedactionOptions);
|
|
4394
|
+
const authorType = issueCommentAuthorTypeSchema.parse(options?.authorType ?? (actor.agentId ? "agent" : actor.userId ? "user" : "system"));
|
|
4395
|
+
assertIssueCommentAuthorTypeAllowed(actor, authorType);
|
|
4396
|
+
const presentation = issueCommentPresentationSchema.nullable().parse(options?.presentation ?? null);
|
|
4397
|
+
const metadata = issueCommentMetadataSchema.nullable().parse(options?.metadata ?? null);
|
|
4398
|
+
const createdAt = options?.createdAt ? new Date(options.createdAt) : null;
|
|
4399
|
+
const [comment] = await db
|
|
4400
|
+
.insert(issueComments)
|
|
4401
|
+
.values({
|
|
4402
|
+
squadId: issue.squadId,
|
|
4403
|
+
issueId,
|
|
4404
|
+
authorAgentId: actor.agentId ?? null,
|
|
4405
|
+
authorUserId: actor.userId ?? null,
|
|
4406
|
+
authorType,
|
|
4407
|
+
createdByRunId: actor.runId ?? null,
|
|
4408
|
+
body: redactedBody,
|
|
4409
|
+
presentation,
|
|
4410
|
+
metadata,
|
|
4411
|
+
...(createdAt && !Number.isNaN(createdAt.getTime()) ? { createdAt } : {}),
|
|
4412
|
+
})
|
|
4413
|
+
.returning();
|
|
4414
|
+
// Update issue's updatedAt so comment activity is reflected in recency sorting
|
|
4415
|
+
await db
|
|
4416
|
+
.update(issues)
|
|
4417
|
+
.set({ updatedAt: new Date() })
|
|
4418
|
+
.where(eq(issues.id, issueId));
|
|
4419
|
+
return redactIssueComment(comment, currentUserRedactionOptions.enabled);
|
|
4420
|
+
},
|
|
4421
|
+
createAttachment: async (input) => {
|
|
4422
|
+
const issue = await db
|
|
4423
|
+
.select({ id: issues.id, squadId: issues.squadId })
|
|
4424
|
+
.from(issues)
|
|
4425
|
+
.where(eq(issues.id, input.issueId))
|
|
4426
|
+
.then((rows) => rows[0] ?? null);
|
|
4427
|
+
if (!issue)
|
|
4428
|
+
throw notFound("Issue not found");
|
|
4429
|
+
if (input.issueCommentId) {
|
|
4430
|
+
const comment = await db
|
|
4431
|
+
.select({ id: issueComments.id, squadId: issueComments.squadId, issueId: issueComments.issueId })
|
|
4432
|
+
.from(issueComments)
|
|
4433
|
+
.where(eq(issueComments.id, input.issueCommentId))
|
|
4434
|
+
.then((rows) => rows[0] ?? null);
|
|
4435
|
+
if (!comment)
|
|
4436
|
+
throw notFound("Issue comment not found");
|
|
4437
|
+
if (comment.squadId !== issue.squadId || comment.issueId !== issue.id) {
|
|
4438
|
+
throw unprocessable("Attachment comment must belong to same issue and squad");
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
return db.transaction(async (tx) => {
|
|
4442
|
+
const [asset] = await tx
|
|
4443
|
+
.insert(assets)
|
|
4444
|
+
.values({
|
|
4445
|
+
squadId: issue.squadId,
|
|
4446
|
+
provider: input.provider,
|
|
4447
|
+
objectKey: input.objectKey,
|
|
4448
|
+
contentType: input.contentType,
|
|
4449
|
+
byteSize: input.byteSize,
|
|
4450
|
+
sha256: input.sha256,
|
|
4451
|
+
originalFilename: input.originalFilename ?? null,
|
|
4452
|
+
createdByAgentId: input.createdByAgentId ?? null,
|
|
4453
|
+
createdByUserId: input.createdByUserId ?? null,
|
|
4454
|
+
})
|
|
4455
|
+
.returning();
|
|
4456
|
+
const [attachment] = await tx
|
|
4457
|
+
.insert(issueAttachments)
|
|
4458
|
+
.values({
|
|
4459
|
+
squadId: issue.squadId,
|
|
4460
|
+
issueId: issue.id,
|
|
4461
|
+
assetId: asset.id,
|
|
4462
|
+
issueCommentId: input.issueCommentId ?? null,
|
|
4463
|
+
})
|
|
4464
|
+
.returning();
|
|
4465
|
+
return {
|
|
4466
|
+
id: attachment.id,
|
|
4467
|
+
squadId: attachment.squadId,
|
|
4468
|
+
issueId: attachment.issueId,
|
|
4469
|
+
issueCommentId: attachment.issueCommentId,
|
|
4470
|
+
assetId: attachment.assetId,
|
|
4471
|
+
provider: asset.provider,
|
|
4472
|
+
objectKey: asset.objectKey,
|
|
4473
|
+
contentType: asset.contentType,
|
|
4474
|
+
byteSize: asset.byteSize,
|
|
4475
|
+
sha256: asset.sha256,
|
|
4476
|
+
originalFilename: asset.originalFilename,
|
|
4477
|
+
createdByAgentId: asset.createdByAgentId,
|
|
4478
|
+
createdByUserId: asset.createdByUserId,
|
|
4479
|
+
createdAt: attachment.createdAt,
|
|
4480
|
+
updatedAt: attachment.updatedAt,
|
|
4481
|
+
};
|
|
4482
|
+
});
|
|
4483
|
+
},
|
|
4484
|
+
listAttachments: async (issueId) => db
|
|
4485
|
+
.select({
|
|
4486
|
+
id: issueAttachments.id,
|
|
4487
|
+
squadId: issueAttachments.squadId,
|
|
4488
|
+
issueId: issueAttachments.issueId,
|
|
4489
|
+
issueCommentId: issueAttachments.issueCommentId,
|
|
4490
|
+
assetId: issueAttachments.assetId,
|
|
4491
|
+
provider: assets.provider,
|
|
4492
|
+
objectKey: assets.objectKey,
|
|
4493
|
+
contentType: assets.contentType,
|
|
4494
|
+
byteSize: assets.byteSize,
|
|
4495
|
+
sha256: assets.sha256,
|
|
4496
|
+
originalFilename: assets.originalFilename,
|
|
4497
|
+
createdByAgentId: assets.createdByAgentId,
|
|
4498
|
+
createdByUserId: assets.createdByUserId,
|
|
4499
|
+
createdAt: issueAttachments.createdAt,
|
|
4500
|
+
updatedAt: issueAttachments.updatedAt,
|
|
4501
|
+
})
|
|
4502
|
+
.from(issueAttachments)
|
|
4503
|
+
.innerJoin(assets, eq(issueAttachments.assetId, assets.id))
|
|
4504
|
+
.where(eq(issueAttachments.issueId, issueId))
|
|
4505
|
+
.orderBy(desc(issueAttachments.createdAt)),
|
|
4506
|
+
getAttachmentById: async (id) => db
|
|
4507
|
+
.select({
|
|
4508
|
+
id: issueAttachments.id,
|
|
4509
|
+
squadId: issueAttachments.squadId,
|
|
4510
|
+
issueId: issueAttachments.issueId,
|
|
4511
|
+
issueCommentId: issueAttachments.issueCommentId,
|
|
4512
|
+
assetId: issueAttachments.assetId,
|
|
4513
|
+
provider: assets.provider,
|
|
4514
|
+
objectKey: assets.objectKey,
|
|
4515
|
+
contentType: assets.contentType,
|
|
4516
|
+
byteSize: assets.byteSize,
|
|
4517
|
+
sha256: assets.sha256,
|
|
4518
|
+
originalFilename: assets.originalFilename,
|
|
4519
|
+
createdByAgentId: assets.createdByAgentId,
|
|
4520
|
+
createdByUserId: assets.createdByUserId,
|
|
4521
|
+
createdAt: issueAttachments.createdAt,
|
|
4522
|
+
updatedAt: issueAttachments.updatedAt,
|
|
4523
|
+
})
|
|
4524
|
+
.from(issueAttachments)
|
|
4525
|
+
.innerJoin(assets, eq(issueAttachments.assetId, assets.id))
|
|
4526
|
+
.where(eq(issueAttachments.id, id))
|
|
4527
|
+
.then((rows) => rows[0] ?? null),
|
|
4528
|
+
removeAttachment: async (id) => db.transaction(async (tx) => {
|
|
4529
|
+
const existing = await tx
|
|
4530
|
+
.select({
|
|
4531
|
+
id: issueAttachments.id,
|
|
4532
|
+
squadId: issueAttachments.squadId,
|
|
4533
|
+
issueId: issueAttachments.issueId,
|
|
4534
|
+
issueCommentId: issueAttachments.issueCommentId,
|
|
4535
|
+
assetId: issueAttachments.assetId,
|
|
4536
|
+
provider: assets.provider,
|
|
4537
|
+
objectKey: assets.objectKey,
|
|
4538
|
+
contentType: assets.contentType,
|
|
4539
|
+
byteSize: assets.byteSize,
|
|
4540
|
+
sha256: assets.sha256,
|
|
4541
|
+
originalFilename: assets.originalFilename,
|
|
4542
|
+
createdByAgentId: assets.createdByAgentId,
|
|
4543
|
+
createdByUserId: assets.createdByUserId,
|
|
4544
|
+
createdAt: issueAttachments.createdAt,
|
|
4545
|
+
updatedAt: issueAttachments.updatedAt,
|
|
4546
|
+
})
|
|
4547
|
+
.from(issueAttachments)
|
|
4548
|
+
.innerJoin(assets, eq(issueAttachments.assetId, assets.id))
|
|
4549
|
+
.where(eq(issueAttachments.id, id))
|
|
4550
|
+
.then((rows) => rows[0] ?? null);
|
|
4551
|
+
if (!existing)
|
|
4552
|
+
return null;
|
|
4553
|
+
await tx.delete(issueAttachments).where(eq(issueAttachments.id, id));
|
|
4554
|
+
await tx.delete(assets).where(eq(assets.id, existing.assetId));
|
|
4555
|
+
return existing;
|
|
4556
|
+
}),
|
|
4557
|
+
findMentionedAgents: async (squadId, body) => {
|
|
4558
|
+
const re = /\B@([^\s@,!?.]+)/g;
|
|
4559
|
+
const tokens = new Set();
|
|
4560
|
+
let m;
|
|
4561
|
+
while ((m = re.exec(body)) !== null) {
|
|
4562
|
+
const normalized = normalizeAgentMentionToken(m[1]);
|
|
4563
|
+
if (normalized)
|
|
4564
|
+
tokens.add(normalized.toLowerCase());
|
|
4565
|
+
}
|
|
4566
|
+
const explicitAgentMentionIds = extractAgentMentionIds(body);
|
|
4567
|
+
if (tokens.size === 0 && explicitAgentMentionIds.length === 0)
|
|
4568
|
+
return [];
|
|
4569
|
+
const rows = await db.select({ id: agents.id, name: agents.name })
|
|
4570
|
+
.from(agents).where(eq(agents.squadId, squadId));
|
|
4571
|
+
const resolved = new Set(explicitAgentMentionIds);
|
|
4572
|
+
for (const agent of rows) {
|
|
4573
|
+
if (tokens.has(agent.name.toLowerCase())) {
|
|
4574
|
+
resolved.add(agent.id);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
return [...resolved];
|
|
4578
|
+
},
|
|
4579
|
+
findMentionedProjectIds: async (issueId, opts) => {
|
|
4580
|
+
const issue = await db
|
|
4581
|
+
.select({
|
|
4582
|
+
squadId: issues.squadId,
|
|
4583
|
+
title: issues.title,
|
|
4584
|
+
description: issues.description,
|
|
4585
|
+
})
|
|
4586
|
+
.from(issues)
|
|
4587
|
+
.where(eq(issues.id, issueId))
|
|
4588
|
+
.then((rows) => rows[0] ?? null);
|
|
4589
|
+
if (!issue)
|
|
4590
|
+
return [];
|
|
4591
|
+
const mentionedIds = new Set();
|
|
4592
|
+
for (const source of [issue.title, issue.description ?? ""]) {
|
|
4593
|
+
for (const projectId of extractProjectMentionIds(source)) {
|
|
4594
|
+
mentionedIds.add(projectId);
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
if (opts?.includeCommentBodies !== false) {
|
|
4598
|
+
const comments = await db
|
|
4599
|
+
.select({ body: issueComments.body })
|
|
4600
|
+
.from(issueComments)
|
|
4601
|
+
.where(eq(issueComments.issueId, issueId));
|
|
4602
|
+
for (const comment of comments) {
|
|
4603
|
+
for (const projectId of extractProjectMentionIds(comment.body)) {
|
|
4604
|
+
mentionedIds.add(projectId);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
if (mentionedIds.size === 0)
|
|
4609
|
+
return [];
|
|
4610
|
+
const rows = await db
|
|
4611
|
+
.select({ id: projects.id })
|
|
4612
|
+
.from(projects)
|
|
4613
|
+
.where(and(eq(projects.squadId, issue.squadId), inArray(projects.id, [...mentionedIds])));
|
|
4614
|
+
const valid = new Set(rows.map((row) => row.id));
|
|
4615
|
+
return [...mentionedIds].filter((projectId) => valid.has(projectId));
|
|
4616
|
+
},
|
|
4617
|
+
getAncestors: async (issueId) => {
|
|
4618
|
+
const raw = [];
|
|
4619
|
+
const visited = new Set([issueId]);
|
|
4620
|
+
const start = await db.select().from(issues).where(eq(issues.id, issueId)).then(r => r[0] ?? null);
|
|
4621
|
+
let currentId = start?.parentId ?? null;
|
|
4622
|
+
while (currentId && !visited.has(currentId) && raw.length < 50) {
|
|
4623
|
+
visited.add(currentId);
|
|
4624
|
+
const parent = await db.select({
|
|
4625
|
+
id: issues.id, identifier: issues.identifier, title: issues.title, description: issues.description,
|
|
4626
|
+
status: issues.status, priority: issues.priority,
|
|
4627
|
+
assigneeAgentId: issues.assigneeAgentId, projectId: issues.projectId,
|
|
4628
|
+
goalId: issues.goalId, parentId: issues.parentId,
|
|
4629
|
+
}).from(issues).where(eq(issues.id, currentId)).then(r => r[0] ?? null);
|
|
4630
|
+
if (!parent)
|
|
4631
|
+
break;
|
|
4632
|
+
raw.push({
|
|
4633
|
+
id: parent.id, identifier: parent.identifier ?? null, title: parent.title, description: parent.description ?? null,
|
|
4634
|
+
status: parent.status, priority: parent.priority,
|
|
4635
|
+
assigneeAgentId: parent.assigneeAgentId ?? null,
|
|
4636
|
+
projectId: parent.projectId ?? null, goalId: parent.goalId ?? null,
|
|
4637
|
+
});
|
|
4638
|
+
currentId = parent.parentId ?? null;
|
|
4639
|
+
}
|
|
4640
|
+
// Batch-fetch referenced projects and goals
|
|
4641
|
+
const projectIds = [...new Set(raw.map(a => a.projectId).filter((id) => id != null))];
|
|
4642
|
+
const goalIds = [...new Set(raw.map(a => a.goalId).filter((id) => id != null))];
|
|
4643
|
+
const projectMap = new Map();
|
|
4644
|
+
const goalMap = new Map();
|
|
4645
|
+
if (projectIds.length > 0) {
|
|
4646
|
+
const workspaceRows = await db
|
|
4647
|
+
.select()
|
|
4648
|
+
.from(projectWorkspaces)
|
|
4649
|
+
.where(inArray(projectWorkspaces.projectId, projectIds))
|
|
4650
|
+
.orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id));
|
|
4651
|
+
const workspaceMap = new Map();
|
|
4652
|
+
for (const workspace of workspaceRows) {
|
|
4653
|
+
const existing = workspaceMap.get(workspace.projectId);
|
|
4654
|
+
if (existing)
|
|
4655
|
+
existing.push(workspace);
|
|
4656
|
+
else
|
|
4657
|
+
workspaceMap.set(workspace.projectId, [workspace]);
|
|
4658
|
+
}
|
|
4659
|
+
const rows = await db.select({
|
|
4660
|
+
id: projects.id, name: projects.name, description: projects.description,
|
|
4661
|
+
status: projects.status, goalId: projects.goalId,
|
|
4662
|
+
}).from(projects).where(inArray(projects.id, projectIds));
|
|
4663
|
+
for (const r of rows) {
|
|
4664
|
+
const projectWorkspaceRows = workspaceMap.get(r.id) ?? [];
|
|
4665
|
+
const workspaces = projectWorkspaceRows.map((workspace) => ({
|
|
4666
|
+
id: workspace.id,
|
|
4667
|
+
squadId: workspace.squadId,
|
|
4668
|
+
projectId: workspace.projectId,
|
|
4669
|
+
name: workspace.name,
|
|
4670
|
+
cwd: workspace.cwd,
|
|
4671
|
+
repoUrl: workspace.repoUrl ?? null,
|
|
4672
|
+
repoRef: workspace.repoRef ?? null,
|
|
4673
|
+
metadata: workspace.metadata ?? null,
|
|
4674
|
+
isPrimary: workspace.isPrimary,
|
|
4675
|
+
createdAt: workspace.createdAt,
|
|
4676
|
+
updatedAt: workspace.updatedAt,
|
|
4677
|
+
}));
|
|
4678
|
+
const primaryWorkspace = workspaces.find((workspace) => workspace.isPrimary) ?? workspaces[0] ?? null;
|
|
4679
|
+
projectMap.set(r.id, {
|
|
4680
|
+
...r,
|
|
4681
|
+
workspaces,
|
|
4682
|
+
primaryWorkspace,
|
|
4683
|
+
});
|
|
4684
|
+
// Also collect goalIds from projects
|
|
4685
|
+
if (r.goalId && !goalIds.includes(r.goalId))
|
|
4686
|
+
goalIds.push(r.goalId);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (goalIds.length > 0) {
|
|
4690
|
+
const rows = await db.select({
|
|
4691
|
+
id: goals.id, title: goals.title, description: goals.description,
|
|
4692
|
+
level: goals.level, status: goals.status,
|
|
4693
|
+
}).from(goals).where(inArray(goals.id, goalIds));
|
|
4694
|
+
for (const r of rows)
|
|
4695
|
+
goalMap.set(r.id, r);
|
|
4696
|
+
}
|
|
4697
|
+
return raw.map(a => ({
|
|
4698
|
+
...a,
|
|
4699
|
+
project: a.projectId ? projectMap.get(a.projectId) ?? null : null,
|
|
4700
|
+
goal: a.goalId ? goalMap.get(a.goalId) ?? null : null,
|
|
4701
|
+
}));
|
|
4702
|
+
},
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
//# sourceMappingURL=issues.js.map
|