@penclipai/server 2026.704.0 → 2026.714.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/process/execute.d.ts.map +1 -1
- package/dist/adapters/process/execute.js +1 -0
- package/dist/adapters/process/execute.js.map +1 -1
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/registry.js +79 -51
- package/dist/adapters/registry.js.map +1 -1
- package/dist/adapters/utils.d.ts +5 -0
- package/dist/adapters/utils.d.ts.map +1 -1
- package/dist/adapters/utils.js.map +1 -1
- package/dist/agent-auth-jwt.d.ts +5 -1
- package/dist/agent-auth-jwt.d.ts.map +1 -1
- package/dist/agent-auth-jwt.js +72 -20
- package/dist/agent-auth-jwt.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +10 -0
- package/dist/app.js.map +1 -1
- package/dist/built-ins/agents/reflection-coach/AGENTS.md +47 -0
- package/dist/built-ins/agents/reflection-coach/routines/recent-agent-reflection.md +77 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +208 -116
- package/dist/index.js.map +1 -1
- package/dist/middleware/api-compression.d.ts +8 -0
- package/dist/middleware/api-compression.d.ts.map +1 -0
- package/dist/middleware/api-compression.js +207 -0
- package/dist/middleware/api-compression.js.map +1 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +130 -3
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/error-handler.d.ts.map +1 -1
- package/dist/middleware/error-handler.js +27 -0
- package/dist/middleware/error-handler.js.map +1 -1
- package/dist/onboarding-assets/default/AGENTS.md +1 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +11 -0
- package/dist/redaction.js.map +1 -1
- package/dist/routes/access.d.ts.map +1 -1
- package/dist/routes/access.js +4 -3
- package/dist/routes/access.js.map +1 -1
- package/dist/routes/adapters.d.ts.map +1 -1
- package/dist/routes/adapters.js +2 -0
- package/dist/routes/adapters.js.map +1 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +222 -87
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/attention.d.ts +3 -0
- package/dist/routes/attention.d.ts.map +1 -0
- package/dist/routes/attention.js +24 -0
- package/dist/routes/attention.js.map +1 -0
- package/dist/routes/authz.d.ts.map +1 -1
- package/dist/routes/authz.js +29 -1
- package/dist/routes/authz.js.map +1 -1
- package/dist/routes/board-chat.d.ts.map +1 -1
- package/dist/routes/board-chat.js +4 -1
- package/dist/routes/board-chat.js.map +1 -1
- package/dist/routes/built-in-agents.d.ts +3 -0
- package/dist/routes/built-in-agents.d.ts.map +1 -0
- package/dist/routes/built-in-agents.js +267 -0
- package/dist/routes/built-in-agents.js.map +1 -0
- package/dist/routes/cases.d.ts +46 -0
- package/dist/routes/cases.d.ts.map +1 -0
- package/dist/routes/cases.js +1395 -0
- package/dist/routes/cases.js.map +1 -0
- package/dist/routes/companies.d.ts.map +1 -1
- package/dist/routes/companies.js +4 -1
- package/dist/routes/companies.js.map +1 -1
- package/dist/routes/company-skills.d.ts.map +1 -1
- package/dist/routes/company-skills.js +453 -33
- package/dist/routes/company-skills.js.map +1 -1
- package/dist/routes/environments.d.ts.map +1 -1
- package/dist/routes/environments.js +71 -8
- package/dist/routes/environments.js.map +1 -1
- package/dist/routes/execution-workspaces.d.ts.map +1 -1
- package/dist/routes/execution-workspaces.js +89 -3
- package/dist/routes/execution-workspaces.js.map +1 -1
- package/dist/routes/health.d.ts +2 -0
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +30 -0
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/inbox-dismissals.d.ts.map +1 -1
- package/dist/routes/inbox-dismissals.js +80 -19
- package/dist/routes/inbox-dismissals.js.map +1 -1
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/issues.d.ts +25 -0
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +1455 -46
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/openapi.d.ts.map +1 -1
- package/dist/routes/openapi.js +448 -11
- package/dist/routes/openapi.js.map +1 -1
- package/dist/routes/pipelines.js +2 -2
- package/dist/routes/pipelines.js.map +1 -1
- package/dist/routes/resource-memberships.d.ts.map +1 -1
- package/dist/routes/resource-memberships.js +13 -5
- package/dist/routes/resource-memberships.js.map +1 -1
- package/dist/routes/secrets.d.ts.map +1 -1
- package/dist/routes/secrets.js +261 -1
- package/dist/routes/secrets.js.map +1 -1
- package/dist/routes/sidebar-badges.d.ts.map +1 -1
- package/dist/routes/sidebar-badges.js +18 -2
- package/dist/routes/sidebar-badges.js.map +1 -1
- package/dist/routes/user-profiles.d.ts.map +1 -1
- package/dist/routes/user-profiles.js +5 -4
- package/dist/routes/user-profiles.js.map +1 -1
- package/dist/server-info.d.ts +2 -0
- package/dist/server-info.d.ts.map +1 -1
- package/dist/server-info.js +20 -5
- package/dist/server-info.js.map +1 -1
- package/dist/services/access.d.ts +4 -4
- package/dist/services/access.d.ts.map +1 -1
- package/dist/services/activity.d.ts +3 -3
- package/dist/services/activity.d.ts.map +1 -1
- package/dist/services/activity.js +5 -4
- package/dist/services/activity.js.map +1 -1
- package/dist/services/adapter-registry-bootstrap.reconcile.test.js +1 -0
- package/dist/services/adapter-registry-bootstrap.reconcile.test.js.map +1 -1
- package/dist/services/agent-secret-bindings.d.ts +15 -0
- package/dist/services/agent-secret-bindings.d.ts.map +1 -1
- package/dist/services/agent-secret-bindings.js +43 -0
- package/dist/services/agent-secret-bindings.js.map +1 -1
- package/dist/services/agents.d.ts +194 -133
- package/dist/services/agents.d.ts.map +1 -1
- package/dist/services/agents.js +107 -4
- package/dist/services/agents.js.map +1 -1
- package/dist/services/approvals.d.ts +10 -10
- package/dist/services/approvals.d.ts.map +1 -1
- package/dist/services/approvals.js +9 -1
- package/dist/services/approvals.js.map +1 -1
- package/dist/services/assets.d.ts +5 -5
- package/dist/services/attention.d.ts +11 -0
- package/dist/services/attention.d.ts.map +1 -0
- package/dist/services/attention.js +1024 -0
- package/dist/services/attention.js.map +1 -0
- package/dist/services/authorization.d.ts +15 -2
- package/dist/services/authorization.d.ts.map +1 -1
- package/dist/services/authorization.js +324 -8
- package/dist/services/authorization.js.map +1 -1
- package/dist/services/board-auth.d.ts +2 -2
- package/dist/services/budgets.d.ts.map +1 -1
- package/dist/services/budgets.js +8 -7
- package/dist/services/budgets.js.map +1 -1
- package/dist/services/built-in-agent-metadata.d.ts +9 -0
- package/dist/services/built-in-agent-metadata.d.ts.map +1 -0
- package/dist/services/built-in-agent-metadata.js +39 -0
- package/dist/services/built-in-agent-metadata.js.map +1 -0
- package/dist/services/built-in-agents.d.ts +394 -0
- package/dist/services/built-in-agents.d.ts.map +1 -0
- package/dist/services/built-in-agents.js +1426 -0
- package/dist/services/built-in-agents.js.map +1 -0
- package/dist/services/change-consent-gate.d.ts +18 -0
- package/dist/services/change-consent-gate.d.ts.map +1 -0
- package/dist/services/change-consent-gate.js +170 -0
- package/dist/services/change-consent-gate.js.map +1 -0
- package/dist/services/companies.d.ts +14 -8
- package/dist/services/companies.d.ts.map +1 -1
- package/dist/services/companies.js +6 -1
- package/dist/services/companies.js.map +1 -1
- package/dist/services/company-artifacts.d.ts +4 -1
- package/dist/services/company-artifacts.d.ts.map +1 -1
- package/dist/services/company-artifacts.js +9 -1
- package/dist/services/company-artifacts.js.map +1 -1
- package/dist/services/company-member-roles.d.ts.map +1 -1
- package/dist/services/company-member-roles.js +2 -0
- package/dist/services/company-member-roles.js.map +1 -1
- package/dist/services/company-portability.d.ts +1 -1
- package/dist/services/company-portability.d.ts.map +1 -1
- package/dist/services/company-portability.js +113 -8
- package/dist/services/company-portability.js.map +1 -1
- package/dist/services/company-search.d.ts.map +1 -1
- package/dist/services/company-search.js +843 -294
- package/dist/services/company-search.js.map +1 -1
- package/dist/services/company-skills.d.ts +64 -3
- package/dist/services/company-skills.d.ts.map +1 -1
- package/dist/services/company-skills.js +1205 -20
- package/dist/services/company-skills.js.map +1 -1
- package/dist/services/costs.d.ts +9 -8
- package/dist/services/costs.d.ts.map +1 -1
- package/dist/services/costs.js +9 -2
- package/dist/services/costs.js.map +1 -1
- package/dist/services/dashboard.d.ts +2 -0
- package/dist/services/dashboard.d.ts.map +1 -1
- package/dist/services/dashboard.js +60 -17
- package/dist/services/dashboard.js.map +1 -1
- package/dist/services/database-backup-health.d.ts +34 -0
- package/dist/services/database-backup-health.d.ts.map +1 -0
- package/dist/services/database-backup-health.js +104 -0
- package/dist/services/database-backup-health.js.map +1 -0
- package/dist/services/document-annotations.d.ts +169 -1
- package/dist/services/document-annotations.d.ts.map +1 -1
- package/dist/services/document-annotations.js +272 -1
- package/dist/services/document-annotations.js.map +1 -1
- package/dist/services/documents.d.ts +368 -0
- package/dist/services/documents.d.ts.map +1 -1
- package/dist/services/documents.js +2 -2
- package/dist/services/documents.js.map +1 -1
- package/dist/services/environment-config.d.ts.map +1 -1
- package/dist/services/environment-config.js +6 -0
- package/dist/services/environment-config.js.map +1 -1
- package/dist/services/environment-custom-image-runtime.d.ts +28 -0
- package/dist/services/environment-custom-image-runtime.d.ts.map +1 -1
- package/dist/services/environment-custom-image-runtime.js +106 -7
- package/dist/services/environment-custom-image-runtime.js.map +1 -1
- package/dist/services/environment-custom-images.d.ts +31 -1
- package/dist/services/environment-custom-images.d.ts.map +1 -1
- package/dist/services/environment-custom-images.js +110 -4
- package/dist/services/environment-custom-images.js.map +1 -1
- package/dist/services/environment-execution-target.d.ts.map +1 -1
- package/dist/services/environment-execution-target.js +1 -3
- package/dist/services/environment-execution-target.js.map +1 -1
- package/dist/services/environment-probe.d.ts +1 -0
- package/dist/services/environment-probe.d.ts.map +1 -1
- package/dist/services/environment-probe.js +99 -5
- package/dist/services/environment-probe.js.map +1 -1
- package/dist/services/environments.d.ts +3 -1
- package/dist/services/environments.d.ts.map +1 -1
- package/dist/services/environments.js +98 -3
- package/dist/services/environments.js.map +1 -1
- package/dist/services/execution-workspace-policy.d.ts +29 -3
- package/dist/services/execution-workspace-policy.d.ts.map +1 -1
- package/dist/services/execution-workspace-policy.js +44 -1
- package/dist/services/execution-workspace-policy.js.map +1 -1
- package/dist/services/execution-workspaces.d.ts +64 -1
- package/dist/services/execution-workspaces.d.ts.map +1 -1
- package/dist/services/execution-workspaces.js +634 -3
- package/dist/services/execution-workspaces.js.map +1 -1
- package/dist/services/external-objects.d.ts +26 -26
- package/dist/services/feedback.d.ts +2 -2
- package/dist/services/finance.d.ts +5 -5
- package/dist/services/goals.d.ts +8 -8
- package/dist/services/heartbeat-stop-metadata.d.ts +2 -2
- package/dist/services/heartbeat-stop-metadata.d.ts.map +1 -1
- package/dist/services/heartbeat-stop-metadata.js +4 -0
- package/dist/services/heartbeat-stop-metadata.js.map +1 -1
- package/dist/services/heartbeat-stop-metadata.test.js +9 -0
- package/dist/services/heartbeat-stop-metadata.test.js.map +1 -1
- package/dist/services/heartbeat.d.ts +133 -28
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +1963 -239
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/inbox-dismissals.d.ts +26 -0
- package/dist/services/inbox-dismissals.d.ts.map +1 -1
- package/dist/services/inbox-dismissals.js +33 -18
- package/dist/services/inbox-dismissals.js.map +1 -1
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -1
- package/dist/services/index.js.map +1 -1
- package/dist/services/instance-settings.d.ts +25 -1
- package/dist/services/instance-settings.d.ts.map +1 -1
- package/dist/services/instance-settings.js +103 -5
- package/dist/services/instance-settings.js.map +1 -1
- package/dist/services/issue-continuation-summary.js +1 -1
- package/dist/services/issue-continuation-summary.js.map +1 -1
- package/dist/services/issue-recovery-actions.d.ts +3 -0
- package/dist/services/issue-recovery-actions.d.ts.map +1 -1
- package/dist/services/issue-recovery-actions.js +9 -0
- package/dist/services/issue-recovery-actions.js.map +1 -1
- package/dist/services/issue-rewake-throttle.d.ts +92 -0
- package/dist/services/issue-rewake-throttle.d.ts.map +1 -0
- package/dist/services/issue-rewake-throttle.js +139 -0
- package/dist/services/issue-rewake-throttle.js.map +1 -0
- package/dist/services/issue-thread-interactions.d.ts +8 -1
- package/dist/services/issue-thread-interactions.d.ts.map +1 -1
- package/dist/services/issue-thread-interactions.js +231 -16
- package/dist/services/issue-thread-interactions.js.map +1 -1
- package/dist/services/issue-visibility.d.ts +4 -0
- package/dist/services/issue-visibility.d.ts.map +1 -0
- package/dist/services/issue-visibility.js +9 -0
- package/dist/services/issue-visibility.js.map +1 -0
- package/dist/services/issues.d.ts +225 -105
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +540 -18
- package/dist/services/issues.js.map +1 -1
- package/dist/services/local-service-supervisor.d.ts +3 -0
- package/dist/services/local-service-supervisor.d.ts.map +1 -1
- package/dist/services/local-service-supervisor.js +51 -0
- package/dist/services/local-service-supervisor.js.map +1 -1
- package/dist/services/pipeline-case-outputs.d.ts.map +1 -1
- package/dist/services/pipeline-case-outputs.js +2 -1
- package/dist/services/pipeline-case-outputs.js.map +1 -1
- package/dist/services/pipelines-aggregation.d.ts.map +1 -1
- package/dist/services/pipelines-aggregation.js +3 -2
- package/dist/services/pipelines-aggregation.js.map +1 -1
- package/dist/services/pipelines.d.ts +165 -165
- package/dist/services/pipelines.d.ts.map +1 -1
- package/dist/services/pipelines.js +6 -4
- package/dist/services/pipelines.js.map +1 -1
- package/dist/services/plugin-database.d.ts +2 -2
- package/dist/services/plugin-environment-driver.d.ts +1 -1
- package/dist/services/plugin-host-services.d.ts.map +1 -1
- package/dist/services/plugin-host-services.js +3 -1
- package/dist/services/plugin-host-services.js.map +1 -1
- package/dist/services/plugin-managed-routines.d.ts +1 -0
- package/dist/services/plugin-managed-routines.d.ts.map +1 -1
- package/dist/services/plugin-registry.d.ts +21 -21
- package/dist/services/productivity-review.d.ts +1 -0
- package/dist/services/productivity-review.d.ts.map +1 -1
- package/dist/services/productivity-review.js +6 -5
- package/dist/services/productivity-review.js.map +1 -1
- package/dist/services/projects.d.ts +9 -9
- package/dist/services/recovery/service.d.ts +82 -54
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +200 -35
- package/dist/services/recovery/service.js.map +1 -1
- package/dist/services/recovery/successful-run-handoff.d.ts +1 -0
- package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
- package/dist/services/recovery/successful-run-handoff.js +2 -0
- package/dist/services/recovery/successful-run-handoff.js.map +1 -1
- package/dist/services/recovery/successful-run-handoff.test.js +20 -0
- package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
- package/dist/services/resource-memberships.d.ts +13 -10
- package/dist/services/resource-memberships.d.ts.map +1 -1
- package/dist/services/resource-memberships.js +89 -20
- package/dist/services/resource-memberships.js.map +1 -1
- package/dist/services/responsible-user-denial-run-outcomes.d.ts +60 -0
- package/dist/services/responsible-user-denial-run-outcomes.d.ts.map +1 -0
- package/dist/services/responsible-user-denial-run-outcomes.js +56 -0
- package/dist/services/responsible-user-denial-run-outcomes.js.map +1 -0
- package/dist/services/responsible-user-denial-run-outcomes.test.d.ts +2 -0
- package/dist/services/responsible-user-denial-run-outcomes.test.d.ts.map +1 -0
- package/dist/services/responsible-user-denial-run-outcomes.test.js +75 -0
- package/dist/services/responsible-user-denial-run-outcomes.test.js.map +1 -0
- package/dist/services/routines.d.ts +30 -8
- package/dist/services/routines.d.ts.map +1 -1
- package/dist/services/routines.js +241 -18
- package/dist/services/routines.js.map +1 -1
- package/dist/services/run-liveness.d.ts.map +1 -1
- package/dist/services/run-liveness.js +21 -0
- package/dist/services/run-liveness.js.map +1 -1
- package/dist/services/run-log-store.d.ts +1 -0
- package/dist/services/run-log-store.d.ts.map +1 -1
- package/dist/services/run-log-store.js +4 -0
- package/dist/services/run-log-store.js.map +1 -1
- package/dist/services/run-scratch.d.ts +42 -0
- package/dist/services/run-scratch.d.ts.map +1 -0
- package/dist/services/run-scratch.js +111 -0
- package/dist/services/run-scratch.js.map +1 -0
- package/dist/services/run-scratch.test.d.ts +2 -0
- package/dist/services/run-scratch.test.d.ts.map +1 -0
- package/dist/services/run-scratch.test.js +98 -0
- package/dist/services/run-scratch.test.js.map +1 -0
- package/dist/services/secrets.d.ts +1261 -64
- package/dist/services/secrets.d.ts.map +1 -1
- package/dist/services/secrets.js +1089 -96
- package/dist/services/secrets.js.map +1 -1
- package/dist/services/task-watchdogs.d.ts +1 -0
- package/dist/services/task-watchdogs.d.ts.map +1 -1
- package/dist/services/task-watchdogs.js +78 -9
- package/dist/services/task-watchdogs.js.map +1 -1
- package/dist/services/work-timeline.d.ts.map +1 -1
- package/dist/services/work-timeline.js +34 -2
- package/dist/services/work-timeline.js.map +1 -1
- package/dist/services/workspace-runtime-read-model.d.ts +30 -29
- package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
- package/dist/services/workspace-runtime-read-model.js.map +1 -1
- package/dist/services/workspace-runtime.d.ts +59 -13
- package/dist/services/workspace-runtime.d.ts.map +1 -1
- package/dist/services/workspace-runtime.js +1121 -102
- package/dist/services/workspace-runtime.js.map +1 -1
- package/dist/skills-catalog/catalog/bundled/paperclip-operations/reflection-coach/SKILL.md +202 -0
- package/dist/skills-catalog/catalog/bundled/product/paperclip-capsules/SKILL.md +1 -1
- package/dist/skills-catalog/catalog/bundled/product/wireframe/SKILL.md +1 -1
- package/dist/skills-catalog/catalog/optional/content/release-announcement/SKILL.md +90 -0
- package/dist/skills-catalog/catalog/optional/finance/ramp/SKILL.md +98 -0
- package/dist/skills-catalog/generated/catalog.json +84 -12
- package/dist/ui-branding.d.ts +7 -0
- package/dist/ui-branding.d.ts.map +1 -1
- package/dist/ui-branding.js +8 -2
- package/dist/ui-branding.js.map +1 -1
- package/dist/version.d.ts +14 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +114 -3
- package/dist/version.js.map +1 -1
- package/package.json +19 -20
- package/skills/paperclip/SKILL.md +45 -10
- package/skills/paperclip/references/api-reference.md +246 -1
- package/skills/paperclip/references/cases.md +295 -0
- package/skills/paperclip/references/company-skills.md +1 -1
- package/skills/paperclip-board/SKILL.md +3 -4
- package/skills/paperclip-converting-plans-to-tasks/SKILL.md +3 -7
- package/skills/para-memory-files/SKILL.md +3 -7
- package/ui-dist/assets/{abnfDiagram-VRR7QNED-BsFz2IiG.js → abnfDiagram-VRR7QNED-Dtgfy1PK.js} +1 -1
- package/ui-dist/assets/{arc-HZhAwbOs.js → arc-BGdvNvs8.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-BteMjuOn.js → architectureDiagram-ZJ3FMSHR-CK19SQDx.js} +1 -1
- package/ui-dist/assets/{blockDiagram-677ZJIJ3-Iq66uZjf.js → blockDiagram-677ZJIJ3-Kh4N3XG4.js} +1 -1
- package/ui-dist/assets/{browser-ponyfill-BBsuyZJt.js → browser-ponyfill-B3hXtEFW.js} +1 -1
- package/ui-dist/assets/{c4Diagram-LMCZKHZV-_nlglj7I.js → c4Diagram-LMCZKHZV-CBHpGrgU.js} +1 -1
- package/ui-dist/assets/channel-4fjB8e8N.js +1 -0
- package/ui-dist/assets/{chunk-2Q5K7J3B-Cvz7WwSQ.js → chunk-2Q5K7J3B-BdHbqi7k.js} +1 -1
- package/ui-dist/assets/{chunk-32BRIVSS-LLubniCS.js → chunk-32BRIVSS-DxtzZzpt.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-BOst5hcr.js → chunk-5VM5RSS4-DMKxBbgu.js} +1 -1
- package/ui-dist/assets/{chunk-EX3LRPZG-D5WNShVm.js → chunk-EX3LRPZG-BaIOPi4v.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-CA07x4av.js → chunk-JWPE2WC7-DGo3MPmr.js} +1 -1
- package/ui-dist/assets/{chunk-MOJQB5TN-Clfi83rK.js → chunk-MOJQB5TN-BEhQg0uD.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-BhZ6p7YJ.js → chunk-RYQCIY6F-HPJ6zRpJ.js} +1 -1
- package/ui-dist/assets/{chunk-V7JOEXUC-CdexJGoV.js → chunk-V7JOEXUC-Bl_6cAJD.js} +1 -1
- package/ui-dist/assets/{chunk-VR4S4FIN-8vjmBhr1.js → chunk-VR4S4FIN-KMjgGCsw.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-CqmCaplj.js → chunk-XXDRQBXY-oWGAIKjr.js} +1 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-CTXxO5l9.js +1 -0
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CTXxO5l9.js +1 -0
- package/ui-dist/assets/{cose-bilkent-JH36ORCC-CY-zAygY.js → cose-bilkent-JH36ORCC-Ckqse1pH.js} +1 -1
- package/ui-dist/assets/{cynefin-VYW2F7L2-BQSQ1HKr.js → cynefin-VYW2F7L2-DfZ8Hzqi.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-NJCuFrZL.js → cynefinDiagram-TSTJHNR4-BkDQo9oi.js} +1 -1
- package/ui-dist/assets/{dagre-VKFMJZFB-DAUPIc8g.js → dagre-VKFMJZFB-ClghXQ6E.js} +1 -1
- package/ui-dist/assets/{diagram-FQU43EPY-DvMwJlCi.js → diagram-FQU43EPY-D5a0CwpV.js} +1 -1
- package/ui-dist/assets/{diagram-G47NLZAW-DPY0Jz_y.js → diagram-G47NLZAW-hVjYR_xl.js} +1 -1
- package/ui-dist/assets/{diagram-NH7WQ7WH-BpopeEhp.js → diagram-NH7WQ7WH-BlDPgD5Z.js} +1 -1
- package/ui-dist/assets/{diagram-OA4YK3LP-Qpu05z9X.js → diagram-OA4YK3LP-D0iZKIS3.js} +1 -1
- package/ui-dist/assets/{diagram-WEI45ONY-DHnvw9XU.js → diagram-WEI45ONY-m6hrfD0J.js} +1 -1
- package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-D61S54_7.js → ebnfDiagram-CCIWWBDH-D4SQYLod.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q63AITRT-pplAN_x2.js → erDiagram-Q63AITRT-Byzb6-I5.js} +1 -1
- package/ui-dist/assets/{flowDiagram-23GEKE2U-BaBcbyDq.js → flowDiagram-23GEKE2U-CaTUkFO6.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-NO4QXBWP-BOJSII2b.js → ganttDiagram-NO4QXBWP-DmdMQ3ES.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-aYAaO4a2.js → gitGraphDiagram-IHSO6WYX-Bt-1g2CS.js} +1 -1
- package/ui-dist/assets/{index-CZxYajxF.js → index-2Dk9yO0W.js} +1 -1
- package/ui-dist/assets/{index-B8H0SHw1.js → index-8jLpgFNU.js} +1 -1
- package/ui-dist/assets/{index-3EKATXgc.js → index-AP_iimxs.js} +1 -1
- package/ui-dist/assets/{index-C2JgdjPa.js → index-BNJasQY9.js} +1 -1
- package/ui-dist/assets/{index-BaiskB3o.js → index-BO-aNmyX.js} +1 -1
- package/ui-dist/assets/{index-B5YenxAQ.js → index-BPCzCn1Y.js} +1 -1
- package/ui-dist/assets/{index-Da2kqT1M.js → index-BSLnLTb2.js} +1 -1
- package/ui-dist/assets/{index-By0oeEvz.js → index-CCZ2qgHN.js} +1 -1
- package/ui-dist/assets/{index-bUFCSEgv.js → index-CLZXR1k9.js} +1 -1
- package/ui-dist/assets/{index-CJlw4l9L.js → index-CTNfrE1s.js} +1 -1
- package/ui-dist/assets/{index-TmpY6Xh_.js → index-ChNd25s7.js} +1 -1
- package/ui-dist/assets/{index-BRgdHUNR.js → index-CjHiMOEO.js} +1 -1
- package/ui-dist/assets/{index-DrL6pHSJ.js → index-CmeJQpXZ.js} +1 -1
- package/ui-dist/assets/index-CyuYtn7U.css +1 -0
- package/ui-dist/assets/{index-CKKytnOd.js → index-CzZjM8eh.js} +1 -1
- package/ui-dist/assets/{index-Dceamza_.js → index-D-6T0Aft.js} +1 -1
- package/ui-dist/assets/{index-CjDqB1SX.js → index-DG_7V1pG.js} +1 -1
- package/ui-dist/assets/index-DIKcgxCt.js +676 -0
- package/ui-dist/assets/{index-BKVeJ3Jh.js → index-DiUerHmi.js} +1 -1
- package/ui-dist/assets/{index-C-nKjFZh.js → index-NZ7v5Fc2.js} +1 -1
- package/ui-dist/assets/{index-CvPPuNs1.js → index-Py70EvBg.js} +1 -1
- package/ui-dist/assets/{index-LHkZdu0h.js → index-R3aYn0qM.js} +1 -1
- package/ui-dist/assets/{index-BT8b79Bd.js → index-YXpm7gW-.js} +1 -1
- package/ui-dist/assets/{index-CemenVOf.js → index-n4q3gzki.js} +1 -1
- package/ui-dist/assets/{index-D3ODiUup.js → index-vlZ3GoYY.js} +1 -1
- package/ui-dist/assets/{infoDiagram-FWYZ7A6U-C7_90qGq.js → infoDiagram-FWYZ7A6U-jdi5ciVg.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-T-nPPdTt.js → ishikawaDiagram-FXEZZL3T-CMz5VvYp.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-5HDEW3XC-C3hqQN0R.js → journeyDiagram-5HDEW3XC-DK6dk2R-.js} +1 -1
- package/ui-dist/assets/{kanban-definition-HUTT4EX6-CPoD9DUm.js → kanban-definition-HUTT4EX6-DX5cZL-D.js} +1 -1
- package/ui-dist/assets/{linear-D-S8yr3Z.js → linear-CI96ABKp.js} +1 -1
- package/ui-dist/assets/{mermaid.core-DhvH8shK.js → mermaid.core-DnsEVfbd.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-LN4V7U3C-CxCIKrKo.js → mindmap-definition-LN4V7U3C-BdDCY7hf.js} +1 -1
- package/ui-dist/assets/{pegDiagram-2B236MQR-D4G-PC6N.js → pegDiagram-2B236MQR-DuY4TF-f.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ENE6RG2P-N4X8UvqY.js → pieDiagram-ENE6RG2P-bgFw7dm3.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CZohoDln.js → quadrantDiagram-ABIIQ3AL-BCZVWyWS.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-RFXS5EU6-fMGQVhwF.js → railroadDiagram-RFXS5EU6-D9V5_Cgc.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-TGXJPOKE-CK0IuUmk.js → requirementDiagram-TGXJPOKE-CItsya2r.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-pSfb3CqG.js → sankeyDiagram-HTMAVEWB-Cz9IHsZO.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CbhkqIeq.js → sequenceDiagram-DBY2YBRQ-hMSqTiwK.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-B6vqqPx-.js → sizeCapture-X5ZJPWSS-0g9M5wMf.js} +1 -1
- package/ui-dist/assets/{stateDiagram-2N3HPSRC-DIJf9h5C.js → stateDiagram-2N3HPSRC-F8_YhgWl.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-fZImUBtq.js +1 -0
- package/ui-dist/assets/{swimlanes-5IMT3BWC-DIX_LV8O.js → swimlanes-5IMT3BWC-CCZqPbpE.js} +2 -2
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-9C1UT51R.js +8 -0
- package/ui-dist/assets/{timeline-definition-FHXFAJF6-CD-Pxb_g.js → timeline-definition-FHXFAJF6-BeUIr18x.js} +1 -1
- package/ui-dist/assets/{vennDiagram-L72KCM5P-BAZsWL0R.js → vennDiagram-L72KCM5P-D3d5h_G4.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-EHGQE667-CnNyfwX5.js → wardleyDiagram-EHGQE667-Bzt-As9A.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-FW5EYKEG-j9YgsD3Y.js → xychartDiagram-FW5EYKEG-C2mly0xf.js} +1 -1
- package/ui-dist/fonts/InterVariable-Italic.woff2 +0 -0
- package/ui-dist/fonts/InterVariable.woff2 +0 -0
- package/ui-dist/fonts/NOTICE.md +17 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/locales/en/common.json +1142 -5
- package/ui-dist/locales/zh-CN/common.json +1140 -3
- package/ui-dist/assets/channel-CsiI2LYw.js +0 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-CBn1Jqo8.js +0 -1
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CBn1Jqo8.js +0 -1
- package/ui-dist/assets/index-BsL8K1vL.css +0 -1
- package/ui-dist/assets/index-D__fTsb2.js +0 -643
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DpwLD7YL.js +0 -1
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-Cklauutg.js +0 -8
package/dist/routes/issues.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { Router } from "express";
|
|
3
3
|
import multer from "multer";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { and, asc, desc, eq, inArray, isNull, notInArray } from "drizzle-orm";
|
|
6
6
|
import { activityLog, agents, documents, executionWorkspaces, heartbeatRuns, issueComments, issueDocuments, issueExecutionDecisions, issueRelations, issues as issueRows, issueWorkProducts, pipelineCaseIssueLinks, pipelineCases, pipelineStages, pipelines, projectWorkspaces, } from "@penclipai/db";
|
|
7
|
-
import { addIssueCommentSchema, acceptIssueThreadInteractionSchema, attachmentArtifactWorkProductMetadataSchema, cancelIssueThreadInteractionSchema, companySearchQuerySchema, createIssueAttachmentMetadataSchema, createIssueThreadInteractionSchema, createIssueWorkProductSchema, createIssueLabelSchema, createAcceptedPlanDecompositionSchema, checkoutIssueSchema, createDocumentAnnotationCommentSchema, createDocumentAnnotationThreadSchema, createChildIssueSchema, createIssueSchema, resolveCreateIssueStatusDefault, resolveIssueRecoveryActionSchema, feedbackTargetTypeSchema, feedbackTraceStatusSchema, feedbackVoteValueSchema, upsertIssueFeedbackVoteSchema, upsertIssueWatchdogSchema, linkIssueApprovalSchema, issueDocumentKeySchema, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, ISSUE_WATCHDOG_DISCOVERY_KINDS, TASK_WATCHDOG_PRODUCT_BUG_ORIGIN_KIND, rejectIssueThreadInteractionSchema, restoreIssueDocumentRevisionSchema, respondIssueThreadInteractionSchema, updateIssueWorkProductSchema, updateDocumentAnnotationThreadSchema, upsertIssueDocumentSchema, updateIssueSchema, getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, isUuidLike, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@penclipai/shared";
|
|
7
|
+
import { addIssueCommentSchema, acceptIssueThreadInteractionSchema, attachmentArtifactWorkProductMetadataSchema, cancelIssueThreadInteractionSchema, companySearchQuerySchema, createIssueAttachmentMetadataSchema, createIssueThreadInteractionSchema, createIssueWorkProductSchema, createIssueLabelSchema, createAcceptedPlanDecompositionSchema, checkoutIssueSchema, createDocumentAnnotationCommentSchema, createDocumentAnnotationThreadSchema, createChildIssueSchema, createIssueSchema, resolveCreateIssueStatusDefault, resolveIssueRecoveryActionSchema, feedbackTargetTypeSchema, feedbackTraceStatusSchema, feedbackVoteValueSchema, upsertIssueFeedbackVoteSchema, upsertIssueWatchdogSchema, linkIssueApprovalSchema, issueDocumentKeySchema, ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY, ISSUE_WATCHDOG_DISCOVERY_KINDS, TASK_WATCHDOG_PRODUCT_BUG_ORIGIN_KIND, rejectIssueThreadInteractionSchema, restoreIssueDocumentRevisionSchema, respondIssueThreadInteractionSchema, submitIssueThreadInteractionVerdictsSchema, updateIssueWorkProductSchema, updateDocumentAnnotationThreadSchema, upsertIssueDocumentSchema, updateIssueSchema, getClosedIsolatedExecutionWorkspaceMessage, isClosedIsolatedExecutionWorkspace, isUuidLike, normalizeIssueIdentifier as normalizeIssueReferenceIdentifier, } from "@penclipai/shared";
|
|
8
8
|
import { trackAgentTaskCompleted } from "@penclipai/shared/telemetry";
|
|
9
9
|
import { getTelemetryClient } from "../telemetry.js";
|
|
10
10
|
import { validate } from "../middleware/validate.js";
|
|
11
11
|
import * as serviceIndex from "../services/index.js";
|
|
12
|
-
import { accessService, agentService, companyService, companySearchService, goalService, heartbeatService, issueApprovalService, issueRecoveryActionService, issueThreadInteractionService, ISSUE_LIST_DEFAULT_LIMIT, ISSUE_LIST_MAX_LIMIT, issueReferenceService, issueService, clampIssueListLimit, documentService, documentAnnotationService, logActivity, projectService, routineService, workProductService, } from "../services/index.js";
|
|
12
|
+
import { accessService, agentService, companySkillService, companyService, companySearchService, goalService, heartbeatService, issueApprovalService, issueRecoveryActionService, issueThreadInteractionService, ISSUE_LIST_DEFAULT_LIMIT, ISSUE_LIST_MAX_LIMIT, issueReferenceService, issueService, clampIssueListLimit, documentService, documentAnnotationService, logActivity, projectService, routineService, workProductService, } from "../services/index.js";
|
|
13
13
|
import { buildPlanReviewContext } from "../services/plan-review-context.js";
|
|
14
14
|
import { TASK_WATCHDOG_ORIGIN_KIND, resolveTaskWatchdogMutationScope, taskWatchdogScopeAllowsIssueMutation, } from "../services/task-watchdog-scope.js";
|
|
15
15
|
import { logger } from "../middleware/logger.js";
|
|
@@ -24,7 +24,8 @@ import { assertEnvironmentSelectionForCompany } from "./environment-selection.js
|
|
|
24
24
|
import { executionWorkspaceService as executionWorkspaceServiceDirect } from "../services/execution-workspaces.js";
|
|
25
25
|
import { feedbackService } from "../services/feedback.js";
|
|
26
26
|
import { instanceSettingsService } from "../services/instance-settings.js";
|
|
27
|
-
import { readAcceptedPlanConfirmationTarget } from "../services/issues.js";
|
|
27
|
+
import { ISSUE_BLOCKER_DIAGNOSTICS_MAX_BLOCKERS, ISSUE_WAKE_DIAGNOSTICS_LOOKBACK_DAYS, ISSUE_WAKE_DIAGNOSTICS_MAX_ACTIVITY_RECORDS, ISSUE_WAKE_DIAGNOSTICS_MAX_WAKE_REQUESTS, readAcceptedPlanConfirmationTarget, } from "../services/issues.js";
|
|
28
|
+
import { authorizationDeniedDetails } from "../services/authorization.js";
|
|
28
29
|
import { environmentService } from "../services/environments.js";
|
|
29
30
|
import { environmentRuntimeService } from "../services/environment-runtime.js";
|
|
30
31
|
import { redactSensitiveText } from "../redaction.js";
|
|
@@ -198,6 +199,67 @@ function readNonEmptyString(value) {
|
|
|
198
199
|
function readObject(value) {
|
|
199
200
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
200
201
|
}
|
|
202
|
+
function hasOwn(record, key) {
|
|
203
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
204
|
+
}
|
|
205
|
+
async function auditAgentIssueCreateAttributionSpoof(input) {
|
|
206
|
+
const actor = getActorInfo(input.req);
|
|
207
|
+
await logActivity(input.db, {
|
|
208
|
+
companyId: input.companyId,
|
|
209
|
+
actorType: actor.actorType,
|
|
210
|
+
actorId: actor.actorId,
|
|
211
|
+
agentId: actor.agentId,
|
|
212
|
+
runId: actor.runId,
|
|
213
|
+
action: input.action === "rejected"
|
|
214
|
+
? "issue.attribution_spoof_rejected"
|
|
215
|
+
: "issue.attribution_spoof_stripped",
|
|
216
|
+
entityType: input.entityId ? "issue" : "company",
|
|
217
|
+
entityId: input.entityId ?? input.companyId,
|
|
218
|
+
details: {
|
|
219
|
+
surface: input.surface,
|
|
220
|
+
field: input.field,
|
|
221
|
+
requestedValue: input.requestedValue,
|
|
222
|
+
derivedFrom: "authenticated_actor",
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function sanitizeIssueCreateAttribution(db, req, res, companyId, input, options) {
|
|
227
|
+
const sanitized = { ...input };
|
|
228
|
+
if (req.actor.type !== "agent")
|
|
229
|
+
return sanitized;
|
|
230
|
+
if (hasOwn(sanitized, "responsibleUserId") && sanitized.responsibleUserId != null) {
|
|
231
|
+
await auditAgentIssueCreateAttributionSpoof({
|
|
232
|
+
db,
|
|
233
|
+
req,
|
|
234
|
+
companyId,
|
|
235
|
+
entityId: options.entityId,
|
|
236
|
+
surface: options.surface,
|
|
237
|
+
field: "responsibleUserId",
|
|
238
|
+
action: "rejected",
|
|
239
|
+
requestedValue: readNonEmptyString(sanitized.responsibleUserId),
|
|
240
|
+
});
|
|
241
|
+
res.status(422).json({ error: "Agent-created issues cannot set responsibleUserId" });
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
if (hasOwn(sanitized, "createdByUserId") && sanitized.createdByUserId != null) {
|
|
245
|
+
await auditAgentIssueCreateAttributionSpoof({
|
|
246
|
+
db,
|
|
247
|
+
req,
|
|
248
|
+
companyId,
|
|
249
|
+
entityId: options.entityId,
|
|
250
|
+
surface: options.surface,
|
|
251
|
+
field: "createdByUserId",
|
|
252
|
+
action: "stripped",
|
|
253
|
+
requestedValue: readNonEmptyString(sanitized.createdByUserId),
|
|
254
|
+
});
|
|
255
|
+
delete sanitized.createdByUserId;
|
|
256
|
+
}
|
|
257
|
+
delete sanitized.responsibleUserId;
|
|
258
|
+
return sanitized;
|
|
259
|
+
}
|
|
260
|
+
function authenticatedActorResponsibleUserId(req) {
|
|
261
|
+
return req.actor.type === "agent" ? req.actor.onBehalfOfUserId ?? null : null;
|
|
262
|
+
}
|
|
201
263
|
function readPlanConfirmationTargetForIssue(payload, issueId) {
|
|
202
264
|
const target = readObject(readObject(payload).target);
|
|
203
265
|
if (target.type !== "issue_document" || target.key !== "plan")
|
|
@@ -250,7 +312,7 @@ function emptyWorkspaceNameMaps() {
|
|
|
250
312
|
};
|
|
251
313
|
}
|
|
252
314
|
function summarizeIssueWorkspaceForActivity(issue, names) {
|
|
253
|
-
const settings = parseIssueExecutionWorkspaceSettings(issue.executionWorkspaceSettings);
|
|
315
|
+
const settings = parseIssueExecutionWorkspaceSettings(issue.executionWorkspaceSettings, { includeEnvironmentId: true });
|
|
254
316
|
const mode = settings?.mode ?? issue.executionWorkspacePreference ?? null;
|
|
255
317
|
const executionWorkspaceId = issue.executionWorkspaceId ?? null;
|
|
256
318
|
const projectWorkspaceId = issue.projectWorkspaceId ?? null;
|
|
@@ -411,6 +473,469 @@ function withRecoveryActionsOnRelationSummaries(relations, recoveryActionByIssue
|
|
|
411
473
|
blocks: relations.blocks.map(augment),
|
|
412
474
|
};
|
|
413
475
|
}
|
|
476
|
+
function toIssueBlockerDiagnosticSummary(issue) {
|
|
477
|
+
return {
|
|
478
|
+
id: issue.id,
|
|
479
|
+
identifier: issue.identifier,
|
|
480
|
+
title: issue.title,
|
|
481
|
+
status: issue.status,
|
|
482
|
+
priority: issue.priority,
|
|
483
|
+
assigneeAgentId: issue.assigneeAgentId,
|
|
484
|
+
assigneeUserId: issue.assigneeUserId,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function blockerDiagnosticLabel(issue) {
|
|
488
|
+
return issue.identifier ?? issue.title;
|
|
489
|
+
}
|
|
490
|
+
function buildIssueBlockerDiagnosticsResponse(input) {
|
|
491
|
+
const issue = toIssueBlockerDiagnosticSummary(input.issue);
|
|
492
|
+
const visibleBlockerIds = new Set(input.visibleBlockers.map((blocker) => blocker.id));
|
|
493
|
+
const omittedUnauthorizedBlockerCount = input.blockers.filter((blocker) => !visibleBlockerIds.has(blocker.id)).length;
|
|
494
|
+
const completeVisibleSet = !input.truncated && omittedUnauthorizedBlockerCount === 0;
|
|
495
|
+
const unresolvedIds = new Set(input.readiness.unresolvedBlockerIssueIds);
|
|
496
|
+
const pendingFinalizeIds = new Set(input.readiness.pendingFinalizeBlockerIssueIds);
|
|
497
|
+
const blockers = input.visibleBlockers.map((blockerRow) => {
|
|
498
|
+
const blocker = toIssueBlockerDiagnosticSummary(blockerRow);
|
|
499
|
+
const isPendingFinalize = pendingFinalizeIds.has(blocker.id);
|
|
500
|
+
const isUnresolved = unresolvedIds.has(blocker.id);
|
|
501
|
+
const flags = [];
|
|
502
|
+
if (issue.status === "blocked" && blocker.status === "done")
|
|
503
|
+
flags.push("done_but_blocking");
|
|
504
|
+
if (blocker.status === "cancelled")
|
|
505
|
+
flags.push("cancelled_blocker_in_set");
|
|
506
|
+
if (isPendingFinalize)
|
|
507
|
+
flags.push("workspace_finalize_pending");
|
|
508
|
+
return {
|
|
509
|
+
...blocker,
|
|
510
|
+
isUnresolved,
|
|
511
|
+
isPendingFinalize,
|
|
512
|
+
isDependencyReady: blocker.status === "done" && !isPendingFinalize,
|
|
513
|
+
flags,
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
const readiness = completeVisibleSet
|
|
517
|
+
? {
|
|
518
|
+
allBlockersDone: input.readiness.allBlockersDone,
|
|
519
|
+
isDependencyReady: input.readiness.isDependencyReady,
|
|
520
|
+
unresolvedBlockerCount: input.readiness.unresolvedBlockerIssueIds.length,
|
|
521
|
+
pendingFinalizeBlockerCount: input.readiness.pendingFinalizeBlockerIssueIds.length,
|
|
522
|
+
}
|
|
523
|
+
: null;
|
|
524
|
+
const reportedOmittedUnauthorizedBlockerCount = input.truncated
|
|
525
|
+
? null
|
|
526
|
+
: omittedUnauthorizedBlockerCount;
|
|
527
|
+
return {
|
|
528
|
+
issue,
|
|
529
|
+
diagnosis: buildIssueBlockerDiagnosis({
|
|
530
|
+
issue,
|
|
531
|
+
blockers,
|
|
532
|
+
readiness,
|
|
533
|
+
omittedUnauthorizedBlockerCount: reportedOmittedUnauthorizedBlockerCount,
|
|
534
|
+
truncated: input.truncated,
|
|
535
|
+
maxBlockers: input.maxBlockers ?? ISSUE_BLOCKER_DIAGNOSTICS_MAX_BLOCKERS,
|
|
536
|
+
}),
|
|
537
|
+
readiness,
|
|
538
|
+
blockers,
|
|
539
|
+
omittedUnauthorizedBlockerCount: reportedOmittedUnauthorizedBlockerCount,
|
|
540
|
+
truncated: input.truncated,
|
|
541
|
+
caps: {
|
|
542
|
+
maxBlockers: input.maxBlockers ?? ISSUE_BLOCKER_DIAGNOSTICS_MAX_BLOCKERS,
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function buildIssueBlockerDiagnosis(input) {
|
|
547
|
+
if (input.truncated) {
|
|
548
|
+
return `Blocker diagnostics for ${blockerDiagnosticLabel(input.issue)} are truncated at ${input.maxBlockers} blockers, so readiness is not reported.`;
|
|
549
|
+
}
|
|
550
|
+
const omittedUnauthorizedBlockerCount = input.omittedUnauthorizedBlockerCount ?? 0;
|
|
551
|
+
if (omittedUnauthorizedBlockerCount > 0) {
|
|
552
|
+
return `One or more blockers for ${blockerDiagnosticLabel(input.issue)} are outside this actor's authorization boundary, so this diagnosis only covers visible blockers.`;
|
|
553
|
+
}
|
|
554
|
+
if (input.blockers.length === 0) {
|
|
555
|
+
return input.issue.status === "blocked"
|
|
556
|
+
? `${blockerDiagnosticLabel(input.issue)} is blocked but has no first-class blocker relations.`
|
|
557
|
+
: null;
|
|
558
|
+
}
|
|
559
|
+
const pendingFinalize = input.blockers.find((blocker) => blocker.isPendingFinalize);
|
|
560
|
+
if (pendingFinalize) {
|
|
561
|
+
return `${blockerDiagnosticLabel(input.issue)} is waiting for ${blockerDiagnosticLabel(pendingFinalize)} to finish workspace finalization.`;
|
|
562
|
+
}
|
|
563
|
+
const cancelled = input.blockers.find((blocker) => blocker.status === "cancelled");
|
|
564
|
+
if (cancelled) {
|
|
565
|
+
return `${blockerDiagnosticLabel(input.issue)} is blocked by ${blockerDiagnosticLabel(cancelled)}, which is cancelled; cancelled blockers do not resolve until the blocker relation is removed or replaced.`;
|
|
566
|
+
}
|
|
567
|
+
const unresolved = input.blockers.find((blocker) => blocker.isUnresolved);
|
|
568
|
+
if (unresolved) {
|
|
569
|
+
return `${blockerDiagnosticLabel(input.issue)} is blocked by ${blockerDiagnosticLabel(unresolved)}, which is ${unresolved.status}.`;
|
|
570
|
+
}
|
|
571
|
+
if (input.readiness?.isDependencyReady && input.issue.status === "blocked") {
|
|
572
|
+
return `All blockers for ${blockerDiagnosticLabel(input.issue)} are resolved, but the issue is still blocked; this is likely a stale blocker hold.`;
|
|
573
|
+
}
|
|
574
|
+
if (input.readiness?.isDependencyReady) {
|
|
575
|
+
return `All blockers for ${blockerDiagnosticLabel(input.issue)} are resolved.`;
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const ISSUE_WAKE_DIAGNOSTIC_KNOWN_SOURCES = new Set([
|
|
580
|
+
"timer",
|
|
581
|
+
"assignment",
|
|
582
|
+
"on_demand",
|
|
583
|
+
"automation",
|
|
584
|
+
]);
|
|
585
|
+
const ISSUE_WAKE_DIAGNOSTIC_KNOWN_REASONS = new Set([
|
|
586
|
+
"issue_assigned",
|
|
587
|
+
"issue_blockers_resolved",
|
|
588
|
+
"issue_commented",
|
|
589
|
+
"issue_comment_mentioned",
|
|
590
|
+
"issue_dependencies_blocked",
|
|
591
|
+
"issue_tree_hold_active",
|
|
592
|
+
"missing_issue_comment",
|
|
593
|
+
"process_lost_retry",
|
|
594
|
+
"run_liveness_continuation",
|
|
595
|
+
"heartbeat.disabled",
|
|
596
|
+
"heartbeat.timer.no_actionable_work",
|
|
597
|
+
"heartbeat.wakeOnDemand.disabled",
|
|
598
|
+
]);
|
|
599
|
+
const ISSUE_WAKE_DIAGNOSTIC_KNOWN_STATUSES = new Set([
|
|
600
|
+
"queued",
|
|
601
|
+
"claimed",
|
|
602
|
+
"coalesced",
|
|
603
|
+
"skipped",
|
|
604
|
+
"completed",
|
|
605
|
+
"failed",
|
|
606
|
+
"cancelled",
|
|
607
|
+
"deferred_issue_execution",
|
|
608
|
+
]);
|
|
609
|
+
function dateToIso(value) {
|
|
610
|
+
if (!value)
|
|
611
|
+
return null;
|
|
612
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
613
|
+
}
|
|
614
|
+
function projectWakeDiagnosticSource(value) {
|
|
615
|
+
if (!value)
|
|
616
|
+
return null;
|
|
617
|
+
return ISSUE_WAKE_DIAGNOSTIC_KNOWN_SOURCES.has(value) ? value : "other";
|
|
618
|
+
}
|
|
619
|
+
function projectWakeDiagnosticReason(value) {
|
|
620
|
+
if (!value)
|
|
621
|
+
return null;
|
|
622
|
+
return ISSUE_WAKE_DIAGNOSTIC_KNOWN_REASONS.has(value) ? value : "other";
|
|
623
|
+
}
|
|
624
|
+
function projectWakeDiagnosticStatus(value) {
|
|
625
|
+
return ISSUE_WAKE_DIAGNOSTIC_KNOWN_STATUSES.has(value) ? value : "other";
|
|
626
|
+
}
|
|
627
|
+
function wakeFailureClass(status, rawError) {
|
|
628
|
+
if (status === "failed" || rawError)
|
|
629
|
+
return "failed";
|
|
630
|
+
if (status === "cancelled")
|
|
631
|
+
return "cancelled";
|
|
632
|
+
if (status === "skipped")
|
|
633
|
+
return "skipped";
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
function projectIssueWakeRequest(row, options) {
|
|
637
|
+
const status = projectWakeDiagnosticStatus(row.status);
|
|
638
|
+
return {
|
|
639
|
+
kind: "wake_request",
|
|
640
|
+
agentId: options.includeInternalIds ? row.agentId : null,
|
|
641
|
+
source: projectWakeDiagnosticSource(row.source) ?? "other",
|
|
642
|
+
reason: projectWakeDiagnosticReason(row.reason),
|
|
643
|
+
status,
|
|
644
|
+
coalescedCount: row.coalescedCount,
|
|
645
|
+
runId: options.includeInternalIds ? row.runId : null,
|
|
646
|
+
requestedAt: dateToIso(row.requestedAt),
|
|
647
|
+
claimedAt: dateToIso(row.claimedAt),
|
|
648
|
+
finishedAt: dateToIso(row.finishedAt),
|
|
649
|
+
failureClass: wakeFailureClass(status, row.error),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function wakeDiagnosticActivityAction(action) {
|
|
653
|
+
return action === "issue.tree_hold_wakeup_deferred" ? action : "other";
|
|
654
|
+
}
|
|
655
|
+
function wakeDiagnosticActivityEntityType(entityType) {
|
|
656
|
+
return entityType === "issue" || entityType === "agent_wakeup_request" ? entityType : "other";
|
|
657
|
+
}
|
|
658
|
+
function projectIssueWakeActivityRecord(row, issueId, options) {
|
|
659
|
+
const details = row.details && typeof row.details === "object" ? row.details : {};
|
|
660
|
+
const action = wakeDiagnosticActivityAction(row.action);
|
|
661
|
+
const rootIssueId = readNonEmptyString(details["rootIssueId"]);
|
|
662
|
+
const detailIssueId = readNonEmptyString(details["issueId"]);
|
|
663
|
+
const projectedRootIssueId = rootIssueId === issueId || detailIssueId === issueId || (row.entityType === "issue" && row.entityId === issueId)
|
|
664
|
+
? issueId
|
|
665
|
+
: null;
|
|
666
|
+
return {
|
|
667
|
+
kind: "activity",
|
|
668
|
+
action,
|
|
669
|
+
entityType: wakeDiagnosticActivityEntityType(row.entityType),
|
|
670
|
+
agentId: options.includeInternalIds ? row.agentId ?? readNonEmptyString(details["agentId"]) : null,
|
|
671
|
+
runId: options.includeInternalIds ? row.runId : null,
|
|
672
|
+
createdAt: dateToIso(row.createdAt),
|
|
673
|
+
source: projectWakeDiagnosticSource(readNonEmptyString(details["source"])),
|
|
674
|
+
requestedReason: projectWakeDiagnosticReason(readNonEmptyString(details["requestedReason"])),
|
|
675
|
+
previousReason: projectWakeDiagnosticReason(readNonEmptyString(details["previousReason"])),
|
|
676
|
+
rootIssueId: projectedRootIssueId,
|
|
677
|
+
holdId: options.includeInternalIds ? readNonEmptyString(details["holdId"]) : null,
|
|
678
|
+
summary: action === "issue.tree_hold_wakeup_deferred"
|
|
679
|
+
? "Wake was deferred because an active issue-tree hold was present."
|
|
680
|
+
: "Wake-related activity was recorded.",
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function issueWakeDiagnosticEventTimestamp(event) {
|
|
684
|
+
const timestamp = event.kind === "wake_request" ? event.requestedAt : event.createdAt;
|
|
685
|
+
return new Date(timestamp).getTime();
|
|
686
|
+
}
|
|
687
|
+
function wakeDiagnosticReasonPhrase(reason) {
|
|
688
|
+
return reason ? ` for ${reason}` : "";
|
|
689
|
+
}
|
|
690
|
+
function buildIssueWakeDiagnosis(input) {
|
|
691
|
+
if (input.truncated) {
|
|
692
|
+
return `Wake diagnostics for ${blockerDiagnosticLabel(input.issue)} are truncated to ${input.maxWakeRequests} wake requests and ${input.maxActivityRecords} activity records over ${input.lookbackDays} days, so the diagnosis only covers returned records.`;
|
|
693
|
+
}
|
|
694
|
+
const latest = input.events[0];
|
|
695
|
+
if (latest?.kind === "activity" && latest.action === "issue.tree_hold_wakeup_deferred") {
|
|
696
|
+
return `The most recent wake-related activity for ${blockerDiagnosticLabel(input.issue)} was deferred by an active issue-tree hold.`;
|
|
697
|
+
}
|
|
698
|
+
if (latest?.kind === "wake_request") {
|
|
699
|
+
if (latest.status === "deferred_issue_execution") {
|
|
700
|
+
return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} is deferred${wakeDiagnosticReasonPhrase(latest.reason)}.`;
|
|
701
|
+
}
|
|
702
|
+
if (latest.status === "failed") {
|
|
703
|
+
return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} failed${wakeDiagnosticReasonPhrase(latest.reason)}; raw error text is withheld.`;
|
|
704
|
+
}
|
|
705
|
+
if (latest.status === "skipped" || latest.status === "cancelled" || latest.status === "coalesced") {
|
|
706
|
+
const coalesced = latest.coalescedCount > 0 ? ` and coalesced ${latest.coalescedCount} additional request(s)` : "";
|
|
707
|
+
return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} was ${latest.status}${wakeDiagnosticReasonPhrase(latest.reason)}${coalesced}.`;
|
|
708
|
+
}
|
|
709
|
+
if (latest.status === "queued" || latest.status === "claimed") {
|
|
710
|
+
return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} is currently ${latest.status}${wakeDiagnosticReasonPhrase(latest.reason)}.`;
|
|
711
|
+
}
|
|
712
|
+
if (latest.status === "completed") {
|
|
713
|
+
return `The most recent wake for ${blockerDiagnosticLabel(input.issue)} completed${wakeDiagnosticReasonPhrase(latest.reason)}.`;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (input.events.length > 0)
|
|
717
|
+
return null;
|
|
718
|
+
const blockerDiagnostics = input.blockerDiagnostics;
|
|
719
|
+
if (blockerDiagnostics.truncated) {
|
|
720
|
+
return `No wake rows are visible for ${blockerDiagnosticLabel(input.issue)} in the bounded window, and blocker diagnostics are truncated, so no wake cause is inferred.`;
|
|
721
|
+
}
|
|
722
|
+
if ((blockerDiagnostics.omittedUnauthorizedBlockerCount ?? 0) > 0) {
|
|
723
|
+
return `No wake rows are visible for ${blockerDiagnosticLabel(input.issue)} in the bounded window, and one or more blockers are outside this actor's authorization boundary.`;
|
|
724
|
+
}
|
|
725
|
+
if (input.issue.status !== "blocked" || blockerDiagnostics.blockers.length === 0)
|
|
726
|
+
return null;
|
|
727
|
+
const pendingFinalize = blockerDiagnostics.blockers.find((blocker) => blocker.isPendingFinalize);
|
|
728
|
+
if (pendingFinalize) {
|
|
729
|
+
return `No wake row exists for ${blockerDiagnosticLabel(input.issue)} in the bounded window. ${blockerDiagnosticLabel(input.issue)} is waiting for ${blockerDiagnosticLabel(pendingFinalize)} to finish workspace finalization, so issue_blockers_resolved has not fired.`;
|
|
730
|
+
}
|
|
731
|
+
const cancelled = blockerDiagnostics.blockers.find((blocker) => blocker.status === "cancelled");
|
|
732
|
+
if (cancelled) {
|
|
733
|
+
return `No wake row exists for ${blockerDiagnosticLabel(input.issue)} in the bounded window. ${blockerDiagnosticLabel(input.issue)} is blocked by ${blockerDiagnosticLabel(cancelled)}, which is cancelled; cancelled blockers do not fire issue_blockers_resolved.`;
|
|
734
|
+
}
|
|
735
|
+
const unresolved = blockerDiagnostics.blockers.find((blocker) => blocker.isUnresolved);
|
|
736
|
+
if (unresolved) {
|
|
737
|
+
return `No wake row exists for ${blockerDiagnosticLabel(input.issue)} in the bounded window. ${blockerDiagnosticLabel(input.issue)} is blocked by ${blockerDiagnosticLabel(unresolved)}, which is ${unresolved.status}, so issue_blockers_resolved has not fired.`;
|
|
738
|
+
}
|
|
739
|
+
if (blockerDiagnostics.readiness?.isDependencyReady) {
|
|
740
|
+
return `No wake row exists for ${blockerDiagnosticLabel(input.issue)} in the bounded window. All visible blockers are resolved, but the issue is still blocked; this is likely a stale blocker hold or an older wake outside the lookback window.`;
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
function buildIssueWakeDiagnosticsResponse(input) {
|
|
745
|
+
const issue = toIssueBlockerDiagnosticSummary(input.issue);
|
|
746
|
+
const events = [
|
|
747
|
+
...input.wakeRequests.map((record) => projectIssueWakeRequest(record, { includeInternalIds: input.includeInternalIds })),
|
|
748
|
+
...input.activityRecords.map((record) => projectIssueWakeActivityRecord(record, issue.id, { includeInternalIds: input.includeInternalIds })),
|
|
749
|
+
].sort((left, right) => issueWakeDiagnosticEventTimestamp(right) - issueWakeDiagnosticEventTimestamp(left));
|
|
750
|
+
const truncated = input.truncatedWakeRequests || input.truncatedActivityRecords;
|
|
751
|
+
const maxWakeRequests = input.maxWakeRequests ?? ISSUE_WAKE_DIAGNOSTICS_MAX_WAKE_REQUESTS;
|
|
752
|
+
const maxActivityRecords = input.maxActivityRecords ?? ISSUE_WAKE_DIAGNOSTICS_MAX_ACTIVITY_RECORDS;
|
|
753
|
+
const lookbackDays = input.lookbackDays ?? ISSUE_WAKE_DIAGNOSTICS_LOOKBACK_DAYS;
|
|
754
|
+
const diagnosis = buildIssueWakeDiagnosis({
|
|
755
|
+
issue,
|
|
756
|
+
events,
|
|
757
|
+
blockerDiagnostics: input.blockerDiagnostics,
|
|
758
|
+
truncated,
|
|
759
|
+
maxWakeRequests,
|
|
760
|
+
maxActivityRecords,
|
|
761
|
+
lookbackDays,
|
|
762
|
+
});
|
|
763
|
+
return {
|
|
764
|
+
issue,
|
|
765
|
+
diagnosis,
|
|
766
|
+
likelyReason: diagnosis,
|
|
767
|
+
events,
|
|
768
|
+
wakeRequestCount: input.wakeRequests.length,
|
|
769
|
+
activityRecordCount: input.activityRecords.length,
|
|
770
|
+
truncated,
|
|
771
|
+
truncatedSections: {
|
|
772
|
+
wakeRequests: input.truncatedWakeRequests,
|
|
773
|
+
activityRecords: input.truncatedActivityRecords,
|
|
774
|
+
},
|
|
775
|
+
caps: {
|
|
776
|
+
maxWakeRequests,
|
|
777
|
+
maxActivityRecords,
|
|
778
|
+
lookbackDays,
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function groupByIssueId(rows) {
|
|
783
|
+
const map = new Map();
|
|
784
|
+
for (const row of rows) {
|
|
785
|
+
const issueRows = map.get(row.issueId) ?? [];
|
|
786
|
+
issueRows.push(row);
|
|
787
|
+
map.set(row.issueId, issueRows);
|
|
788
|
+
}
|
|
789
|
+
return map;
|
|
790
|
+
}
|
|
791
|
+
function groupBlockersByBlockedIssueId(rows) {
|
|
792
|
+
const map = new Map();
|
|
793
|
+
for (const row of rows) {
|
|
794
|
+
const issueRows = map.get(row.blockedIssueId) ?? [];
|
|
795
|
+
issueRows.push(row);
|
|
796
|
+
map.set(row.blockedIssueId, issueRows);
|
|
797
|
+
}
|
|
798
|
+
return map;
|
|
799
|
+
}
|
|
800
|
+
function issueSubtreeEdgeTimestamp(edge) {
|
|
801
|
+
return edge.timestamp ? new Date(edge.timestamp).getTime() : 0;
|
|
802
|
+
}
|
|
803
|
+
function buildIssueSubtreeDiagnosis(input) {
|
|
804
|
+
if (input.truncated) {
|
|
805
|
+
return `Subtree diagnostics for ${blockerDiagnosticLabel(input.issue)} are bounded to depth ${input.caps.maxDepth} and ${input.caps.maxNodes} nodes, so the diagnosis only covers returned visible nodes.`;
|
|
806
|
+
}
|
|
807
|
+
if ((input.omittedUnauthorizedNodeCount ?? 0) > 0) {
|
|
808
|
+
return `One or more subtree nodes under ${blockerDiagnosticLabel(input.issue)} are outside this actor's authorization boundary, so this diagnosis only covers visible nodes.`;
|
|
809
|
+
}
|
|
810
|
+
const blockedNodeWithDiagnosis = input.nodes.find((node) => node.issue.status === "blocked" && node.diagnosis);
|
|
811
|
+
const firstNodeWithDiagnosis = blockedNodeWithDiagnosis ?? input.nodes.find((node) => node.diagnosis);
|
|
812
|
+
if (!firstNodeWithDiagnosis?.diagnosis)
|
|
813
|
+
return null;
|
|
814
|
+
return `${blockerDiagnosticLabel(firstNodeWithDiagnosis.issue)} appears to be the subtree stall point: ${firstNodeWithDiagnosis.diagnosis}`;
|
|
815
|
+
}
|
|
816
|
+
function buildIssueSubtreeDiagnosticsResponse(input) {
|
|
817
|
+
const issue = toIssueBlockerDiagnosticSummary(input.issue);
|
|
818
|
+
const visibleNodeIds = new Set(input.visibleNodes.map((node) => node.id));
|
|
819
|
+
const visibleBlockerIdsByIssueId = groupBlockersByBlockedIssueId(input.visibleBlockers);
|
|
820
|
+
const omittedUnauthorizedNodeCount = input.truncatedNodes || input.truncatedDepth
|
|
821
|
+
? null
|
|
822
|
+
: input.nodes.filter((node) => !visibleNodeIds.has(node.id)).length;
|
|
823
|
+
const nodeResponses = [];
|
|
824
|
+
const edges = [];
|
|
825
|
+
for (const node of input.visibleNodes) {
|
|
826
|
+
const rawBlockers = input.blockersByIssueId.get(node.id) ?? [];
|
|
827
|
+
const visibleBlockers = visibleBlockerIdsByIssueId.get(node.id) ?? [];
|
|
828
|
+
const blockerResponse = buildIssueBlockerDiagnosticsResponse({
|
|
829
|
+
issue: node,
|
|
830
|
+
blockers: rawBlockers,
|
|
831
|
+
visibleBlockers,
|
|
832
|
+
readiness: input.readinessByIssueId.get(node.id) ?? {
|
|
833
|
+
allBlockersDone: true,
|
|
834
|
+
isDependencyReady: true,
|
|
835
|
+
unresolvedBlockerIssueIds: [],
|
|
836
|
+
pendingFinalizeBlockerIssueIds: [],
|
|
837
|
+
},
|
|
838
|
+
truncated: input.truncatedBlockerIssueIds.has(node.id),
|
|
839
|
+
maxBlockers: input.caps.maxBlockersPerNode,
|
|
840
|
+
});
|
|
841
|
+
const wakeResponse = buildIssueWakeDiagnosticsResponse({
|
|
842
|
+
issue: node,
|
|
843
|
+
wakeRequests: input.wakeRequestsByIssueId.get(node.id) ?? [],
|
|
844
|
+
activityRecords: input.activityRecordsByIssueId.get(node.id) ?? [],
|
|
845
|
+
blockerDiagnostics: blockerResponse,
|
|
846
|
+
truncatedWakeRequests: input.truncatedWakeIssueIds.has(node.id),
|
|
847
|
+
truncatedActivityRecords: input.truncatedActivityIssueIds.has(node.id),
|
|
848
|
+
includeInternalIds: input.includeInternalIds,
|
|
849
|
+
maxWakeRequests: input.caps.maxWakeRequestsPerNode,
|
|
850
|
+
maxActivityRecords: input.caps.maxActivityRecordsPerNode,
|
|
851
|
+
lookbackDays: input.caps.lookbackDays,
|
|
852
|
+
});
|
|
853
|
+
const nodeDiagnosis = wakeResponse.diagnosis ?? blockerResponse.diagnosis;
|
|
854
|
+
if (node.parentId && visibleNodeIds.has(node.parentId)) {
|
|
855
|
+
edges.push({
|
|
856
|
+
kind: "parent",
|
|
857
|
+
fromIssueId: node.parentId,
|
|
858
|
+
toIssueId: node.id,
|
|
859
|
+
timestamp: dateToIso(node.createdAt),
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
for (const blocker of visibleBlockers) {
|
|
863
|
+
edges.push({
|
|
864
|
+
kind: "blocks",
|
|
865
|
+
fromIssueId: blocker.id,
|
|
866
|
+
toIssueId: node.id,
|
|
867
|
+
timestamp: dateToIso(blocker.relationCreatedAt),
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
for (const event of wakeResponse.events) {
|
|
871
|
+
if (event.kind === "wake_request") {
|
|
872
|
+
edges.push({
|
|
873
|
+
kind: "wake_request",
|
|
874
|
+
issueId: node.id,
|
|
875
|
+
agentId: event.agentId,
|
|
876
|
+
reason: event.reason,
|
|
877
|
+
status: event.status,
|
|
878
|
+
timestamp: event.requestedAt,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
edges.push({
|
|
883
|
+
kind: "activity",
|
|
884
|
+
issueId: node.id,
|
|
885
|
+
action: event.action,
|
|
886
|
+
timestamp: event.createdAt,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
nodeResponses.push({
|
|
891
|
+
issue: toIssueBlockerDiagnosticSummary(node),
|
|
892
|
+
parentId: node.parentId && visibleNodeIds.has(node.parentId) ? node.parentId : null,
|
|
893
|
+
depth: node.depth,
|
|
894
|
+
diagnosis: nodeDiagnosis,
|
|
895
|
+
likelyReason: nodeDiagnosis,
|
|
896
|
+
blockers: blockerResponse.blockers,
|
|
897
|
+
blockerReadiness: blockerResponse.readiness,
|
|
898
|
+
omittedUnauthorizedBlockerCount: blockerResponse.omittedUnauthorizedBlockerCount,
|
|
899
|
+
wakeEvents: wakeResponse.events,
|
|
900
|
+
wakeRequestCount: wakeResponse.wakeRequestCount,
|
|
901
|
+
activityRecordCount: wakeResponse.activityRecordCount,
|
|
902
|
+
truncated: blockerResponse.truncated || wakeResponse.truncated,
|
|
903
|
+
truncatedSections: {
|
|
904
|
+
blockers: blockerResponse.truncated,
|
|
905
|
+
wakeRequests: wakeResponse.truncatedSections.wakeRequests,
|
|
906
|
+
activityRecords: wakeResponse.truncatedSections.activityRecords,
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
edges.sort((left, right) => issueSubtreeEdgeTimestamp(right) - issueSubtreeEdgeTimestamp(left));
|
|
911
|
+
const truncatedSections = {
|
|
912
|
+
nodes: input.truncatedNodes,
|
|
913
|
+
depth: input.truncatedDepth,
|
|
914
|
+
blockers: input.truncatedBlockerIssueIds.size > 0,
|
|
915
|
+
wakeRequests: input.truncatedWakeIssueIds.size > 0,
|
|
916
|
+
activityRecords: input.truncatedActivityIssueIds.size > 0,
|
|
917
|
+
};
|
|
918
|
+
const truncated = Object.values(truncatedSections).some(Boolean);
|
|
919
|
+
const diagnosis = buildIssueSubtreeDiagnosis({
|
|
920
|
+
issue,
|
|
921
|
+
nodes: nodeResponses,
|
|
922
|
+
omittedUnauthorizedNodeCount,
|
|
923
|
+
truncated,
|
|
924
|
+
caps: input.caps,
|
|
925
|
+
});
|
|
926
|
+
return {
|
|
927
|
+
issue,
|
|
928
|
+
diagnosis,
|
|
929
|
+
likelyReason: diagnosis,
|
|
930
|
+
nodes: nodeResponses,
|
|
931
|
+
edges,
|
|
932
|
+
nodeCount: nodeResponses.length,
|
|
933
|
+
omittedUnauthorizedNodeCount,
|
|
934
|
+
truncated,
|
|
935
|
+
truncatedSections,
|
|
936
|
+
caps: input.caps,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
414
939
|
const ACTIVE_REVIEW_APPROVAL_STATUSES = new Set(["pending", "revision_requested"]);
|
|
415
940
|
const INVALID_AGENT_IN_REVIEW_DISPOSITION_MESSAGE = "invalid_issue_disposition: Agent-authored updates that move an issue to in_review must include a real review path. " +
|
|
416
941
|
"This request would leave the issue in_review without anyone or anything owning the next action. " +
|
|
@@ -513,7 +1038,7 @@ async function assertCanManageIssueMonitor(accessSvc, req, companyId, assigneeAg
|
|
|
513
1038
|
resource: { type: "company", companyId },
|
|
514
1039
|
});
|
|
515
1040
|
if (!runtimeDecision.allowed) {
|
|
516
|
-
throw forbidden(runtimeDecision.explanation);
|
|
1041
|
+
throw forbidden(runtimeDecision.explanation, authorizationDeniedDetails(runtimeDecision));
|
|
517
1042
|
}
|
|
518
1043
|
if (req.actor.type === "agent" && req.actor.agentId && req.actor.agentId === assigneeAgentId)
|
|
519
1044
|
return;
|
|
@@ -601,6 +1126,12 @@ function isAssigneeSelfCommentOnTerminalIssue(input) {
|
|
|
601
1126
|
return false;
|
|
602
1127
|
return input.actorId === input.assigneeAgentId;
|
|
603
1128
|
}
|
|
1129
|
+
const REQUEST_ITEM_VERDICTS_WAKE_COALESCE_WINDOW_MS = 2_000;
|
|
1130
|
+
function buildRequestItemVerdictsWakeIdempotencyKey(args) {
|
|
1131
|
+
const now = args.at ?? new Date();
|
|
1132
|
+
const bucket = Math.floor(now.getTime() / REQUEST_ITEM_VERDICTS_WAKE_COALESCE_WINDOW_MS);
|
|
1133
|
+
return `request_item_verdicts:${args.issueId}:${args.interactionId}:${bucket}`;
|
|
1134
|
+
}
|
|
604
1135
|
function queueResolvedInteractionContinuationWakeup(input) {
|
|
605
1136
|
if (input.interaction.continuationPolicy !== "wake_assignee"
|
|
606
1137
|
&& input.interaction.continuationPolicy !== "wake_assignee_on_accept")
|
|
@@ -617,6 +1148,13 @@ function queueResolvedInteractionContinuationWakeup(input) {
|
|
|
617
1148
|
const planTarget = readPlanConfirmationTargetForIssue(input.interaction.payload, input.issue.id);
|
|
618
1149
|
const interactionResult = readConfirmationResultForWake(input.interaction.result);
|
|
619
1150
|
const checkboxSelection = readCheckboxSelectionForWake(input.interaction);
|
|
1151
|
+
const newlyResolvedItemIds = input.newlyResolvedItemIds?.filter((value) => value.length > 0) ?? [];
|
|
1152
|
+
const itemVerdicts = newlyResolvedItemIds.length > 0
|
|
1153
|
+
? {
|
|
1154
|
+
newlyResolvedItemIds,
|
|
1155
|
+
coalesceWindowMs: REQUEST_ITEM_VERDICTS_WAKE_COALESCE_WINDOW_MS,
|
|
1156
|
+
}
|
|
1157
|
+
: null;
|
|
620
1158
|
const planReviewInteraction = planTarget && input.interaction.kind === "request_confirmation"
|
|
621
1159
|
? {
|
|
622
1160
|
id: input.interaction.id,
|
|
@@ -640,8 +1178,10 @@ function queueResolvedInteractionContinuationWakeup(input) {
|
|
|
640
1178
|
sourceRunId: input.interaction.sourceRunId ?? null,
|
|
641
1179
|
...(planReviewInteraction ? { planReviewInteraction } : {}),
|
|
642
1180
|
...(checkboxSelection ? { checkboxSelection } : {}),
|
|
1181
|
+
...(itemVerdicts ? { itemVerdicts, newlyResolvedItemIds } : {}),
|
|
643
1182
|
mutation: "interaction",
|
|
644
1183
|
},
|
|
1184
|
+
idempotencyKey: input.idempotencyKey ?? null,
|
|
645
1185
|
requestedByActorType: input.actor.actorType,
|
|
646
1186
|
requestedByActorId: input.actor.actorId,
|
|
647
1187
|
contextSnapshot: {
|
|
@@ -654,6 +1194,7 @@ function queueResolvedInteractionContinuationWakeup(input) {
|
|
|
654
1194
|
sourceRunId: input.interaction.sourceRunId ?? null,
|
|
655
1195
|
...(planReviewInteraction ? { planReviewInteraction } : {}),
|
|
656
1196
|
...(checkboxSelection ? { checkboxSelection } : {}),
|
|
1197
|
+
...(itemVerdicts ? { itemVerdicts, newlyResolvedItemIds } : {}),
|
|
657
1198
|
wakeReason: "issue_commented",
|
|
658
1199
|
source: input.source,
|
|
659
1200
|
...(forceFreshSession ? { forceFreshSession: true } : {}),
|
|
@@ -802,6 +1343,329 @@ class AutoApprovalIssueMissingError extends Error {
|
|
|
802
1343
|
this.name = "AutoApprovalIssueMissingError";
|
|
803
1344
|
}
|
|
804
1345
|
}
|
|
1346
|
+
function toCompactIssue(issue) {
|
|
1347
|
+
return {
|
|
1348
|
+
id: issue.id,
|
|
1349
|
+
companyId: issue.companyId,
|
|
1350
|
+
projectId: issue.projectId,
|
|
1351
|
+
projectWorkspaceId: issue.projectWorkspaceId,
|
|
1352
|
+
goalId: issue.goalId,
|
|
1353
|
+
parentId: issue.parentId,
|
|
1354
|
+
title: issue.title,
|
|
1355
|
+
description: issue.description,
|
|
1356
|
+
status: issue.status,
|
|
1357
|
+
workMode: issue.workMode,
|
|
1358
|
+
priority: issue.priority,
|
|
1359
|
+
assigneeAgentId: issue.assigneeAgentId,
|
|
1360
|
+
assigneeUserId: issue.assigneeUserId,
|
|
1361
|
+
checkoutRunId: issue.checkoutRunId,
|
|
1362
|
+
executionRunId: issue.executionRunId,
|
|
1363
|
+
executionAgentNameKey: issue.executionAgentNameKey,
|
|
1364
|
+
executionLockedAt: issue.executionLockedAt,
|
|
1365
|
+
createdByAgentId: issue.createdByAgentId,
|
|
1366
|
+
createdByUserId: issue.createdByUserId,
|
|
1367
|
+
issueNumber: issue.issueNumber,
|
|
1368
|
+
identifier: issue.identifier,
|
|
1369
|
+
originKind: issue.originKind,
|
|
1370
|
+
originId: issue.originId,
|
|
1371
|
+
originRunId: issue.originRunId,
|
|
1372
|
+
requestDepth: issue.requestDepth,
|
|
1373
|
+
billingCode: issue.billingCode,
|
|
1374
|
+
executionWorkspaceId: issue.executionWorkspaceId,
|
|
1375
|
+
startedAt: issue.startedAt,
|
|
1376
|
+
completedAt: issue.completedAt,
|
|
1377
|
+
cancelledAt: issue.cancelledAt,
|
|
1378
|
+
createdAt: issue.createdAt,
|
|
1379
|
+
updatedAt: issue.updatedAt,
|
|
1380
|
+
...(issue.labelIds ? { labelIds: issue.labelIds } : {}),
|
|
1381
|
+
...(issue.labels ? { labels: issue.labels } : {}),
|
|
1382
|
+
...(issue.blockedBy ? { blockedBy: issue.blockedBy } : {}),
|
|
1383
|
+
...(issue.blockerAttention ? { blockerAttention: issue.blockerAttention } : {}),
|
|
1384
|
+
...(issue.blockedInboxAttention !== undefined ? { blockedInboxAttention: issue.blockedInboxAttention } : {}),
|
|
1385
|
+
...(issue.productivityReview ? { productivityReview: issue.productivityReview } : {}),
|
|
1386
|
+
...(issue.scheduledRetry ? { scheduledRetry: issue.scheduledRetry } : {}),
|
|
1387
|
+
...(issue.liveDescendantCount !== undefined ? { liveDescendantCount: issue.liveDescendantCount } : {}),
|
|
1388
|
+
...(issue.myLastTouchAt !== undefined ? { myLastTouchAt: issue.myLastTouchAt } : {}),
|
|
1389
|
+
...(issue.lastExternalCommentAt !== undefined ? { lastExternalCommentAt: issue.lastExternalCommentAt } : {}),
|
|
1390
|
+
...(issue.lastActivityAt !== undefined ? { lastActivityAt: issue.lastActivityAt } : {}),
|
|
1391
|
+
...(issue.isUnreadForMe !== undefined ? { isUnreadForMe: issue.isUnreadForMe } : {}),
|
|
1392
|
+
activeRecoveryAction: issue.activeRecoveryAction ?? null,
|
|
1393
|
+
successfulRunHandoff: issue.successfulRunHandoff ?? null,
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
function compactIssueListEtag(issues) {
|
|
1397
|
+
const hash = createHash("sha256")
|
|
1398
|
+
.update(JSON.stringify(issues))
|
|
1399
|
+
.digest("base64url");
|
|
1400
|
+
return `"compact-issues:${hash}"`;
|
|
1401
|
+
}
|
|
1402
|
+
function requestMatchesEtag(ifNoneMatchHeader, etag) {
|
|
1403
|
+
if (!ifNoneMatchHeader)
|
|
1404
|
+
return false;
|
|
1405
|
+
return ifNoneMatchHeader
|
|
1406
|
+
.split(",")
|
|
1407
|
+
.map((value) => value.trim())
|
|
1408
|
+
.some((value) => value === "*" || value === etag || value === `W/${etag}`);
|
|
1409
|
+
}
|
|
1410
|
+
const ISSUE_LIST_SERVER_CACHE_TTL_MS = 2_000;
|
|
1411
|
+
const ISSUE_LIST_SERVER_CACHE_STALE_MS = 5_000;
|
|
1412
|
+
export const ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES = 256;
|
|
1413
|
+
const ISSUE_LIST_STORM_WINDOW_MS = 500;
|
|
1414
|
+
const ISSUE_LIST_STORM_THRESHOLD = 4;
|
|
1415
|
+
const ISSUE_LIST_MAX_ACTOR_CLIENT_INFLIGHT = 8;
|
|
1416
|
+
const issueListResponseCache = new Map();
|
|
1417
|
+
const issueListInflight = new Map();
|
|
1418
|
+
const issueListActorClientInflight = new Map();
|
|
1419
|
+
export function __getIssueListResponseCacheSizeForTests() {
|
|
1420
|
+
return issueListResponseCache.size;
|
|
1421
|
+
}
|
|
1422
|
+
export function __clearIssueListResponseCacheForTests() {
|
|
1423
|
+
issueListResponseCache.clear();
|
|
1424
|
+
}
|
|
1425
|
+
function shortHash(value) {
|
|
1426
|
+
return createHash("sha256").update(value).digest("base64url").slice(0, 16);
|
|
1427
|
+
}
|
|
1428
|
+
function stableJson(value) {
|
|
1429
|
+
if (value === null || typeof value !== "object")
|
|
1430
|
+
return JSON.stringify(value);
|
|
1431
|
+
if (Array.isArray(value))
|
|
1432
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
1433
|
+
const record = value;
|
|
1434
|
+
return `{${Object.keys(record)
|
|
1435
|
+
.sort()
|
|
1436
|
+
.filter((key) => record[key] !== undefined)
|
|
1437
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
|
|
1438
|
+
.join(",")}}`;
|
|
1439
|
+
}
|
|
1440
|
+
function normalizeIssueListCacheValue(value) {
|
|
1441
|
+
if (value === undefined)
|
|
1442
|
+
return undefined;
|
|
1443
|
+
if (value === null)
|
|
1444
|
+
return null;
|
|
1445
|
+
if (Array.isArray(value)) {
|
|
1446
|
+
return value.map(normalizeIssueListCacheValue).sort((a, b) => stableJson(a).localeCompare(stableJson(b)));
|
|
1447
|
+
}
|
|
1448
|
+
if (typeof value === "object") {
|
|
1449
|
+
const normalized = {};
|
|
1450
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
1451
|
+
const next = normalizeIssueListCacheValue(nestedValue);
|
|
1452
|
+
if (next !== undefined)
|
|
1453
|
+
normalized[key] = next;
|
|
1454
|
+
}
|
|
1455
|
+
return normalized;
|
|
1456
|
+
}
|
|
1457
|
+
return value;
|
|
1458
|
+
}
|
|
1459
|
+
function issueListActorIdentity(req, companyId) {
|
|
1460
|
+
if (req.actor.type === "agent") {
|
|
1461
|
+
const onBehalfMembership = req.actor.onBehalfOfUserId
|
|
1462
|
+
? req.actor.onBehalfOfMemberships?.find((membership) => membership.companyId === companyId) ?? null
|
|
1463
|
+
: null;
|
|
1464
|
+
const key = [
|
|
1465
|
+
"agent",
|
|
1466
|
+
companyId,
|
|
1467
|
+
req.actor.agentId ?? "unknown-agent",
|
|
1468
|
+
req.actor.keyId ?? req.actor.source ?? "agent-auth",
|
|
1469
|
+
req.actor.onBehalfOfUserId ?? "no-responsible-user",
|
|
1470
|
+
onBehalfMembership?.status ?? "no-responsible-user-status",
|
|
1471
|
+
onBehalfMembership?.membershipRole ?? "no-responsible-user-role",
|
|
1472
|
+
].join(":");
|
|
1473
|
+
return { actorType: "agent", key, hash: shortHash(key) };
|
|
1474
|
+
}
|
|
1475
|
+
if (req.actor.type === "board") {
|
|
1476
|
+
const sessionPart = req.actor.source === "session"
|
|
1477
|
+
? `cookie:${shortHash(String(req.headers.cookie ?? "no-cookie"))}`
|
|
1478
|
+
: req.actor.keyId ?? req.actor.source ?? "board";
|
|
1479
|
+
const key = [
|
|
1480
|
+
"board",
|
|
1481
|
+
companyId,
|
|
1482
|
+
req.actor.source ?? "board",
|
|
1483
|
+
req.actor.userId ?? "unknown-user",
|
|
1484
|
+
sessionPart,
|
|
1485
|
+
].join(":");
|
|
1486
|
+
return { actorType: "board", key, hash: shortHash(key) };
|
|
1487
|
+
}
|
|
1488
|
+
const key = ["none", companyId, req.actor.source ?? "none"].join(":");
|
|
1489
|
+
return { actorType: "none", key, hash: shortHash(key) };
|
|
1490
|
+
}
|
|
1491
|
+
function issueListClientIdentity(req) {
|
|
1492
|
+
const forwardedFor = Array.isArray(req.headers["x-forwarded-for"])
|
|
1493
|
+
? req.headers["x-forwarded-for"][0]
|
|
1494
|
+
: req.headers["x-forwarded-for"];
|
|
1495
|
+
const client = [
|
|
1496
|
+
String(forwardedFor ?? req.ip ?? "unknown-ip").split(",")[0]?.trim() ?? "unknown-ip",
|
|
1497
|
+
req.header("user-agent") ?? "unknown-agent",
|
|
1498
|
+
].join(":");
|
|
1499
|
+
return { key: client, hash: shortHash(client) };
|
|
1500
|
+
}
|
|
1501
|
+
function safeRefererPath(req) {
|
|
1502
|
+
const referer = req.header("referer");
|
|
1503
|
+
if (!referer)
|
|
1504
|
+
return null;
|
|
1505
|
+
try {
|
|
1506
|
+
return new URL(referer).pathname;
|
|
1507
|
+
}
|
|
1508
|
+
catch {
|
|
1509
|
+
return referer.split("?")[0]?.slice(0, 160) ?? null;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function issueListRequestKey(input) {
|
|
1513
|
+
const route = "GET /api/companies/:companyId/issues";
|
|
1514
|
+
const actor = issueListActorIdentity(input.req, input.companyId);
|
|
1515
|
+
const client = issueListClientIdentity(input.req);
|
|
1516
|
+
const normalizedQuery = normalizeIssueListCacheValue(input.normalizedQuery);
|
|
1517
|
+
const queryKeys = Object.keys(normalizedQuery).sort();
|
|
1518
|
+
const key = stableJson({
|
|
1519
|
+
actor: actor.key,
|
|
1520
|
+
companyId: input.companyId,
|
|
1521
|
+
query: normalizedQuery,
|
|
1522
|
+
route,
|
|
1523
|
+
});
|
|
1524
|
+
return {
|
|
1525
|
+
actor,
|
|
1526
|
+
client,
|
|
1527
|
+
key,
|
|
1528
|
+
keyHash: shortHash(key),
|
|
1529
|
+
queryKeys,
|
|
1530
|
+
route,
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function pruneIssueListResponseCache(now) {
|
|
1534
|
+
for (const [key, entry] of issueListResponseCache) {
|
|
1535
|
+
if (entry.staleUntil <= now)
|
|
1536
|
+
issueListResponseCache.delete(key);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
function touchIssueListResponseCacheEntry(key, entry) {
|
|
1540
|
+
issueListResponseCache.delete(key);
|
|
1541
|
+
issueListResponseCache.set(key, entry);
|
|
1542
|
+
}
|
|
1543
|
+
function trimIssueListResponseCache() {
|
|
1544
|
+
while (issueListResponseCache.size > ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES) {
|
|
1545
|
+
const oldestKey = issueListResponseCache.keys().next().value;
|
|
1546
|
+
if (oldestKey === undefined)
|
|
1547
|
+
return;
|
|
1548
|
+
issueListResponseCache.delete(oldestKey);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
function setIssueListResponseCacheEntry(key, entry) {
|
|
1552
|
+
touchIssueListResponseCacheEntry(key, entry);
|
|
1553
|
+
trimIssueListResponseCache();
|
|
1554
|
+
}
|
|
1555
|
+
function decrementIssueListActorClientInflight(actorClientKey) {
|
|
1556
|
+
const next = (issueListActorClientInflight.get(actorClientKey) ?? 1) - 1;
|
|
1557
|
+
if (next <= 0)
|
|
1558
|
+
issueListActorClientInflight.delete(actorClientKey);
|
|
1559
|
+
else
|
|
1560
|
+
issueListActorClientInflight.set(actorClientKey, next);
|
|
1561
|
+
}
|
|
1562
|
+
async function coordinateIssueListGet(input) {
|
|
1563
|
+
const now = Date.now();
|
|
1564
|
+
pruneIssueListResponseCache(now);
|
|
1565
|
+
const cached = input.allowTtlCache ? issueListResponseCache.get(input.requestKey.key) : undefined;
|
|
1566
|
+
if (cached && cached.expiresAt > now) {
|
|
1567
|
+
touchIssueListResponseCacheEntry(input.requestKey.key, cached);
|
|
1568
|
+
return { response: cached.response, cacheStatus: "hit", identicalInFlightCount: 0 };
|
|
1569
|
+
}
|
|
1570
|
+
const existing = issueListInflight.get(input.requestKey.key);
|
|
1571
|
+
if (existing) {
|
|
1572
|
+
existing.waiterCount += 1;
|
|
1573
|
+
const identicalInFlightCount = existing.waiterCount + 1;
|
|
1574
|
+
if (!existing.stormLogged &&
|
|
1575
|
+
identicalInFlightCount >= ISSUE_LIST_STORM_THRESHOLD &&
|
|
1576
|
+
now - existing.startedAt <= ISSUE_LIST_STORM_WINDOW_MS) {
|
|
1577
|
+
existing.stormLogged = true;
|
|
1578
|
+
const event = {
|
|
1579
|
+
event: "request_storm_detected",
|
|
1580
|
+
route: input.requestKey.route,
|
|
1581
|
+
companyId: input.companyId,
|
|
1582
|
+
actorType: input.requestKey.actor.actorType,
|
|
1583
|
+
actorIdentityHash: input.requestKey.actor.hash,
|
|
1584
|
+
clientHash: input.requestKey.client.hash,
|
|
1585
|
+
cacheKeyHash: input.requestKey.keyHash,
|
|
1586
|
+
queryKeys: input.requestKey.queryKeys,
|
|
1587
|
+
identicalInFlightCount,
|
|
1588
|
+
windowMs: now - existing.startedAt,
|
|
1589
|
+
referer: safeRefererPath(input.req),
|
|
1590
|
+
visibilityHint: input.req.header("x-paperclip-tab-visible") ?? null,
|
|
1591
|
+
};
|
|
1592
|
+
logger.warn(event, "request_storm_detected");
|
|
1593
|
+
input.diagnostics?.onStormDetected?.(event);
|
|
1594
|
+
}
|
|
1595
|
+
const response = await existing.promise;
|
|
1596
|
+
return { response, cacheStatus: "coalesced", identicalInFlightCount };
|
|
1597
|
+
}
|
|
1598
|
+
const actorClientKey = `${input.requestKey.actor.key}:${input.requestKey.client.key}`;
|
|
1599
|
+
const actorClientInflight = issueListActorClientInflight.get(actorClientKey) ?? 0;
|
|
1600
|
+
if (actorClientInflight >= ISSUE_LIST_MAX_ACTOR_CLIENT_INFLIGHT) {
|
|
1601
|
+
if (cached && cached.staleUntil > now) {
|
|
1602
|
+
touchIssueListResponseCacheEntry(input.requestKey.key, cached);
|
|
1603
|
+
return { response: cached.response, cacheStatus: "stale", identicalInFlightCount: 0 };
|
|
1604
|
+
}
|
|
1605
|
+
return { response: null, cacheStatus: "retry", identicalInFlightCount: 0, retryAfterSeconds: 1 };
|
|
1606
|
+
}
|
|
1607
|
+
issueListActorClientInflight.set(actorClientKey, actorClientInflight + 1);
|
|
1608
|
+
const promise = (async () => {
|
|
1609
|
+
await input.diagnostics?.onComputeStart?.({
|
|
1610
|
+
companyId: input.companyId,
|
|
1611
|
+
cacheKeyHash: input.requestKey.keyHash,
|
|
1612
|
+
});
|
|
1613
|
+
return input.compute();
|
|
1614
|
+
})();
|
|
1615
|
+
const inflightEntry = {
|
|
1616
|
+
promise,
|
|
1617
|
+
startedAt: now,
|
|
1618
|
+
waiterCount: 0,
|
|
1619
|
+
stormLogged: false,
|
|
1620
|
+
};
|
|
1621
|
+
issueListInflight.set(input.requestKey.key, inflightEntry);
|
|
1622
|
+
try {
|
|
1623
|
+
const response = await promise;
|
|
1624
|
+
if (input.allowTtlCache) {
|
|
1625
|
+
setIssueListResponseCacheEntry(input.requestKey.key, {
|
|
1626
|
+
response,
|
|
1627
|
+
expiresAt: Date.now() + ISSUE_LIST_SERVER_CACHE_TTL_MS,
|
|
1628
|
+
staleUntil: Date.now() + ISSUE_LIST_SERVER_CACHE_STALE_MS,
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
return { response, cacheStatus: "miss", identicalInFlightCount: 1 };
|
|
1632
|
+
}
|
|
1633
|
+
finally {
|
|
1634
|
+
if (issueListInflight.get(input.requestKey.key) === inflightEntry) {
|
|
1635
|
+
issueListInflight.delete(input.requestKey.key);
|
|
1636
|
+
}
|
|
1637
|
+
decrementIssueListActorClientInflight(actorClientKey);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function estimatedJsonBytes(value) {
|
|
1641
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
1642
|
+
}
|
|
1643
|
+
function logIssueListRequest(input) {
|
|
1644
|
+
input.res.once("finish", () => {
|
|
1645
|
+
const contentEncoding = input.res.getHeader("content-encoding");
|
|
1646
|
+
const contentLength = Number(input.res.getHeader("content-length"));
|
|
1647
|
+
logger.debug({
|
|
1648
|
+
event: "safe_get_request_observed",
|
|
1649
|
+
route: input.requestKey.route,
|
|
1650
|
+
companyId: input.companyId,
|
|
1651
|
+
actorType: input.requestKey.actor.actorType,
|
|
1652
|
+
actorIdentityHash: input.requestKey.actor.hash,
|
|
1653
|
+
clientHash: input.requestKey.client.hash,
|
|
1654
|
+
cacheKeyHash: input.requestKey.keyHash,
|
|
1655
|
+
queryKeys: input.requestKey.queryKeys,
|
|
1656
|
+
requestCount: input.identicalInFlightCount,
|
|
1657
|
+
durationMs: Date.now() - input.startedAt,
|
|
1658
|
+
statusCode: input.res.statusCode,
|
|
1659
|
+
responseBytes: input.bodyBytes,
|
|
1660
|
+
compressedBytes: contentEncoding && Number.isFinite(contentLength) ? contentLength : null,
|
|
1661
|
+
contentEncoding: contentEncoding ? String(contentEncoding) : null,
|
|
1662
|
+
cacheStatus: input.cacheStatus,
|
|
1663
|
+
etagOutcome: input.etagOutcome,
|
|
1664
|
+
referer: safeRefererPath(input.req),
|
|
1665
|
+
visibilityHint: input.req.header("x-paperclip-tab-visible") ?? null,
|
|
1666
|
+
}, "safe authenticated GET observed");
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
805
1669
|
export function issueRoutes(db, storage, opts = {}) {
|
|
806
1670
|
const router = Router();
|
|
807
1671
|
const svc = issueService(db);
|
|
@@ -826,6 +1690,7 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
826
1690
|
const executionWorkspacesSvc = executionWorkspaceServiceDirect(db);
|
|
827
1691
|
const workProductsSvc = workProductService(db);
|
|
828
1692
|
const documentsSvc = documentService(db);
|
|
1693
|
+
const companySkillsSvc = companySkillService(db);
|
|
829
1694
|
const documentAnnotationsSvc = documentAnnotationService(db);
|
|
830
1695
|
const issueReferencesSvc = issueReferenceService(db);
|
|
831
1696
|
const issueThreadInteractionsSvc = issueThreadInteractionService(db);
|
|
@@ -1430,11 +2295,14 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
1430
2295
|
});
|
|
1431
2296
|
if (decision.allowed)
|
|
1432
2297
|
return;
|
|
1433
|
-
throw forbidden(decision.explanation);
|
|
2298
|
+
throw forbidden(decision.explanation, authorizationDeniedDetails(decision));
|
|
1434
2299
|
}
|
|
1435
2300
|
function isTaskBridgeKeyActor(req) {
|
|
1436
2301
|
return req.actor.type === "agent" && req.actor.source === "agent_key" && req.actor.keyScope?.kind === "task_bridge";
|
|
1437
2302
|
}
|
|
2303
|
+
function isSkillTestScopedActor(req) {
|
|
2304
|
+
return req.actor.type === "agent" && req.actor.keyScope?.kind === "skill_test";
|
|
2305
|
+
}
|
|
1438
2306
|
function taskBridgeOriginForActor(req) {
|
|
1439
2307
|
return isTaskBridgeKeyActor(req) && req.actor.keyId
|
|
1440
2308
|
? { originKind: "task_bridge", originId: req.actor.keyId }
|
|
@@ -2268,6 +3136,136 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2268
3136
|
}
|
|
2269
3137
|
return { project, goal: null };
|
|
2270
3138
|
}
|
|
3139
|
+
function compactIssueProjectWorkspace(workspace) {
|
|
3140
|
+
if (!workspace)
|
|
3141
|
+
return null;
|
|
3142
|
+
return {
|
|
3143
|
+
id: workspace.id,
|
|
3144
|
+
companyId: workspace.companyId,
|
|
3145
|
+
projectId: workspace.projectId,
|
|
3146
|
+
name: workspace.name,
|
|
3147
|
+
sourceType: workspace.sourceType,
|
|
3148
|
+
cwd: workspace.cwd,
|
|
3149
|
+
repoUrl: workspace.repoUrl,
|
|
3150
|
+
repoRef: workspace.repoRef,
|
|
3151
|
+
defaultRef: workspace.defaultRef,
|
|
3152
|
+
visibility: workspace.visibility,
|
|
3153
|
+
setupCommand: workspace.setupCommand,
|
|
3154
|
+
cleanupCommand: workspace.cleanupCommand,
|
|
3155
|
+
remoteProvider: workspace.remoteProvider,
|
|
3156
|
+
remoteWorkspaceRef: workspace.remoteWorkspaceRef,
|
|
3157
|
+
sharedWorkspaceKey: workspace.sharedWorkspaceKey,
|
|
3158
|
+
runtimeConfig: workspace.runtimeConfig,
|
|
3159
|
+
isPrimary: workspace.isPrimary,
|
|
3160
|
+
createdAt: workspace.createdAt,
|
|
3161
|
+
updatedAt: workspace.updatedAt,
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
function compactIssueProject(project) {
|
|
3165
|
+
if (!project)
|
|
3166
|
+
return null;
|
|
3167
|
+
return {
|
|
3168
|
+
id: project.id,
|
|
3169
|
+
companyId: project.companyId,
|
|
3170
|
+
urlKey: project.urlKey,
|
|
3171
|
+
goalId: project.goalId,
|
|
3172
|
+
goalIds: project.goalIds,
|
|
3173
|
+
goals: project.goals,
|
|
3174
|
+
name: project.name,
|
|
3175
|
+
description: project.description,
|
|
3176
|
+
status: project.status,
|
|
3177
|
+
leadAgentId: project.leadAgentId,
|
|
3178
|
+
targetDate: project.targetDate,
|
|
3179
|
+
color: project.color,
|
|
3180
|
+
icon: project.icon,
|
|
3181
|
+
env: null,
|
|
3182
|
+
pauseReason: project.pauseReason,
|
|
3183
|
+
pausedAt: project.pausedAt,
|
|
3184
|
+
executionWorkspacePolicy: project.executionWorkspacePolicy,
|
|
3185
|
+
codebase: project.codebase,
|
|
3186
|
+
workspaces: (project.workspaces ?? []).map(compactIssueProjectWorkspace),
|
|
3187
|
+
primaryWorkspace: compactIssueProjectWorkspace(project.primaryWorkspace),
|
|
3188
|
+
managedByPlugin: project.managedByPlugin ?? null,
|
|
3189
|
+
taskCount: project.taskCount,
|
|
3190
|
+
budget: project.budget,
|
|
3191
|
+
archivedAt: project.archivedAt,
|
|
3192
|
+
createdAt: project.createdAt,
|
|
3193
|
+
updatedAt: project.updatedAt,
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
function compactIssueRuntimeService(service) {
|
|
3197
|
+
return {
|
|
3198
|
+
id: service.id,
|
|
3199
|
+
companyId: service.companyId,
|
|
3200
|
+
projectId: service.projectId,
|
|
3201
|
+
projectWorkspaceId: service.projectWorkspaceId,
|
|
3202
|
+
executionWorkspaceId: service.executionWorkspaceId,
|
|
3203
|
+
issueId: service.issueId,
|
|
3204
|
+
scopeType: service.scopeType,
|
|
3205
|
+
scopeId: service.scopeId,
|
|
3206
|
+
serviceName: service.serviceName,
|
|
3207
|
+
status: service.status,
|
|
3208
|
+
lifecycle: service.lifecycle,
|
|
3209
|
+
reuseKey: service.reuseKey,
|
|
3210
|
+
command: service.command,
|
|
3211
|
+
cwd: service.cwd,
|
|
3212
|
+
port: service.port,
|
|
3213
|
+
url: service.url,
|
|
3214
|
+
provider: service.provider,
|
|
3215
|
+
providerRef: service.providerRef,
|
|
3216
|
+
ownerAgentId: service.ownerAgentId,
|
|
3217
|
+
startedByRunId: service.startedByRunId,
|
|
3218
|
+
lastUsedAt: service.lastUsedAt,
|
|
3219
|
+
startedAt: service.startedAt,
|
|
3220
|
+
stoppedAt: service.stoppedAt,
|
|
3221
|
+
healthStatus: service.healthStatus,
|
|
3222
|
+
configIndex: service.configIndex ?? null,
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
function compactIssueExecutionWorkspace(workspace) {
|
|
3226
|
+
if (!workspace)
|
|
3227
|
+
return null;
|
|
3228
|
+
return {
|
|
3229
|
+
id: workspace.id,
|
|
3230
|
+
companyId: workspace.companyId,
|
|
3231
|
+
projectId: workspace.projectId,
|
|
3232
|
+
projectWorkspaceId: workspace.projectWorkspaceId,
|
|
3233
|
+
sourceIssueId: workspace.sourceIssueId,
|
|
3234
|
+
mode: workspace.mode,
|
|
3235
|
+
strategyType: workspace.strategyType,
|
|
3236
|
+
name: workspace.name,
|
|
3237
|
+
status: workspace.status,
|
|
3238
|
+
cwd: workspace.cwd,
|
|
3239
|
+
repoUrl: workspace.repoUrl,
|
|
3240
|
+
baseRef: workspace.baseRef,
|
|
3241
|
+
branchName: workspace.branchName,
|
|
3242
|
+
providerType: workspace.providerType,
|
|
3243
|
+
providerRef: workspace.providerRef,
|
|
3244
|
+
derivedFromExecutionWorkspaceId: workspace.derivedFromExecutionWorkspaceId,
|
|
3245
|
+
lastUsedAt: workspace.lastUsedAt,
|
|
3246
|
+
openedAt: workspace.openedAt,
|
|
3247
|
+
closedAt: workspace.closedAt,
|
|
3248
|
+
cleanupEligibleAt: workspace.cleanupEligibleAt,
|
|
3249
|
+
cleanupReason: workspace.cleanupReason,
|
|
3250
|
+
config: workspace.config
|
|
3251
|
+
? {
|
|
3252
|
+
environmentId: workspace.config.environmentId,
|
|
3253
|
+
provisionCommand: workspace.config.provisionCommand,
|
|
3254
|
+
teardownCommand: workspace.config.teardownCommand,
|
|
3255
|
+
cleanupCommand: workspace.config.cleanupCommand,
|
|
3256
|
+
workspaceRuntime: workspace.config.workspaceRuntime,
|
|
3257
|
+
desiredState: workspace.config.desiredState,
|
|
3258
|
+
serviceStates: workspace.config.serviceStates,
|
|
3259
|
+
}
|
|
3260
|
+
: null,
|
|
3261
|
+
metadata: null,
|
|
3262
|
+
runtimeServices: (workspace.runtimeServices ?? [])
|
|
3263
|
+
.filter((service) => service.status === "starting" || service.status === "running")
|
|
3264
|
+
.map(compactIssueRuntimeService),
|
|
3265
|
+
createdAt: workspace.createdAt,
|
|
3266
|
+
updatedAt: workspace.updatedAt,
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
2271
3269
|
// Resolve issue identifiers (e.g. "PAP-39") to UUIDs for all /issues/:id routes
|
|
2272
3270
|
router.param("id", async (req, res, next, rawId) => {
|
|
2273
3271
|
try {
|
|
@@ -2306,7 +3304,21 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2306
3304
|
res.status(403).json({ error: "Company search is outside this actor's authorization boundary" });
|
|
2307
3305
|
return;
|
|
2308
3306
|
}
|
|
2309
|
-
const
|
|
3307
|
+
const parsedQuery = companySearchQuerySchema.safeParse(req.query);
|
|
3308
|
+
if (!parsedQuery.success) {
|
|
3309
|
+
res.status(400).json({
|
|
3310
|
+
error: parsedQuery.error.issues[0]?.message ?? "Invalid search query",
|
|
3311
|
+
});
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
let query = parsedQuery.data;
|
|
3315
|
+
if (query.assigneeUserId === "me") {
|
|
3316
|
+
if (req.actor.type !== "board" || !req.actor.userId) {
|
|
3317
|
+
res.status(403).json({ error: "assigneeUserId=me requires board authentication" });
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
query = { ...query, assigneeUserId: req.actor.userId };
|
|
3321
|
+
}
|
|
2310
3322
|
const rateLimit = searchRateLimiter.consume(companySearchRateLimitActor(req, companyId));
|
|
2311
3323
|
res.setHeader("X-RateLimit-Limit", String(rateLimit.limit));
|
|
2312
3324
|
res.setHeader("X-RateLimit-Remaining", String(rateLimit.remaining));
|
|
@@ -2322,6 +3334,7 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2322
3334
|
res.json(result);
|
|
2323
3335
|
});
|
|
2324
3336
|
router.get("/companies/:companyId/issues", async (req, res) => {
|
|
3337
|
+
const startedAt = Date.now();
|
|
2325
3338
|
const companyId = req.params.companyId;
|
|
2326
3339
|
assertCompanyAccess(req, companyId);
|
|
2327
3340
|
if (isTaskBridgeKeyActor(req)) {
|
|
@@ -2356,6 +3369,8 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2356
3369
|
const attention = req.query.attention;
|
|
2357
3370
|
const sortField = req.query.sortField;
|
|
2358
3371
|
const sortDir = req.query.sortDir;
|
|
3372
|
+
const view = req.query.view;
|
|
3373
|
+
const compactView = view === "compact";
|
|
2359
3374
|
const hasPlanDocument = parseOptionalBooleanQuery(req.query.hasPlanDocument);
|
|
2360
3375
|
const includeLiveDescendantSummary = parseOptionalBooleanQuery(req.query.includeLiveDescendantSummary);
|
|
2361
3376
|
const assigneeAgentFilterRaw = req.query.assigneeAgentId;
|
|
@@ -2380,6 +3395,10 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2380
3395
|
res.status(400).json({ error: "attention must be 'blocked' when provided" });
|
|
2381
3396
|
return;
|
|
2382
3397
|
}
|
|
3398
|
+
if (view !== undefined && view !== "compact") {
|
|
3399
|
+
res.status(400).json({ error: "view must be 'compact' when provided" });
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
2383
3402
|
if (rawLimit !== undefined && (parsedLimit === null || !Number.isInteger(parsedLimit) || parsedLimit <= 0)) {
|
|
2384
3403
|
res.status(400).json({ error: `limit must be a positive integer up to ${ISSUE_LIST_MAX_LIMIT}` });
|
|
2385
3404
|
return;
|
|
@@ -2425,7 +3444,7 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2425
3444
|
}
|
|
2426
3445
|
}
|
|
2427
3446
|
const offset = parsedOffset ?? 0;
|
|
2428
|
-
const
|
|
3447
|
+
const listFilters = {
|
|
2429
3448
|
attention: attention === "blocked" ? "blocked" : undefined,
|
|
2430
3449
|
status: req.query.status,
|
|
2431
3450
|
assigneeAgentId,
|
|
@@ -2455,36 +3474,145 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2455
3474
|
offset,
|
|
2456
3475
|
sortField: sortField === "updated" ? "updated" : undefined,
|
|
2457
3476
|
sortDir: sortDir === "asc" || sortDir === "desc" ? sortDir : undefined,
|
|
3477
|
+
};
|
|
3478
|
+
const requestKey = issueListRequestKey({
|
|
3479
|
+
req,
|
|
3480
|
+
companyId,
|
|
3481
|
+
normalizedQuery: {
|
|
3482
|
+
...listFilters,
|
|
3483
|
+
view: compactView ? "compact" : undefined,
|
|
3484
|
+
},
|
|
2458
3485
|
});
|
|
2459
|
-
const
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
3486
|
+
const coordinated = await coordinateIssueListGet({
|
|
3487
|
+
req,
|
|
3488
|
+
companyId,
|
|
3489
|
+
requestKey,
|
|
3490
|
+
allowTtlCache: compactView,
|
|
3491
|
+
diagnostics: opts.issueListDiagnostics,
|
|
3492
|
+
compute: async () => {
|
|
3493
|
+
const rawResult = await svc.list(companyId, listFilters);
|
|
3494
|
+
const result = await actorCanReadCompanyScope(req, companyId)
|
|
3495
|
+
? rawResult
|
|
3496
|
+
: await filterIssuesForActor(req, rawResult);
|
|
3497
|
+
const issueIds = result.map((issue) => issue.id);
|
|
3498
|
+
if (compactView) {
|
|
3499
|
+
const [handoffStates, recoveryActionByIssue] = await Promise.all([
|
|
3500
|
+
listSuccessfulRunHandoffStates(db, companyId, issueIds),
|
|
3501
|
+
recoveryActionsSvc.listActiveForIssues(companyId, issueIds),
|
|
3502
|
+
]);
|
|
3503
|
+
const actor = getActorInfo(req);
|
|
3504
|
+
await Promise.all(result.map(async (issue) => {
|
|
3505
|
+
const activeRecoveryAction = recoveryActionByIssue.get(issue.id) ?? null;
|
|
3506
|
+
if (!activeRecoveryAction)
|
|
3507
|
+
return;
|
|
3508
|
+
const revalidated = await revalidateActiveSourceRecoveryForRead({
|
|
3509
|
+
issue,
|
|
3510
|
+
trigger: "read_projection",
|
|
3511
|
+
actor,
|
|
3512
|
+
activeRecoveryAction,
|
|
3513
|
+
});
|
|
3514
|
+
if (revalidated)
|
|
3515
|
+
recoveryActionByIssue.set(issue.id, revalidated);
|
|
3516
|
+
else
|
|
3517
|
+
recoveryActionByIssue.delete(issue.id);
|
|
3518
|
+
}));
|
|
3519
|
+
const compactResult = result.map((issue) => toCompactIssue({
|
|
3520
|
+
...issue,
|
|
3521
|
+
activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null,
|
|
3522
|
+
successfulRunHandoff: handoffStates.get(issue.id) ?? null,
|
|
3523
|
+
}));
|
|
3524
|
+
return {
|
|
3525
|
+
kind: "compact",
|
|
3526
|
+
body: compactResult,
|
|
3527
|
+
etag: compactIssueListEtag(compactResult),
|
|
3528
|
+
cacheControl: "private, must-revalidate",
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
const [handoffStates, recoveryActionByIssue] = await Promise.all([
|
|
3532
|
+
listSuccessfulRunHandoffStates(db, companyId, issueIds),
|
|
3533
|
+
recoveryActionsSvc.listActiveForIssues(companyId, issueIds),
|
|
3534
|
+
]);
|
|
3535
|
+
const actor = getActorInfo(req);
|
|
3536
|
+
await Promise.all(result.map(async (issue) => {
|
|
3537
|
+
const activeRecoveryAction = recoveryActionByIssue.get(issue.id) ?? null;
|
|
3538
|
+
if (!activeRecoveryAction)
|
|
3539
|
+
return;
|
|
3540
|
+
const revalidated = await revalidateActiveSourceRecoveryForRead({
|
|
3541
|
+
issue,
|
|
3542
|
+
trigger: "read_projection",
|
|
3543
|
+
actor,
|
|
3544
|
+
activeRecoveryAction,
|
|
3545
|
+
});
|
|
3546
|
+
if (revalidated)
|
|
3547
|
+
recoveryActionByIssue.set(issue.id, revalidated);
|
|
3548
|
+
else
|
|
3549
|
+
recoveryActionByIssue.delete(issue.id);
|
|
3550
|
+
}));
|
|
3551
|
+
return {
|
|
3552
|
+
kind: "full",
|
|
3553
|
+
body: result.map((issue) => ({
|
|
3554
|
+
...issue,
|
|
3555
|
+
successfulRunHandoff: handoffStates.get(issue.id) ?? null,
|
|
3556
|
+
activeRecoveryAction: recoveryActionByIssue.get(issue.id) ?? null,
|
|
3557
|
+
})),
|
|
3558
|
+
};
|
|
3559
|
+
},
|
|
3560
|
+
});
|
|
3561
|
+
res.setHeader("X-Paperclip-Request-Cache", coordinated.cacheStatus);
|
|
3562
|
+
if (!coordinated.response) {
|
|
3563
|
+
const body = {
|
|
3564
|
+
error: "Too many concurrent issue-list requests for this actor/client",
|
|
3565
|
+
retryAfterSeconds: coordinated.retryAfterSeconds ?? 1,
|
|
3566
|
+
};
|
|
3567
|
+
res.setHeader("Retry-After", String(body.retryAfterSeconds));
|
|
3568
|
+
logIssueListRequest({
|
|
3569
|
+
req,
|
|
3570
|
+
res,
|
|
3571
|
+
companyId,
|
|
3572
|
+
requestKey,
|
|
3573
|
+
startedAt,
|
|
3574
|
+
cacheStatus: "retry",
|
|
3575
|
+
bodyBytes: estimatedJsonBytes(body),
|
|
3576
|
+
etagOutcome: "none",
|
|
3577
|
+
identicalInFlightCount: coordinated.identicalInFlightCount,
|
|
2477
3578
|
});
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
3579
|
+
res.status(429).json(body);
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
if (coordinated.response.kind === "compact") {
|
|
3583
|
+
res.setHeader("Cache-Control", coordinated.response.cacheControl);
|
|
3584
|
+
res.setHeader("ETag", coordinated.response.etag);
|
|
3585
|
+
const etagMatched = requestMatchesEtag(req.header("if-none-match"), coordinated.response.etag);
|
|
3586
|
+
logIssueListRequest({
|
|
3587
|
+
req,
|
|
3588
|
+
res,
|
|
3589
|
+
companyId,
|
|
3590
|
+
requestKey,
|
|
3591
|
+
startedAt,
|
|
3592
|
+
cacheStatus: coordinated.cacheStatus,
|
|
3593
|
+
bodyBytes: etagMatched ? 0 : estimatedJsonBytes(coordinated.response.body),
|
|
3594
|
+
etagOutcome: etagMatched ? "not_modified" : "fresh",
|
|
3595
|
+
identicalInFlightCount: coordinated.identicalInFlightCount,
|
|
3596
|
+
});
|
|
3597
|
+
if (etagMatched) {
|
|
3598
|
+
res.status(304).end();
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3601
|
+
res.json(coordinated.response.body);
|
|
3602
|
+
return;
|
|
3603
|
+
}
|
|
3604
|
+
logIssueListRequest({
|
|
3605
|
+
req,
|
|
3606
|
+
res,
|
|
3607
|
+
companyId,
|
|
3608
|
+
requestKey,
|
|
3609
|
+
startedAt,
|
|
3610
|
+
cacheStatus: coordinated.cacheStatus,
|
|
3611
|
+
bodyBytes: estimatedJsonBytes(coordinated.response.body),
|
|
3612
|
+
etagOutcome: "none",
|
|
3613
|
+
identicalInFlightCount: coordinated.identicalInFlightCount,
|
|
3614
|
+
});
|
|
3615
|
+
res.json(coordinated.response.body);
|
|
2488
3616
|
});
|
|
2489
3617
|
router.get("/companies/:companyId/issues/count", async (req, res) => {
|
|
2490
3618
|
const companyId = req.params.companyId;
|
|
@@ -2745,6 +3873,126 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2745
3873
|
currentExecutionWorkspace,
|
|
2746
3874
|
});
|
|
2747
3875
|
});
|
|
3876
|
+
router.get("/issues/:id/diagnostics/blockers", async (req, res) => {
|
|
3877
|
+
const id = req.params.id;
|
|
3878
|
+
const issue = await svc.getById(id);
|
|
3879
|
+
if (!issue) {
|
|
3880
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3883
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3884
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
3885
|
+
return;
|
|
3886
|
+
const diagnostic = await svc.getBlockerDiagnostics(issue.id);
|
|
3887
|
+
const visibleBlockers = await filterIssuesForActor(req, diagnostic.blockers);
|
|
3888
|
+
const response = buildIssueBlockerDiagnosticsResponse({
|
|
3889
|
+
issue,
|
|
3890
|
+
blockers: diagnostic.blockers,
|
|
3891
|
+
visibleBlockers,
|
|
3892
|
+
readiness: diagnostic.readiness,
|
|
3893
|
+
truncated: diagnostic.truncated,
|
|
3894
|
+
});
|
|
3895
|
+
logger.info({
|
|
3896
|
+
companyId: issue.companyId,
|
|
3897
|
+
issueId: issue.id,
|
|
3898
|
+
actorType: req.actor.type,
|
|
3899
|
+
visibleBlockerCount: response.blockers.length,
|
|
3900
|
+
omittedUnauthorizedBlockerCount: response.omittedUnauthorizedBlockerCount,
|
|
3901
|
+
truncated: response.truncated,
|
|
3902
|
+
}, "issue blocker diagnostics read");
|
|
3903
|
+
res.json(response);
|
|
3904
|
+
});
|
|
3905
|
+
router.get("/issues/:id/diagnostics/wakes", async (req, res) => {
|
|
3906
|
+
const id = req.params.id;
|
|
3907
|
+
const issue = await svc.getById(id);
|
|
3908
|
+
if (!issue) {
|
|
3909
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3912
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3913
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
3914
|
+
return;
|
|
3915
|
+
const [wakeDiagnostic, blockerDiagnostic, includeInternalIds] = await Promise.all([
|
|
3916
|
+
svc.getWakeDiagnostics(issue.id),
|
|
3917
|
+
svc.getBlockerDiagnostics(issue.id),
|
|
3918
|
+
actorCanReadCompanyScope(req, issue.companyId),
|
|
3919
|
+
]);
|
|
3920
|
+
const visibleBlockers = await filterIssuesForActor(req, blockerDiagnostic.blockers);
|
|
3921
|
+
const blockerResponse = buildIssueBlockerDiagnosticsResponse({
|
|
3922
|
+
issue,
|
|
3923
|
+
blockers: blockerDiagnostic.blockers,
|
|
3924
|
+
visibleBlockers,
|
|
3925
|
+
readiness: blockerDiagnostic.readiness,
|
|
3926
|
+
truncated: blockerDiagnostic.truncated,
|
|
3927
|
+
});
|
|
3928
|
+
const response = buildIssueWakeDiagnosticsResponse({
|
|
3929
|
+
issue,
|
|
3930
|
+
wakeRequests: wakeDiagnostic.wakeRequests,
|
|
3931
|
+
activityRecords: wakeDiagnostic.activityRecords,
|
|
3932
|
+
blockerDiagnostics: blockerResponse,
|
|
3933
|
+
truncatedWakeRequests: wakeDiagnostic.truncatedWakeRequests,
|
|
3934
|
+
truncatedActivityRecords: wakeDiagnostic.truncatedActivityRecords,
|
|
3935
|
+
includeInternalIds,
|
|
3936
|
+
});
|
|
3937
|
+
logger.info({
|
|
3938
|
+
companyId: issue.companyId,
|
|
3939
|
+
issueId: issue.id,
|
|
3940
|
+
actorType: req.actor.type,
|
|
3941
|
+
wakeRequestCount: response.wakeRequestCount,
|
|
3942
|
+
activityRecordCount: response.activityRecordCount,
|
|
3943
|
+
internalIdsIncluded: includeInternalIds,
|
|
3944
|
+
truncated: response.truncated,
|
|
3945
|
+
}, "issue wake diagnostics read");
|
|
3946
|
+
res.json(response);
|
|
3947
|
+
});
|
|
3948
|
+
router.get("/issues/:id/diagnostics/subtree", async (req, res) => {
|
|
3949
|
+
const id = req.params.id;
|
|
3950
|
+
const issue = await svc.getById(id);
|
|
3951
|
+
if (!issue) {
|
|
3952
|
+
res.status(404).json({ error: "Issue not found" });
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
assertCompanyAccess(req, issue.companyId);
|
|
3956
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
3957
|
+
return;
|
|
3958
|
+
const [diagnostic, includeInternalIds] = await Promise.all([
|
|
3959
|
+
svc.getSubtreeDiagnostics(issue.id),
|
|
3960
|
+
actorCanReadCompanyScope(req, issue.companyId),
|
|
3961
|
+
]);
|
|
3962
|
+
const allBlockers = [...diagnostic.blockersByIssueId.values()].flat();
|
|
3963
|
+
const [visibleNodes, visibleBlockers] = await Promise.all([
|
|
3964
|
+
filterIssuesForActor(req, diagnostic.nodes),
|
|
3965
|
+
filterIssuesForActor(req, allBlockers),
|
|
3966
|
+
]);
|
|
3967
|
+
const response = buildIssueSubtreeDiagnosticsResponse({
|
|
3968
|
+
issue,
|
|
3969
|
+
nodes: diagnostic.nodes,
|
|
3970
|
+
visibleNodes,
|
|
3971
|
+
blockersByIssueId: diagnostic.blockersByIssueId,
|
|
3972
|
+
visibleBlockers,
|
|
3973
|
+
readinessByIssueId: diagnostic.readinessByIssueId,
|
|
3974
|
+
wakeRequestsByIssueId: diagnostic.wakeRequestsByIssueId,
|
|
3975
|
+
activityRecordsByIssueId: diagnostic.activityRecordsByIssueId,
|
|
3976
|
+
truncatedNodes: diagnostic.truncatedNodes,
|
|
3977
|
+
truncatedDepth: diagnostic.truncatedDepth,
|
|
3978
|
+
truncatedBlockerIssueIds: diagnostic.truncatedBlockerIssueIds,
|
|
3979
|
+
truncatedWakeIssueIds: diagnostic.truncatedWakeIssueIds,
|
|
3980
|
+
truncatedActivityIssueIds: diagnostic.truncatedActivityIssueIds,
|
|
3981
|
+
includeInternalIds,
|
|
3982
|
+
caps: diagnostic.caps,
|
|
3983
|
+
});
|
|
3984
|
+
logger.info({
|
|
3985
|
+
companyId: issue.companyId,
|
|
3986
|
+
issueId: issue.id,
|
|
3987
|
+
actorType: req.actor.type,
|
|
3988
|
+
nodeCount: response.nodeCount,
|
|
3989
|
+
omittedUnauthorizedNodeCount: response.omittedUnauthorizedNodeCount,
|
|
3990
|
+
edgeCount: response.edges.length,
|
|
3991
|
+
internalIdsIncluded: includeInternalIds,
|
|
3992
|
+
truncated: response.truncated,
|
|
3993
|
+
}, "issue subtree diagnostics read");
|
|
3994
|
+
res.json(response);
|
|
3995
|
+
});
|
|
2748
3996
|
router.get("/issues/:id", async (req, res) => {
|
|
2749
3997
|
const id = req.params.id;
|
|
2750
3998
|
const issue = await svc.getById(id);
|
|
@@ -2798,10 +4046,10 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2798
4046
|
relatedWork: referenceSummary,
|
|
2799
4047
|
referencedIssueIdentifiers: referenceSummary.outbound.map((item) => item.issue.identifier ?? item.issue.id),
|
|
2800
4048
|
...documentPayload,
|
|
2801
|
-
project: project
|
|
4049
|
+
project: compactIssueProject(project),
|
|
2802
4050
|
goal: goal ?? null,
|
|
2803
4051
|
mentionedProjects,
|
|
2804
|
-
currentExecutionWorkspace,
|
|
4052
|
+
currentExecutionWorkspace: compactIssueExecutionWorkspace(currentExecutionWorkspace),
|
|
2805
4053
|
workProducts,
|
|
2806
4054
|
linkedCases,
|
|
2807
4055
|
});
|
|
@@ -2914,6 +4162,8 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
2914
4162
|
return;
|
|
2915
4163
|
}
|
|
2916
4164
|
assertCompanyAccess(req, issue.companyId);
|
|
4165
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
4166
|
+
return;
|
|
2917
4167
|
const active = await revalidateActiveSourceRecoveryForRead({
|
|
2918
4168
|
issue,
|
|
2919
4169
|
trigger: "read_projection",
|
|
@@ -3077,6 +4327,8 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
3077
4327
|
return;
|
|
3078
4328
|
}
|
|
3079
4329
|
assertCompanyAccess(req, issue.companyId);
|
|
4330
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
4331
|
+
return;
|
|
3080
4332
|
const objects = await externalObjectsSvc.listForIssue(issue.id);
|
|
3081
4333
|
res.json(objects);
|
|
3082
4334
|
});
|
|
@@ -3088,13 +4340,31 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
3088
4340
|
return;
|
|
3089
4341
|
}
|
|
3090
4342
|
assertCompanyAccess(req, issue.companyId);
|
|
4343
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
4344
|
+
return;
|
|
3091
4345
|
const summary = await externalObjectsSvc.getIssueSummary(issue.id);
|
|
3092
4346
|
res.json(summary);
|
|
3093
4347
|
});
|
|
3094
4348
|
router.post("/companies/:companyId/issues/external-object-summaries", validate(externalObjectSummariesSchema), async (req, res) => {
|
|
3095
4349
|
const companyId = req.params.companyId;
|
|
3096
4350
|
assertCompanyAccess(req, companyId);
|
|
3097
|
-
const
|
|
4351
|
+
const requestedIssueIds = [...new Set(req.body.issueIds)];
|
|
4352
|
+
const candidateIssues = requestedIssueIds.length > 0
|
|
4353
|
+
? await db
|
|
4354
|
+
.select({
|
|
4355
|
+
id: issueRows.id,
|
|
4356
|
+
companyId: issueRows.companyId,
|
|
4357
|
+
projectId: issueRows.projectId,
|
|
4358
|
+
parentId: issueRows.parentId,
|
|
4359
|
+
assigneeAgentId: issueRows.assigneeAgentId,
|
|
4360
|
+
assigneeUserId: issueRows.assigneeUserId,
|
|
4361
|
+
status: issueRows.status,
|
|
4362
|
+
})
|
|
4363
|
+
.from(issueRows)
|
|
4364
|
+
.where(and(eq(issueRows.companyId, companyId), inArray(issueRows.id, requestedIssueIds)))
|
|
4365
|
+
: [];
|
|
4366
|
+
const readableIssueIds = (await filterIssuesForActor(req, candidateIssues)).map((issue) => issue.id);
|
|
4367
|
+
const summaries = await externalObjectsSvc.getIssueSummaries(companyId, readableIssueIds);
|
|
3098
4368
|
res.json({ summaries: Object.fromEntries(summaries) });
|
|
3099
4369
|
});
|
|
3100
4370
|
router.post("/issues/:id/external-objects/refresh", validate(refreshExternalObjectsSchema), async (req, res) => {
|
|
@@ -3182,6 +4452,8 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
3182
4452
|
return;
|
|
3183
4453
|
}
|
|
3184
4454
|
assertCompanyAccess(req, issue.companyId);
|
|
4455
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
4456
|
+
return;
|
|
3185
4457
|
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
|
|
3186
4458
|
if (!keyParsed.success) {
|
|
3187
4459
|
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
|
|
@@ -3251,6 +4523,8 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
3251
4523
|
return;
|
|
3252
4524
|
}
|
|
3253
4525
|
assertCompanyAccess(req, issue.companyId);
|
|
4526
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
4527
|
+
return;
|
|
3254
4528
|
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
|
|
3255
4529
|
if (!keyParsed.success) {
|
|
3256
4530
|
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
|
|
@@ -3555,6 +4829,8 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
3555
4829
|
return;
|
|
3556
4830
|
}
|
|
3557
4831
|
assertCompanyAccess(req, issue.companyId);
|
|
4832
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
4833
|
+
return;
|
|
3558
4834
|
const keyParsed = issueDocumentKeySchema.safeParse(String(req.params.key ?? "").trim().toLowerCase());
|
|
3559
4835
|
if (!keyParsed.success) {
|
|
3560
4836
|
res.status(400).json({ error: "Invalid document key", details: keyParsed.error.issues });
|
|
@@ -4164,6 +5440,8 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4164
5440
|
assertCompanyAccess(req, issue.companyId);
|
|
4165
5441
|
if (await assertLowTrustControlPlaneDenied(req, res, issue.companyId, issue))
|
|
4166
5442
|
return;
|
|
5443
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
5444
|
+
return;
|
|
4167
5445
|
const approvals = await issueApprovalsSvc.listApprovalsForIssue(id);
|
|
4168
5446
|
res.json(approvals);
|
|
4169
5447
|
});
|
|
@@ -4233,10 +5511,25 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4233
5511
|
router.post("/companies/:companyId/issues", applyCreateIssueStatusDefault, validate(createIssueSchema), async (req, res) => {
|
|
4234
5512
|
const companyId = req.params.companyId;
|
|
4235
5513
|
assertCompanyAccess(req, companyId);
|
|
5514
|
+
if (isSkillTestScopedActor(req)) {
|
|
5515
|
+
res.status(403).json({
|
|
5516
|
+
error: "Skill-test run tokens cannot create issues.",
|
|
5517
|
+
details: {
|
|
5518
|
+
scopedIssueId: req.actor.keyScope?.kind === "skill_test" ? req.actor.keyScope.issueId : null,
|
|
5519
|
+
securityPrinciples: ["Least Privilege", "Complete Mediation", "Fail Securely"],
|
|
5520
|
+
},
|
|
5521
|
+
});
|
|
5522
|
+
return;
|
|
5523
|
+
}
|
|
4236
5524
|
if (await assertLowTrustControlPlaneDenied(req, res, companyId, null))
|
|
4237
5525
|
return;
|
|
4238
5526
|
assertNoAgentHostWorkspaceCommandMutation(req, collectIssueWorkspaceCommandPaths(req.body));
|
|
4239
|
-
const
|
|
5527
|
+
const sanitizedBody = await sanitizeIssueCreateAttribution(db, req, res, companyId, req.body, {
|
|
5528
|
+
surface: "issues.create",
|
|
5529
|
+
});
|
|
5530
|
+
if (!sanitizedBody)
|
|
5531
|
+
return;
|
|
5532
|
+
const { watchdogDiscovery: rawWatchdogDiscovery, ...rawCreateBody } = sanitizedBody;
|
|
4240
5533
|
const watchdogDiscovery = normalizeWatchdogDiscovery(rawWatchdogDiscovery);
|
|
4241
5534
|
const watchdogProductBugFollowUp = await resolveTaskWatchdogProductBugFollowUp(req, res, companyId, watchdogDiscovery);
|
|
4242
5535
|
if (watchdogProductBugFollowUp === false)
|
|
@@ -4336,6 +5629,9 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4336
5629
|
...(sourceTrust ? { sourceTrust } : {}),
|
|
4337
5630
|
createdByAgentId: actor.agentId,
|
|
4338
5631
|
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
5632
|
+
actorRunId: actor.runId,
|
|
5633
|
+
actorResponsibleUserId: authenticatedActorResponsibleUserId(req),
|
|
5634
|
+
trustExplicitResponsibleUserId: actor.actorType === "user",
|
|
4339
5635
|
watchdogActorRunId: actor.runId,
|
|
4340
5636
|
});
|
|
4341
5637
|
await issueReferencesSvc.syncIssue(issue.id);
|
|
@@ -4447,9 +5743,15 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4447
5743
|
if (await assertLowTrustControlPlaneDenied(req, res, parent.companyId, parent))
|
|
4448
5744
|
return;
|
|
4449
5745
|
assertNoAgentHostWorkspaceCommandMutation(req, collectIssueWorkspaceCommandPaths(req.body));
|
|
4450
|
-
const
|
|
5746
|
+
const sanitizedBody = await sanitizeIssueCreateAttribution(db, req, res, parent.companyId, req.body, {
|
|
5747
|
+
surface: "issues.children.create",
|
|
5748
|
+
entityId: parent.id,
|
|
5749
|
+
});
|
|
5750
|
+
if (!sanitizedBody)
|
|
5751
|
+
return;
|
|
5752
|
+
const normalizedAssigneeAgentId = await normalizeIssueAssigneeAgentReference(parent.companyId, sanitizedBody.assigneeAgentId);
|
|
4451
5753
|
const createBody = {
|
|
4452
|
-
...
|
|
5754
|
+
...sanitizedBody,
|
|
4453
5755
|
...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}),
|
|
4454
5756
|
};
|
|
4455
5757
|
if (!(await assertCheapRecoveryIssueAssigneeProfileAllowed(req, res, parent, createBody)))
|
|
@@ -4461,7 +5763,7 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4461
5763
|
assigneeUserId: createBody.assigneeUserId ?? null,
|
|
4462
5764
|
};
|
|
4463
5765
|
await assertTaskBridgeCreateAllowed(req, parent.companyId, childAssignmentScope);
|
|
4464
|
-
if (
|
|
5766
|
+
if (sanitizedBody.assigneeAgentId || sanitizedBody.assigneeUserId) {
|
|
4465
5767
|
await assertCanAssignTasks(req, parent.companyId, childAssignmentScope);
|
|
4466
5768
|
}
|
|
4467
5769
|
await assertIssueEnvironmentSelection(parent.companyId, createBody.executionWorkspaceSettings?.environmentId);
|
|
@@ -4493,6 +5795,9 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4493
5795
|
...(sourceTrust ? { sourceTrust } : {}),
|
|
4494
5796
|
createdByAgentId: actor.agentId,
|
|
4495
5797
|
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
5798
|
+
actorRunId: actor.runId,
|
|
5799
|
+
actorResponsibleUserId: authenticatedActorResponsibleUserId(req),
|
|
5800
|
+
trustExplicitResponsibleUserId: actor.actorType === "user",
|
|
4496
5801
|
actorAgentId: actor.agentId,
|
|
4497
5802
|
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
4498
5803
|
watchdogActorRunId: actor.runId,
|
|
@@ -4608,9 +5913,15 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4608
5913
|
return;
|
|
4609
5914
|
const requestedChildren = [];
|
|
4610
5915
|
for (const child of req.body.children) {
|
|
4611
|
-
const
|
|
5916
|
+
const sanitizedChild = await sanitizeIssueCreateAttribution(db, req, res, sourceIssue.companyId, child, {
|
|
5917
|
+
surface: "issues.accepted_plan_decomposition",
|
|
5918
|
+
entityId: sourceIssue.id,
|
|
5919
|
+
});
|
|
5920
|
+
if (!sanitizedChild)
|
|
5921
|
+
return;
|
|
5922
|
+
const normalizedAssigneeAgentId = await normalizeIssueAssigneeAgentReference(sourceIssue.companyId, sanitizedChild.assigneeAgentId);
|
|
4612
5923
|
const childBody = {
|
|
4613
|
-
...
|
|
5924
|
+
...sanitizedChild,
|
|
4614
5925
|
...(normalizedAssigneeAgentId !== undefined ? { assigneeAgentId: normalizedAssigneeAgentId } : {}),
|
|
4615
5926
|
};
|
|
4616
5927
|
requestedChildren.push(childBody);
|
|
@@ -4646,6 +5957,9 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
4646
5957
|
...(sourceTrust ? { sourceTrust } : {}),
|
|
4647
5958
|
createdByAgentId: actor.agentId,
|
|
4648
5959
|
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
5960
|
+
actorRunId: actor.runId,
|
|
5961
|
+
actorResponsibleUserId: authenticatedActorResponsibleUserId(req),
|
|
5962
|
+
trustExplicitResponsibleUserId: actor.actorType === "user",
|
|
4649
5963
|
actorAgentId: actor.agentId,
|
|
4650
5964
|
actorUserId: actor.actorType === "user" ? actor.actorId : null,
|
|
4651
5965
|
});
|
|
@@ -5449,6 +6763,33 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
5449
6763
|
}
|
|
5450
6764
|
}
|
|
5451
6765
|
}
|
|
6766
|
+
if (issue.harnessKind === "skill_test" &&
|
|
6767
|
+
existing.status !== issue.status &&
|
|
6768
|
+
(issue.status === "done" || issue.status === "cancelled")) {
|
|
6769
|
+
const completedRun = await companySkillsSvc.completeTestRunForIssue({
|
|
6770
|
+
companyId: issue.companyId,
|
|
6771
|
+
issueId: issue.id,
|
|
6772
|
+
outcome: issue.status === "done" ? "succeeded" : "cancelled",
|
|
6773
|
+
error: issue.status === "cancelled" ? "Harness issue was cancelled" : null,
|
|
6774
|
+
});
|
|
6775
|
+
if (completedRun) {
|
|
6776
|
+
await logActivity(db, {
|
|
6777
|
+
companyId: issue.companyId,
|
|
6778
|
+
actorType: actor.actorType,
|
|
6779
|
+
actorId: actor.actorId,
|
|
6780
|
+
agentId: actor.agentId,
|
|
6781
|
+
runId: actor.runId,
|
|
6782
|
+
action: "company.skill_test_run_completed",
|
|
6783
|
+
entityType: "company_skill_test_run",
|
|
6784
|
+
entityId: completedRun.id,
|
|
6785
|
+
details: {
|
|
6786
|
+
issueId: issue.id,
|
|
6787
|
+
status: completedRun.status,
|
|
6788
|
+
outputDocumentKey: completedRun.outputDocumentKey,
|
|
6789
|
+
},
|
|
6790
|
+
});
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
5452
6793
|
let comment = null;
|
|
5453
6794
|
if (commentBody) {
|
|
5454
6795
|
const commentReferenceSummaryBefore = updateReferenceSummaryAfter
|
|
@@ -5883,6 +7224,9 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
5883
7224
|
return;
|
|
5884
7225
|
const updated = await svc.checkout(id, req.body.agentId, req.body.expectedStatuses, checkoutRunId);
|
|
5885
7226
|
const actor = getActorInfo(req);
|
|
7227
|
+
if (updated?.harnessKind === "skill_test") {
|
|
7228
|
+
await companySkillsSvc.markTestRunRunning(updated.companyId, updated.id);
|
|
7229
|
+
}
|
|
5886
7230
|
await logActivity(db, {
|
|
5887
7231
|
companyId: issue.companyId,
|
|
5888
7232
|
actorType: actor.actorType,
|
|
@@ -6273,6 +7617,62 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
6273
7617
|
});
|
|
6274
7618
|
res.json(interaction);
|
|
6275
7619
|
});
|
|
7620
|
+
router.post("/issues/:id/interactions/:interactionId/verdicts", validate(submitIssueThreadInteractionVerdictsSchema), async (req, res) => {
|
|
7621
|
+
const id = req.params.id;
|
|
7622
|
+
const interactionId = req.params.interactionId;
|
|
7623
|
+
const issue = await svc.getById(id);
|
|
7624
|
+
if (!issue) {
|
|
7625
|
+
res.status(404).json({ error: "Issue not found" });
|
|
7626
|
+
return;
|
|
7627
|
+
}
|
|
7628
|
+
assertCompanyAccess(req, issue.companyId);
|
|
7629
|
+
if (await rejectAgentIssueThreadInteractionResolution(req, res, issue))
|
|
7630
|
+
return;
|
|
7631
|
+
assertBoard(req);
|
|
7632
|
+
const actor = getActorInfo(req);
|
|
7633
|
+
const { interaction, newlyResolvedItemIds } = await issueThreadInteractionService(db).submitItemVerdicts(issue, interactionId, req.body, {
|
|
7634
|
+
agentId: actor.agentId,
|
|
7635
|
+
userId: actor.actorType === "user" ? actor.actorId : null,
|
|
7636
|
+
});
|
|
7637
|
+
await logActivity(db, {
|
|
7638
|
+
companyId: issue.companyId,
|
|
7639
|
+
actorType: actor.actorType,
|
|
7640
|
+
actorId: actor.actorId,
|
|
7641
|
+
agentId: actor.agentId,
|
|
7642
|
+
runId: actor.runId,
|
|
7643
|
+
action: interaction.status === "expired"
|
|
7644
|
+
? "issue.thread_interaction_expired"
|
|
7645
|
+
: "issue.thread_interaction_item_verdicts_submitted",
|
|
7646
|
+
entityType: "issue",
|
|
7647
|
+
entityId: issue.id,
|
|
7648
|
+
details: {
|
|
7649
|
+
interactionId: interaction.id,
|
|
7650
|
+
interactionKind: interaction.kind,
|
|
7651
|
+
interactionStatus: interaction.status,
|
|
7652
|
+
submittedVerdictCount: Array.isArray(req.body?.verdicts) ? req.body.verdicts.length : 0,
|
|
7653
|
+
newlyResolvedItemCount: newlyResolvedItemIds.length,
|
|
7654
|
+
newlyResolvedItemIds,
|
|
7655
|
+
complete: interaction.kind === "request_item_verdicts"
|
|
7656
|
+
? (interaction.result?.complete ?? false)
|
|
7657
|
+
: false,
|
|
7658
|
+
},
|
|
7659
|
+
});
|
|
7660
|
+
if (newlyResolvedItemIds.length > 0) {
|
|
7661
|
+
queueResolvedInteractionContinuationWakeup({
|
|
7662
|
+
heartbeat,
|
|
7663
|
+
issue,
|
|
7664
|
+
interaction,
|
|
7665
|
+
actor,
|
|
7666
|
+
source: "issue.interaction.verdicts",
|
|
7667
|
+
newlyResolvedItemIds,
|
|
7668
|
+
idempotencyKey: buildRequestItemVerdictsWakeIdempotencyKey({
|
|
7669
|
+
issueId: issue.id,
|
|
7670
|
+
interactionId: interaction.id,
|
|
7671
|
+
}),
|
|
7672
|
+
});
|
|
7673
|
+
}
|
|
7674
|
+
res.json(interaction);
|
|
7675
|
+
});
|
|
6276
7676
|
router.post("/issues/:id/interactions/:interactionId/cancel", validate(cancelIssueThreadInteractionSchema), async (req, res) => {
|
|
6277
7677
|
const id = req.params.id;
|
|
6278
7678
|
const interactionId = req.params.interactionId;
|
|
@@ -7205,6 +8605,8 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
7205
8605
|
return;
|
|
7206
8606
|
}
|
|
7207
8607
|
assertCompanyAccess(req, issue.companyId);
|
|
8608
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
8609
|
+
return;
|
|
7208
8610
|
const attachments = await svc.listAttachments(issueId);
|
|
7209
8611
|
res.json(attachments.map(withContentPath));
|
|
7210
8612
|
});
|
|
@@ -7302,6 +8704,13 @@ export function issueRoutes(db, storage, opts = {}) {
|
|
|
7302
8704
|
return;
|
|
7303
8705
|
}
|
|
7304
8706
|
assertCompanyAccess(req, attachment.companyId);
|
|
8707
|
+
const issue = await svc.getById(attachment.issueId);
|
|
8708
|
+
if (!issue) {
|
|
8709
|
+
res.status(404).json({ error: "Issue not found" });
|
|
8710
|
+
return;
|
|
8711
|
+
}
|
|
8712
|
+
if (!(await assertIssueReadAllowed(req, res, issue)))
|
|
8713
|
+
return;
|
|
7305
8714
|
const contentLength = attachment.byteSize;
|
|
7306
8715
|
const range = parseAttachmentRangeHeader(typeof req.headers.range === "string" ? req.headers.range : undefined, contentLength);
|
|
7307
8716
|
res.setHeader("Accept-Ranges", "bytes");
|