@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
|
@@ -2,14 +2,16 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { and, asc, desc, eq, isNull, sql } from "drizzle-orm";
|
|
6
|
-
import { companies, companySkillComments, companySkillStars, companySkillVersions, companySkills } from "@penclipai/db";
|
|
7
|
-
import { readPaperclipSkillSyncPreference } from "@penclipai/adapter-utils/server-utils";
|
|
8
|
-
import { normalizeAgentUrlKey, parseFrontmatterMarkdown } from "@penclipai/shared";
|
|
5
|
+
import { and, asc, desc, eq, inArray, isNull, lt, sql } from "drizzle-orm";
|
|
6
|
+
import { agents as agentsTable, assets, authUsers, companies, companySkillComments, companySkillStars, companySkillTestInputs, companySkillTestRunTemplates, companySkillTestRuns, companySkillVersions, companySkills, costEvents, documents, issueAttachments, issueDocuments, issues, issueThreadInteractions, issueWorkProducts, } from "@penclipai/db";
|
|
7
|
+
import { readPaperclipSkillSyncPreference, writePaperclipSkillSyncPreference } from "@penclipai/adapter-utils/server-utils";
|
|
8
|
+
import { isUuidLike, normalizeAgentUrlKey, parseFrontmatterMarkdown } from "@penclipai/shared";
|
|
9
9
|
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
|
|
10
10
|
import { conflict, notFound, unprocessable } from "../errors.js";
|
|
11
11
|
import { ghFetch, gitHubApiBase, resolveRawGitHubUrl } from "./github-fetch.js";
|
|
12
12
|
import { agentService } from "./agents.js";
|
|
13
|
+
import { issueDocumentSelect, mapIssueDocumentRow } from "./documents.js";
|
|
14
|
+
import { toIssueWorkProduct } from "./work-products.js";
|
|
13
15
|
import { projectService } from "./projects.js";
|
|
14
16
|
import { normalizePortablePath } from "./portable-path.js";
|
|
15
17
|
import { copyCatalogSkillFile, getCatalogPackageMetadata, getCatalogSkillOrThrow, resolveCatalogSkillReference, } from "./skills-catalog.js";
|
|
@@ -649,6 +651,79 @@ function inventoryEntriesEqual(left, right) {
|
|
|
649
651
|
return other?.path === entry.path && other.kind === entry.kind;
|
|
650
652
|
});
|
|
651
653
|
}
|
|
654
|
+
function stableJsonComparable(value) {
|
|
655
|
+
if (value === undefined)
|
|
656
|
+
return undefined;
|
|
657
|
+
if (Array.isArray(value)) {
|
|
658
|
+
return value.map((entry) => stableJsonComparable(entry) ?? null);
|
|
659
|
+
}
|
|
660
|
+
if (isPlainRecord(value)) {
|
|
661
|
+
const out = {};
|
|
662
|
+
for (const key of Object.keys(value).sort()) {
|
|
663
|
+
const normalized = stableJsonComparable(value[key]);
|
|
664
|
+
if (normalized !== undefined)
|
|
665
|
+
out[key] = normalized ?? null;
|
|
666
|
+
}
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
function stableJsonEqual(left, right) {
|
|
672
|
+
return JSON.stringify(stableJsonComparable(left)) === JSON.stringify(stableJsonComparable(right));
|
|
673
|
+
}
|
|
674
|
+
function isPaperclipBundledSkillKey(key) {
|
|
675
|
+
return getBundledPaperclipSkillSlugFromKey(key) !== null;
|
|
676
|
+
}
|
|
677
|
+
function stripDerivedPaperclipBundledMetadata(key, metadata) {
|
|
678
|
+
if (metadata === null || metadata === undefined)
|
|
679
|
+
return {};
|
|
680
|
+
const comparable = stableJsonComparable(metadata);
|
|
681
|
+
if (!isPlainRecord(comparable))
|
|
682
|
+
return comparable;
|
|
683
|
+
const out = { ...comparable };
|
|
684
|
+
if (out.skillKey === key)
|
|
685
|
+
delete out.skillKey;
|
|
686
|
+
if (out.sourceKind === "paperclip_bundled")
|
|
687
|
+
delete out.sourceKind;
|
|
688
|
+
delete out.missingSource;
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
function importedSkillMetadataEqual(existing, values) {
|
|
692
|
+
const incomingMetadata = isPlainRecord(values.metadata) ? values.metadata : null;
|
|
693
|
+
if (isPaperclipBundledSkillKey(values.key) && asString(incomingMetadata?.sourceKind) === "paperclip_bundled") {
|
|
694
|
+
return JSON.stringify(stripDerivedPaperclipBundledMetadata(existing.key, existing.metadata))
|
|
695
|
+
=== JSON.stringify(stripDerivedPaperclipBundledMetadata(values.key, values.metadata));
|
|
696
|
+
}
|
|
697
|
+
return stableJsonEqual(existing.metadata ?? null, values.metadata);
|
|
698
|
+
}
|
|
699
|
+
function stringArraysEqual(left, right) {
|
|
700
|
+
if (left.length !== right.length)
|
|
701
|
+
return false;
|
|
702
|
+
return left.every((entry, index) => entry === right[index]);
|
|
703
|
+
}
|
|
704
|
+
function importedSkillPersistValuesMatchExisting(existing, values) {
|
|
705
|
+
return existing.companyId === values.companyId
|
|
706
|
+
&& existing.key === values.key
|
|
707
|
+
&& existing.slug === values.slug
|
|
708
|
+
&& existing.name === values.name
|
|
709
|
+
&& existing.description === values.description
|
|
710
|
+
&& existing.markdown === values.markdown
|
|
711
|
+
&& existing.sourceType === values.sourceType
|
|
712
|
+
&& existing.sourceLocator === values.sourceLocator
|
|
713
|
+
&& existing.sourceRef === values.sourceRef
|
|
714
|
+
&& existing.trustLevel === values.trustLevel
|
|
715
|
+
&& existing.compatibility === values.compatibility
|
|
716
|
+
&& inventoryEntriesEqual(existing.fileInventory, normalizeFileInventory({ fileInventory: values.fileInventory }))
|
|
717
|
+
&& existing.iconUrl === values.iconUrl
|
|
718
|
+
&& existing.color === values.color
|
|
719
|
+
&& existing.tagline === values.tagline
|
|
720
|
+
&& existing.authorName === values.authorName
|
|
721
|
+
&& existing.homepageUrl === values.homepageUrl
|
|
722
|
+
&& stringArraysEqual(existing.categories, normalizeCategoryList(values.categories))
|
|
723
|
+
&& existing.sharingScope === values.sharingScope
|
|
724
|
+
&& existing.installCount === values.installCount
|
|
725
|
+
&& importedSkillMetadataEqual(existing, values);
|
|
726
|
+
}
|
|
652
727
|
function inferLocalSkillInventoryMode(skill) {
|
|
653
728
|
const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null;
|
|
654
729
|
const sourceKind = asString(metadata?.sourceKind);
|
|
@@ -999,15 +1074,31 @@ function normalizeMutableSharingScope(value) {
|
|
|
999
1074
|
}
|
|
1000
1075
|
throw unprocessable("Invalid skill sharing scope.");
|
|
1001
1076
|
}
|
|
1002
|
-
function
|
|
1077
|
+
function normalizeCategoryName(value) {
|
|
1003
1078
|
if (typeof value !== "string")
|
|
1004
1079
|
return null;
|
|
1005
|
-
|
|
1080
|
+
const normalized = value.trim().replace(/\s+/g, " ");
|
|
1081
|
+
return normalized.length > 0 ? normalized : null;
|
|
1082
|
+
}
|
|
1083
|
+
function categoryLookupKey(value) {
|
|
1084
|
+
return value.toLocaleLowerCase();
|
|
1006
1085
|
}
|
|
1007
1086
|
function normalizeCategoryList(values) {
|
|
1008
1087
|
if (!Array.isArray(values))
|
|
1009
1088
|
return [];
|
|
1010
|
-
|
|
1089
|
+
const seen = new Set();
|
|
1090
|
+
const categories = [];
|
|
1091
|
+
for (const value of values) {
|
|
1092
|
+
const category = normalizeCategoryName(value);
|
|
1093
|
+
if (!category)
|
|
1094
|
+
continue;
|
|
1095
|
+
const key = categoryLookupKey(category);
|
|
1096
|
+
if (seen.has(key))
|
|
1097
|
+
continue;
|
|
1098
|
+
seen.add(key);
|
|
1099
|
+
categories.push(category);
|
|
1100
|
+
}
|
|
1101
|
+
return categories;
|
|
1011
1102
|
}
|
|
1012
1103
|
function normalizeStoreText(value, maxLength = 500) {
|
|
1013
1104
|
const text = asString(value);
|
|
@@ -1075,6 +1166,127 @@ function toCompanySkillComment(row) {
|
|
|
1075
1166
|
deletedAt: row.deletedAt ?? null,
|
|
1076
1167
|
};
|
|
1077
1168
|
}
|
|
1169
|
+
function toCompanySkillTestInput(row) {
|
|
1170
|
+
return {
|
|
1171
|
+
...row,
|
|
1172
|
+
deletedAt: row.deletedAt ?? null,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
const BUILT_IN_SKILL_TEST_RUN_TEMPLATE_ID = "built-in:default-test-template";
|
|
1176
|
+
const BUILT_IN_SKILL_TEST_RUN_TEMPLATE_DATE = new Date("2026-01-01T00:00:00.000Z");
|
|
1177
|
+
const BUILT_IN_SKILL_TEST_RUN_TEMPLATE_BODY = [
|
|
1178
|
+
"You are running a Skills Studio test for `{{skillName}}` (`{{skillKey}}`), skill version v{{skillVersion}}.",
|
|
1179
|
+
"",
|
|
1180
|
+
"Invoke and use the selected skill under test: `{{skillInvocation}}`. Use the pinned skill revision supplied by Paperclip as the source of truth, regardless of any other runtime skills.",
|
|
1181
|
+
"",
|
|
1182
|
+
"This is a test run. Do not make durable changes outside this test task. Do not mutate unrelated issues, push, publish, send external messages, or affect real work.",
|
|
1183
|
+
"",
|
|
1184
|
+
"If the skill would create documents, images, videos, files, or other assets, create test versions in an obviously test-scoped location when applicable, then post the results back to this task as issue documents, attachments, or work products.",
|
|
1185
|
+
"",
|
|
1186
|
+
"Write the final result to issue document `{{outputDocumentKey}}`, then mark this test task done.",
|
|
1187
|
+
].join("\n");
|
|
1188
|
+
function builtInSkillTestRunTemplate(companyId) {
|
|
1189
|
+
return {
|
|
1190
|
+
id: BUILT_IN_SKILL_TEST_RUN_TEMPLATE_ID,
|
|
1191
|
+
companyId,
|
|
1192
|
+
name: "Default test template",
|
|
1193
|
+
description: "Paperclip's read-only default harness instructions for Skills Studio runs.",
|
|
1194
|
+
body: BUILT_IN_SKILL_TEST_RUN_TEMPLATE_BODY,
|
|
1195
|
+
builtIn: true,
|
|
1196
|
+
createdByAgentId: null,
|
|
1197
|
+
createdByUserId: null,
|
|
1198
|
+
updatedByAgentId: null,
|
|
1199
|
+
updatedByUserId: null,
|
|
1200
|
+
deletedAt: null,
|
|
1201
|
+
createdAt: BUILT_IN_SKILL_TEST_RUN_TEMPLATE_DATE,
|
|
1202
|
+
updatedAt: BUILT_IN_SKILL_TEST_RUN_TEMPLATE_DATE,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function toCompanySkillTestRunTemplate(row) {
|
|
1206
|
+
return {
|
|
1207
|
+
...row,
|
|
1208
|
+
description: row.description ?? null,
|
|
1209
|
+
builtIn: false,
|
|
1210
|
+
createdByAgentId: row.createdByAgentId ?? null,
|
|
1211
|
+
createdByUserId: row.createdByUserId ?? null,
|
|
1212
|
+
updatedByAgentId: row.updatedByAgentId ?? null,
|
|
1213
|
+
updatedByUserId: row.updatedByUserId ?? null,
|
|
1214
|
+
deletedAt: row.deletedAt ?? null,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
const ALLOWED_SKILL_TEST_TEMPLATE_PLACEHOLDERS = new Set([
|
|
1218
|
+
"skillName",
|
|
1219
|
+
"skillKey",
|
|
1220
|
+
"skillInvocation",
|
|
1221
|
+
"skillVersion",
|
|
1222
|
+
"runId",
|
|
1223
|
+
"issueId",
|
|
1224
|
+
"outputDocumentKey",
|
|
1225
|
+
]);
|
|
1226
|
+
function validateSkillTestTemplatePlaceholders(body) {
|
|
1227
|
+
const unknown = new Set();
|
|
1228
|
+
const recognized = /\{\{\s*([A-Za-z][A-Za-z0-9]*)\s*\}\}/g;
|
|
1229
|
+
for (const match of body.matchAll(recognized)) {
|
|
1230
|
+
const key = match[1] ?? "";
|
|
1231
|
+
if (!ALLOWED_SKILL_TEST_TEMPLATE_PLACEHOLDERS.has(key)) {
|
|
1232
|
+
unknown.add(key);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (body.replace(recognized, "").includes("{{") || body.replace(recognized, "").includes("}}")) {
|
|
1236
|
+
throw unprocessable("Malformed template placeholder. Use explicit placeholders like {{skillName}}.");
|
|
1237
|
+
}
|
|
1238
|
+
if (unknown.size > 0) {
|
|
1239
|
+
throw unprocessable(`Unknown template placeholder${unknown.size === 1 ? "" : "s"}: ${Array.from(unknown).sort().join(", ")}`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
function renderSkillTestTemplate(body, values) {
|
|
1243
|
+
validateSkillTestTemplatePlaceholders(body);
|
|
1244
|
+
return body.replace(/\{\{\s*([A-Za-z][A-Za-z0-9]*)\s*\}\}/g, (_match, rawKey) => values[rawKey] ?? "");
|
|
1245
|
+
}
|
|
1246
|
+
function buildHarnessIssueDescription(inputSnapshot, renderedTemplateBody) {
|
|
1247
|
+
const trimmedInput = inputSnapshot.trim();
|
|
1248
|
+
const trimmedTemplate = renderedTemplateBody?.trim() ?? "";
|
|
1249
|
+
return trimmedTemplate ? `${trimmedInput}\n\n---\n\n${trimmedTemplate}` : trimmedInput;
|
|
1250
|
+
}
|
|
1251
|
+
function normalizeTestRunStatus(value) {
|
|
1252
|
+
return value === "running" || value === "succeeded" || value === "failed" || value === "cancelled"
|
|
1253
|
+
? value
|
|
1254
|
+
: "queued";
|
|
1255
|
+
}
|
|
1256
|
+
function emptyTestRunCost() {
|
|
1257
|
+
return {
|
|
1258
|
+
costCents: 0,
|
|
1259
|
+
inputTokens: 0,
|
|
1260
|
+
cachedInputTokens: 0,
|
|
1261
|
+
outputTokens: 0,
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function toCompanySkillTestRun(row, cost = emptyTestRunCost(), taskExpired = false) {
|
|
1265
|
+
return {
|
|
1266
|
+
...row,
|
|
1267
|
+
inputId: row.inputId ?? null,
|
|
1268
|
+
agentConfigSnapshot: isPlainRecord(row.agentConfigSnapshot) ? row.agentConfigSnapshot : {},
|
|
1269
|
+
templateId: row.templateId ?? null,
|
|
1270
|
+
templateName: row.templateName ?? null,
|
|
1271
|
+
templateBody: row.templateBody ?? null,
|
|
1272
|
+
renderedTemplateBody: row.renderedTemplateBody ?? null,
|
|
1273
|
+
harnessIssueDescription: row.harnessIssueDescription || row.inputSnapshot,
|
|
1274
|
+
status: normalizeTestRunStatus(row.status),
|
|
1275
|
+
outputDocumentKey: row.outputDocumentKey || "output",
|
|
1276
|
+
outputSnapshot: row.outputSnapshot ?? "",
|
|
1277
|
+
error: row.error ?? null,
|
|
1278
|
+
deletedAt: row.deletedAt ?? null,
|
|
1279
|
+
supersededAt: row.supersededAt ?? null,
|
|
1280
|
+
harnessIssueExpiresAt: row.harnessIssueExpiresAt ?? null,
|
|
1281
|
+
harnessIssueDeletedAt: row.harnessIssueDeletedAt ?? null,
|
|
1282
|
+
cost,
|
|
1283
|
+
taskExpired,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
function versionInventorySnapshotEqual(left, right) {
|
|
1287
|
+
const normalize = (entries) => JSON.stringify([...entries].sort((a, b) => a.path.localeCompare(b.path)));
|
|
1288
|
+
return normalize(left) === normalize(right);
|
|
1289
|
+
}
|
|
1078
1290
|
function getSkillMeta(skill) {
|
|
1079
1291
|
return isPlainRecord(skill.metadata) ? skill.metadata : {};
|
|
1080
1292
|
}
|
|
@@ -1197,10 +1409,12 @@ async function assertVersionMatchesSkill(db, companyId, skillId, versionId) {
|
|
|
1197
1409
|
});
|
|
1198
1410
|
}
|
|
1199
1411
|
}
|
|
1200
|
-
async function resolveRequestedSkillEntriesOrThrow(db, companyId, skills, requestedSelections) {
|
|
1412
|
+
async function resolveRequestedSkillEntriesOrThrow(db, companyId, skills, requestedSelections, options = {}) {
|
|
1201
1413
|
const missing = new Set();
|
|
1202
1414
|
const ambiguous = new Set();
|
|
1203
1415
|
const resolved = new Map();
|
|
1416
|
+
const unresolved = [];
|
|
1417
|
+
const seenUnresolved = new Set();
|
|
1204
1418
|
for (const rawSelection of requestedSelections) {
|
|
1205
1419
|
const selection = normalizeRequestedDesiredSkillSelection(rawSelection);
|
|
1206
1420
|
if (!selection.key)
|
|
@@ -1223,8 +1437,19 @@ async function resolveRequestedSkillEntriesOrThrow(db, companyId, skills, reques
|
|
|
1223
1437
|
ambiguous.add(selection.key);
|
|
1224
1438
|
continue;
|
|
1225
1439
|
}
|
|
1440
|
+
// Unknown / stale reference (no longer in the company library).
|
|
1441
|
+
if (options.tolerateUnknownReferences) {
|
|
1442
|
+
if (!seenUnresolved.has(selection.key)) {
|
|
1443
|
+
seenUnresolved.add(selection.key);
|
|
1444
|
+
unresolved.push(selection.key);
|
|
1445
|
+
}
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1226
1448
|
missing.add(selection.key);
|
|
1227
1449
|
}
|
|
1450
|
+
// Ambiguous references are always a hard error — they signal a genuine
|
|
1451
|
+
// conflict the caller must disambiguate. Unknown references are only fatal
|
|
1452
|
+
// when the caller has not opted into tolerating (and preserving) stale keys.
|
|
1228
1453
|
if (ambiguous.size > 0 || missing.size > 0) {
|
|
1229
1454
|
const problems = [];
|
|
1230
1455
|
if (ambiguous.size > 0) {
|
|
@@ -1235,7 +1460,7 @@ async function resolveRequestedSkillEntriesOrThrow(db, companyId, skills, reques
|
|
|
1235
1460
|
}
|
|
1236
1461
|
throw unprocessable(`Invalid company skill selection (${problems.join("; ")}).`);
|
|
1237
1462
|
}
|
|
1238
|
-
return Array.from(resolved.values());
|
|
1463
|
+
return { resolved: Array.from(resolved.values()), unresolved };
|
|
1239
1464
|
}
|
|
1240
1465
|
function resolveDesiredSkillKeys(skills, config) {
|
|
1241
1466
|
const preference = readPaperclipSkillSyncPreference(config);
|
|
@@ -1611,17 +1836,52 @@ function deriveSkillSourceInfo(skill) {
|
|
|
1611
1836
|
sourcePath: null,
|
|
1612
1837
|
};
|
|
1613
1838
|
}
|
|
1614
|
-
function enrichSkill(skill, attachedAgentCount, usedByAgents = [], currentVersion = null, starredByCurrentActor = false) {
|
|
1839
|
+
function enrichSkill(skill, attachedAgentCount, usedByAgents = [], currentVersion = null, starredByCurrentActor = false, existingForks = []) {
|
|
1615
1840
|
const source = deriveSkillSourceInfo(skill);
|
|
1616
1841
|
return {
|
|
1617
1842
|
...skill,
|
|
1618
1843
|
attachedAgentCount,
|
|
1619
1844
|
usedByAgents,
|
|
1845
|
+
existingForks,
|
|
1620
1846
|
currentVersion,
|
|
1621
1847
|
starredByCurrentActor,
|
|
1622
1848
|
...source,
|
|
1623
1849
|
};
|
|
1624
1850
|
}
|
|
1851
|
+
function summarizeOriginalSkill(skill) {
|
|
1852
|
+
return {
|
|
1853
|
+
id: skill.id,
|
|
1854
|
+
name: skill.name,
|
|
1855
|
+
slug: skill.slug,
|
|
1856
|
+
sourceType: skill.sourceType,
|
|
1857
|
+
sourceLocator: skill.sourceLocator,
|
|
1858
|
+
sourceRef: skill.sourceRef,
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
function forkCreatedByActor(skill, actor) {
|
|
1862
|
+
const metadata = getSkillMeta(skill);
|
|
1863
|
+
if (actor?.type === "agent" && actor.agentId) {
|
|
1864
|
+
return asString(metadata.forkedByAgentId) === actor.agentId;
|
|
1865
|
+
}
|
|
1866
|
+
if (actor?.type === "user" && actor.userId) {
|
|
1867
|
+
return asString(metadata.forkedByUserId) === actor.userId;
|
|
1868
|
+
}
|
|
1869
|
+
return false;
|
|
1870
|
+
}
|
|
1871
|
+
function summarizeForkSkill(skill, actor, versionCount) {
|
|
1872
|
+
const metadata = getSkillMeta(skill);
|
|
1873
|
+
return {
|
|
1874
|
+
...summarizeOriginalSkill(skill),
|
|
1875
|
+
key: skill.key,
|
|
1876
|
+
forkedFromSkillId: skill.forkedFromSkillId,
|
|
1877
|
+
forkedFromCompanyId: skill.forkedFromCompanyId,
|
|
1878
|
+
currentVersionId: skill.currentVersionId,
|
|
1879
|
+
createdByCurrentActor: forkCreatedByActor(skill, actor),
|
|
1880
|
+
diverged: versionCount > 1 || Boolean(asString(metadata.userModifiedAt)),
|
|
1881
|
+
createdAt: skill.createdAt,
|
|
1882
|
+
updatedAt: skill.updatedAt,
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1625
1885
|
function toCompanySkillListItem(skill, attachedAgentCount) {
|
|
1626
1886
|
const source = deriveSkillSourceInfo(skill);
|
|
1627
1887
|
const metadata = getSkillMeta(skill);
|
|
@@ -1672,6 +1932,47 @@ function toCompanySkillListItem(skill, attachedAgentCount) {
|
|
|
1672
1932
|
packageVersion,
|
|
1673
1933
|
};
|
|
1674
1934
|
}
|
|
1935
|
+
async function listLastEditorsBySkillId(db, companyId, skillIds) {
|
|
1936
|
+
if (skillIds.length === 0)
|
|
1937
|
+
return new Map();
|
|
1938
|
+
const rows = await db
|
|
1939
|
+
.selectDistinctOn([companySkillVersions.companySkillId], {
|
|
1940
|
+
companySkillId: companySkillVersions.companySkillId,
|
|
1941
|
+
authorAgentId: companySkillVersions.authorAgentId,
|
|
1942
|
+
authorUserId: companySkillVersions.authorUserId,
|
|
1943
|
+
userName: authUsers.name,
|
|
1944
|
+
userImage: authUsers.image,
|
|
1945
|
+
agentName: agentsTable.name,
|
|
1946
|
+
})
|
|
1947
|
+
.from(companySkillVersions)
|
|
1948
|
+
.leftJoin(authUsers, eq(authUsers.id, companySkillVersions.authorUserId))
|
|
1949
|
+
.leftJoin(agentsTable, and(eq(agentsTable.companyId, companyId), eq(agentsTable.id, companySkillVersions.authorAgentId)))
|
|
1950
|
+
.where(and(eq(companySkillVersions.companyId, companyId), inArray(companySkillVersions.companySkillId, skillIds)))
|
|
1951
|
+
.orderBy(companySkillVersions.companySkillId, desc(companySkillVersions.createdAt), desc(companySkillVersions.revisionNumber), desc(companySkillVersions.id));
|
|
1952
|
+
const editors = new Map();
|
|
1953
|
+
for (const row of rows) {
|
|
1954
|
+
if (row.authorUserId) {
|
|
1955
|
+
editors.set(row.companySkillId, {
|
|
1956
|
+
kind: "user",
|
|
1957
|
+
id: row.authorUserId,
|
|
1958
|
+
name: row.userName ?? null,
|
|
1959
|
+
imageUrl: row.userImage ?? null,
|
|
1960
|
+
});
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
if (row.authorAgentId) {
|
|
1964
|
+
editors.set(row.companySkillId, {
|
|
1965
|
+
kind: "agent",
|
|
1966
|
+
id: row.authorAgentId,
|
|
1967
|
+
name: row.agentName ?? null,
|
|
1968
|
+
imageUrl: null,
|
|
1969
|
+
});
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
editors.set(row.companySkillId, null);
|
|
1973
|
+
}
|
|
1974
|
+
return editors;
|
|
1975
|
+
}
|
|
1675
1976
|
export function companySkillService(db) {
|
|
1676
1977
|
const agents = agentService(db);
|
|
1677
1978
|
const projects = projectService(db);
|
|
@@ -1727,6 +2028,8 @@ export function companySkillService(db) {
|
|
|
1727
2028
|
for (const skill of skills) {
|
|
1728
2029
|
if (skill.sourceType !== "local_path")
|
|
1729
2030
|
continue;
|
|
2031
|
+
if (isPaperclipBundledSkillKey(skill.key) || asString(skill.metadata?.sourceKind) === "paperclip_bundled")
|
|
2032
|
+
continue;
|
|
1730
2033
|
if (!missingIds.has(skill.id)) {
|
|
1731
2034
|
const metadata = getMissingSourceMarker(skill.metadata)
|
|
1732
2035
|
? withoutMissingSourceMarker(skill.metadata)
|
|
@@ -1737,7 +2040,7 @@ export function companySkillService(db) {
|
|
|
1737
2040
|
: null;
|
|
1738
2041
|
const nextTrustLevel = nextInventory ? deriveTrustLevel(nextInventory) : skill.trustLevel;
|
|
1739
2042
|
const inventoryChanged = nextInventory ? !inventoryEntriesEqual(skill.fileInventory, nextInventory) : false;
|
|
1740
|
-
const metadataChanged =
|
|
2043
|
+
const metadataChanged = !stableJsonEqual(metadata ?? {}, skill.metadata ?? {});
|
|
1741
2044
|
if (inventoryChanged || metadataChanged || nextTrustLevel !== skill.trustLevel) {
|
|
1742
2045
|
await db
|
|
1743
2046
|
.update(companySkills)
|
|
@@ -1754,7 +2057,7 @@ export function companySkillService(db) {
|
|
|
1754
2057
|
const usedByAgents = await usage(companyId, skill.key);
|
|
1755
2058
|
if (usedByAgents.length > 0) {
|
|
1756
2059
|
const metadata = withMissingSourceMarker(skill.metadata, buildMissingLocalSourceMarker(skill));
|
|
1757
|
-
if (
|
|
2060
|
+
if (!stableJsonEqual(metadata, skill.metadata ?? {})) {
|
|
1758
2061
|
await db
|
|
1759
2062
|
.update(companySkills)
|
|
1760
2063
|
.set({ metadata, updatedAt: new Date() })
|
|
@@ -1836,11 +2139,14 @@ export function companySkillService(db) {
|
|
|
1836
2139
|
.then((entries) => entries.map((entry) => toCompanySkillListRow(entry)));
|
|
1837
2140
|
const agentRows = await agents.list(companyId);
|
|
1838
2141
|
const q = query.q?.trim().toLowerCase() ?? "";
|
|
1839
|
-
const categories = new Set((query.categories ?? [])
|
|
2142
|
+
const categories = new Set((query.categories ?? [])
|
|
2143
|
+
.map(normalizeCategoryName)
|
|
2144
|
+
.filter((value) => Boolean(value))
|
|
2145
|
+
.map(categoryLookupKey));
|
|
1840
2146
|
const filtered = rows.filter((skill) => {
|
|
1841
2147
|
if (query.scope && skill.sharingScope !== query.scope)
|
|
1842
2148
|
return false;
|
|
1843
|
-
if (categories.size > 0 && !skill.categories.some((category) => categories.has(category)))
|
|
2149
|
+
if (categories.size > 0 && !skill.categories.some((category) => categories.has(categoryLookupKey(category))))
|
|
1844
2150
|
return false;
|
|
1845
2151
|
if (q) {
|
|
1846
2152
|
const haystack = [
|
|
@@ -1878,6 +2184,13 @@ export function companySkillService(db) {
|
|
|
1878
2184
|
return right.forkCount - left.forkCount || left.name.localeCompare(right.name);
|
|
1879
2185
|
return left.name.localeCompare(right.name) || left.key.localeCompare(right.key);
|
|
1880
2186
|
});
|
|
2187
|
+
if (query.include?.includes("lastEditor")) {
|
|
2188
|
+
const lastEditors = await listLastEditorsBySkillId(db, companyId, items.map((item) => item.id));
|
|
2189
|
+
return items.map((item) => ({
|
|
2190
|
+
...item,
|
|
2191
|
+
lastEditor: lastEditors.get(item.id) ?? null,
|
|
2192
|
+
}));
|
|
2193
|
+
}
|
|
1881
2194
|
return items;
|
|
1882
2195
|
}
|
|
1883
2196
|
async function listFull(companyId) {
|
|
@@ -1928,6 +2241,18 @@ export function companySkillService(db) {
|
|
|
1928
2241
|
.then((rows) => rows[0] ?? null);
|
|
1929
2242
|
return row ? toCompanySkill(row) : null;
|
|
1930
2243
|
}
|
|
2244
|
+
async function getBySlugIfUnique(companyId, slug) {
|
|
2245
|
+
const rows = await db
|
|
2246
|
+
.select(selectCompanySkillColumns())
|
|
2247
|
+
.from(companySkills)
|
|
2248
|
+
.where(and(eq(companySkills.companyId, companyId), eq(companySkills.slug, slug)));
|
|
2249
|
+
return rows.length === 1 ? toCompanySkill(rows[0]) : null;
|
|
2250
|
+
}
|
|
2251
|
+
async function getByRouteRef(companyId, ref) {
|
|
2252
|
+
return (isUuidLike(ref) ? await getById(companyId, ref) : null)
|
|
2253
|
+
?? await getBySlugIfUnique(companyId, ref)
|
|
2254
|
+
?? await getByKey(companyId, ref);
|
|
2255
|
+
}
|
|
1931
2256
|
async function getVersion(companyId, skillId, versionId) {
|
|
1932
2257
|
const row = await db
|
|
1933
2258
|
.select()
|
|
@@ -2019,13 +2344,48 @@ export function companySkillService(db) {
|
|
|
2019
2344
|
versionId: desiredEntry.versionId ?? null,
|
|
2020
2345
|
}));
|
|
2021
2346
|
}
|
|
2347
|
+
async function versionCount(companyId, skillId) {
|
|
2348
|
+
const [{ value }] = await db
|
|
2349
|
+
.select({ value: sql `count(*)::int` })
|
|
2350
|
+
.from(companySkillVersions)
|
|
2351
|
+
.where(and(eq(companySkillVersions.companyId, companyId), eq(companySkillVersions.companySkillId, skillId)));
|
|
2352
|
+
return Number(value ?? 0);
|
|
2353
|
+
}
|
|
2354
|
+
async function existingForkSummaries(companyId, sourceSkillId, actor) {
|
|
2355
|
+
const rows = await db
|
|
2356
|
+
.select(selectCompanySkillColumns())
|
|
2357
|
+
.from(companySkills)
|
|
2358
|
+
.where(and(eq(companySkills.companyId, companyId), eq(companySkills.forkedFromSkillId, sourceSkillId)))
|
|
2359
|
+
.orderBy(desc(companySkills.updatedAt), asc(companySkills.name));
|
|
2360
|
+
const summaries = [];
|
|
2361
|
+
for (const row of rows) {
|
|
2362
|
+
const skill = toCompanySkill(row);
|
|
2363
|
+
summaries.push(summarizeForkSkill(skill, actor, await versionCount(companyId, skill.id)));
|
|
2364
|
+
}
|
|
2365
|
+
return summaries;
|
|
2366
|
+
}
|
|
2022
2367
|
async function detail(companyId, id, actor) {
|
|
2023
2368
|
await ensureSkillInventoryCurrent(companyId);
|
|
2024
|
-
const skill = await
|
|
2369
|
+
const skill = await getByRouteRef(companyId, id);
|
|
2025
2370
|
if (!skill)
|
|
2026
2371
|
return null;
|
|
2027
2372
|
const usedByAgents = await usage(companyId, skill.key);
|
|
2028
|
-
|
|
2373
|
+
const existingForks = await existingForkSummaries(companyId, skill.id, actor);
|
|
2374
|
+
return enrichSkill(skill, usedByAgents.length, usedByAgents, await getCurrentVersion(skill), await isStarredByActor(companyId, skill.id, actor), existingForks);
|
|
2375
|
+
}
|
|
2376
|
+
async function forkPrecheck(companyId, skillId, actor) {
|
|
2377
|
+
await ensureSkillInventoryCurrent(companyId);
|
|
2378
|
+
const skill = await getById(companyId, skillId);
|
|
2379
|
+
if (!skill)
|
|
2380
|
+
return null;
|
|
2381
|
+
const usedByAgents = await usage(companyId, skill.key);
|
|
2382
|
+
return {
|
|
2383
|
+
skillId: skill.id,
|
|
2384
|
+
original: summarizeOriginalSkill(skill),
|
|
2385
|
+
agentUsageCount: usedByAgents.length,
|
|
2386
|
+
usedByAgents,
|
|
2387
|
+
existingForks: await existingForkSummaries(companyId, skill.id, actor),
|
|
2388
|
+
};
|
|
2029
2389
|
}
|
|
2030
2390
|
async function collectVersionFileInventory(companyId, skill) {
|
|
2031
2391
|
const out = [];
|
|
@@ -2231,6 +2591,117 @@ export function companySkillService(db) {
|
|
|
2231
2591
|
throw notFound("Skill comment not found");
|
|
2232
2592
|
return toCompanySkillComment(row);
|
|
2233
2593
|
}
|
|
2594
|
+
async function planSkillReassignments(companyId, source, forkKey, reassignAgentIds) {
|
|
2595
|
+
const requestedAgentIds = Array.from(new Set(reassignAgentIds ?? []));
|
|
2596
|
+
if (requestedAgentIds.length === 0)
|
|
2597
|
+
return [];
|
|
2598
|
+
const skills = await listReferenceTargets(companyId);
|
|
2599
|
+
const agentRows = await agents.list(companyId, { includeTerminated: true });
|
|
2600
|
+
const byId = new Map(agentRows.map((agent) => [agent.id, agent]));
|
|
2601
|
+
const missingAgentIds = requestedAgentIds.filter((agentId) => !byId.has(agentId));
|
|
2602
|
+
if (missingAgentIds.length > 0) {
|
|
2603
|
+
throw notFound(`Agent not found for skill reassignment: ${missingAgentIds.join(", ")}`);
|
|
2604
|
+
}
|
|
2605
|
+
return requestedAgentIds.map((agentId) => {
|
|
2606
|
+
const agent = byId.get(agentId);
|
|
2607
|
+
if (agent.companyId !== companyId) {
|
|
2608
|
+
throw unprocessable("Cannot reassign a skill for an agent in another company.", { agentId });
|
|
2609
|
+
}
|
|
2610
|
+
const adapterConfig = agent.adapterConfig;
|
|
2611
|
+
const desiredEntries = resolveDesiredSkillEntries(skills, adapterConfig);
|
|
2612
|
+
const hasSource = desiredEntries.some((entry) => entry.key === source.key);
|
|
2613
|
+
if (!hasSource) {
|
|
2614
|
+
throw unprocessable(`Agent "${agent.name}" does not currently use skill "${source.name}".`, {
|
|
2615
|
+
agentId,
|
|
2616
|
+
skillId: source.id,
|
|
2617
|
+
skillKey: source.key,
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
return {
|
|
2621
|
+
agentId,
|
|
2622
|
+
reassignment: {
|
|
2623
|
+
agentId,
|
|
2624
|
+
previousSkillKey: source.key,
|
|
2625
|
+
nextSkillKey: forkKey,
|
|
2626
|
+
},
|
|
2627
|
+
};
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
async function applySkillReassignments(companyId, source, forkKey, planned) {
|
|
2631
|
+
if (planned.length === 0)
|
|
2632
|
+
return [];
|
|
2633
|
+
const skills = await listReferenceTargets(companyId);
|
|
2634
|
+
await db.transaction(async (tx) => {
|
|
2635
|
+
for (const item of planned) {
|
|
2636
|
+
const row = await tx
|
|
2637
|
+
.select({
|
|
2638
|
+
id: agentsTable.id,
|
|
2639
|
+
name: agentsTable.name,
|
|
2640
|
+
adapterConfig: agentsTable.adapterConfig,
|
|
2641
|
+
})
|
|
2642
|
+
.from(agentsTable)
|
|
2643
|
+
.where(and(eq(agentsTable.companyId, companyId), eq(agentsTable.id, item.agentId)))
|
|
2644
|
+
.for("update")
|
|
2645
|
+
.then((rows) => rows[0] ?? null);
|
|
2646
|
+
if (!row)
|
|
2647
|
+
throw notFound(`Agent not found for skill reassignment: ${item.agentId}`);
|
|
2648
|
+
const adapterConfig = row.adapterConfig;
|
|
2649
|
+
const desiredEntries = resolveDesiredSkillEntries(skills, adapterConfig);
|
|
2650
|
+
const hasSource = desiredEntries.some((entry) => entry.key === source.key);
|
|
2651
|
+
if (!hasSource) {
|
|
2652
|
+
throw unprocessable(`Agent "${row.name}" does not currently use skill "${source.name}".`, {
|
|
2653
|
+
agentId: item.agentId,
|
|
2654
|
+
skillId: source.id,
|
|
2655
|
+
skillKey: source.key,
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
const nextEntries = desiredEntries.map((entry) => entry.key === source.key
|
|
2659
|
+
? { key: forkKey, versionId: null }
|
|
2660
|
+
: entry);
|
|
2661
|
+
const updated = await tx
|
|
2662
|
+
.update(agentsTable)
|
|
2663
|
+
.set({
|
|
2664
|
+
adapterConfig: writePaperclipSkillSyncPreference(adapterConfig, nextEntries),
|
|
2665
|
+
updatedAt: new Date(),
|
|
2666
|
+
})
|
|
2667
|
+
.where(and(eq(agentsTable.companyId, companyId), eq(agentsTable.id, item.agentId)))
|
|
2668
|
+
.returning({ id: agentsTable.id })
|
|
2669
|
+
.then((rows) => rows[0] ?? null);
|
|
2670
|
+
if (!updated)
|
|
2671
|
+
throw notFound(`Agent not found for skill reassignment: ${item.agentId}`);
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
return planned.map((item) => item.reassignment);
|
|
2675
|
+
}
|
|
2676
|
+
async function cleanupFailedFork(companyId, sourceSkillId, forkSkillId, forkDir) {
|
|
2677
|
+
await db.transaction(async (tx) => {
|
|
2678
|
+
await tx
|
|
2679
|
+
.update(companySkills)
|
|
2680
|
+
.set({ currentVersionId: null, updatedAt: new Date() })
|
|
2681
|
+
.where(and(eq(companySkills.id, forkSkillId), eq(companySkills.companyId, companyId)));
|
|
2682
|
+
await tx
|
|
2683
|
+
.delete(companySkillComments)
|
|
2684
|
+
.where(and(eq(companySkillComments.companyId, companyId), eq(companySkillComments.companySkillId, forkSkillId)));
|
|
2685
|
+
await tx
|
|
2686
|
+
.delete(companySkillStars)
|
|
2687
|
+
.where(and(eq(companySkillStars.companyId, companyId), eq(companySkillStars.companySkillId, forkSkillId)));
|
|
2688
|
+
await tx
|
|
2689
|
+
.delete(companySkillVersions)
|
|
2690
|
+
.where(and(eq(companySkillVersions.companyId, companyId), eq(companySkillVersions.companySkillId, forkSkillId)));
|
|
2691
|
+
await tx
|
|
2692
|
+
.delete(companySkills)
|
|
2693
|
+
.where(and(eq(companySkills.id, forkSkillId), eq(companySkills.companyId, companyId)));
|
|
2694
|
+
await tx
|
|
2695
|
+
.update(companySkills)
|
|
2696
|
+
.set({
|
|
2697
|
+
forkCount: sql `greatest(${companySkills.forkCount} - 1, 0)`,
|
|
2698
|
+
installCount: sql `greatest(${companySkills.installCount} - 1, 0)`,
|
|
2699
|
+
updatedAt: new Date(),
|
|
2700
|
+
})
|
|
2701
|
+
.where(and(eq(companySkills.id, sourceSkillId), eq(companySkills.companyId, companyId)));
|
|
2702
|
+
});
|
|
2703
|
+
await fs.rm(forkDir, { recursive: true, force: true });
|
|
2704
|
+
}
|
|
2234
2705
|
async function forkSkill(companyId, skillId, input = {}, actor = null) {
|
|
2235
2706
|
await ensureSkillInventoryCurrent(companyId);
|
|
2236
2707
|
const source = await getById(companyId, skillId);
|
|
@@ -2240,6 +2711,8 @@ export function companySkillService(db) {
|
|
|
2240
2711
|
const usedSlugs = new Set(existing.map((skill) => normalizeSkillSlug(skill.slug) ?? skill.slug));
|
|
2241
2712
|
const forkSlug = uniqueSkillSlug(normalizeSkillSlug(input.slug ?? `${source.slug}-fork`) ?? `${source.slug}-fork`, usedSlugs);
|
|
2242
2713
|
const forkName = input.name?.trim() || `${source.name} Fork`;
|
|
2714
|
+
const forkKey = `company/${companyId}/${forkSlug}`;
|
|
2715
|
+
const plannedReassignments = await planSkillReassignments(companyId, source, forkKey, input.reassignAgentIds);
|
|
2243
2716
|
const managedRoot = resolveManagedSkillsRoot(companyId);
|
|
2244
2717
|
const forkDir = path.resolve(managedRoot, forkSlug);
|
|
2245
2718
|
await fs.rm(forkDir, { recursive: true, force: true });
|
|
@@ -2267,7 +2740,7 @@ export function companySkillService(db) {
|
|
|
2267
2740
|
forkedByUserId: actor?.type === "user" ? actor.userId ?? null : null,
|
|
2268
2741
|
};
|
|
2269
2742
|
const imported = await upsertImportedSkills(companyId, [{
|
|
2270
|
-
key:
|
|
2743
|
+
key: forkKey,
|
|
2271
2744
|
slug: forkSlug,
|
|
2272
2745
|
name: asString(parsed.frontmatter.name) ?? forkName,
|
|
2273
2746
|
description: asString(parsed.frontmatter.description) ?? source.description,
|
|
@@ -2305,11 +2778,24 @@ export function companySkillService(db) {
|
|
|
2305
2778
|
})
|
|
2306
2779
|
.where(and(eq(companySkills.id, source.id), eq(companySkills.companyId, companyId)));
|
|
2307
2780
|
await createVersion(companyId, forked.id, { label: "Initial version" }, actor);
|
|
2308
|
-
|
|
2781
|
+
const persistedFork = await getById(companyId, forked.id).then((skill) => {
|
|
2309
2782
|
if (!skill)
|
|
2310
2783
|
throw notFound("Forked skill not found");
|
|
2311
2784
|
return skill;
|
|
2312
2785
|
});
|
|
2786
|
+
let reassignments;
|
|
2787
|
+
try {
|
|
2788
|
+
reassignments = await applySkillReassignments(companyId, source, forkKey, plannedReassignments);
|
|
2789
|
+
}
|
|
2790
|
+
catch (error) {
|
|
2791
|
+
await cleanupFailedFork(companyId, source.id, forked.id, forkDir);
|
|
2792
|
+
throw error;
|
|
2793
|
+
}
|
|
2794
|
+
return {
|
|
2795
|
+
skill: persistedFork,
|
|
2796
|
+
original: summarizeOriginalSkill(source),
|
|
2797
|
+
reassignments,
|
|
2798
|
+
};
|
|
2313
2799
|
}
|
|
2314
2800
|
async function updateStatus(companyId, skillId) {
|
|
2315
2801
|
await ensureSkillInventoryCurrent(companyId);
|
|
@@ -2676,6 +3162,58 @@ export function companySkillService(db) {
|
|
|
2676
3162
|
throw notFound("Skill file not found");
|
|
2677
3163
|
return detail;
|
|
2678
3164
|
}
|
|
3165
|
+
async function deleteFile(companyId, skillId, input, actor = null) {
|
|
3166
|
+
await ensureSkillInventoryCurrent(companyId);
|
|
3167
|
+
const skill = await getById(companyId, skillId);
|
|
3168
|
+
if (!skill)
|
|
3169
|
+
throw notFound("Skill not found");
|
|
3170
|
+
const source = deriveSkillSourceInfo(skill);
|
|
3171
|
+
if (!source.editable || skill.sourceType !== "local_path") {
|
|
3172
|
+
throw unprocessable(source.editableReason ?? "This skill cannot be edited.");
|
|
3173
|
+
}
|
|
3174
|
+
const normalizedPath = normalizePortablePath(input.path);
|
|
3175
|
+
if (!normalizedPath) {
|
|
3176
|
+
throw unprocessable("Skill file path is required.");
|
|
3177
|
+
}
|
|
3178
|
+
const deletedPaths = input.target === "folder"
|
|
3179
|
+
? skill.fileInventory
|
|
3180
|
+
.map((entry) => normalizePortablePath(entry.path))
|
|
3181
|
+
.filter((entryPath) => entryPath.startsWith(`${normalizedPath}/`))
|
|
3182
|
+
: skill.fileInventory
|
|
3183
|
+
.map((entry) => normalizePortablePath(entry.path))
|
|
3184
|
+
.filter((entryPath) => entryPath === normalizedPath);
|
|
3185
|
+
if (deletedPaths.length === 0) {
|
|
3186
|
+
throw notFound(input.target === "folder" ? "Skill folder not found" : "Skill file not found");
|
|
3187
|
+
}
|
|
3188
|
+
if (deletedPaths.includes("SKILL.md")) {
|
|
3189
|
+
throw unprocessable("SKILL.md cannot be deleted.");
|
|
3190
|
+
}
|
|
3191
|
+
const absolutePath = resolveLocalSkillFilePath(skill, normalizedPath);
|
|
3192
|
+
if (!absolutePath)
|
|
3193
|
+
throw notFound("Skill file not found");
|
|
3194
|
+
await fs.rm(absolutePath, {
|
|
3195
|
+
recursive: input.target === "folder",
|
|
3196
|
+
force: false,
|
|
3197
|
+
}).catch((error) => {
|
|
3198
|
+
if (error.code === "ENOENT") {
|
|
3199
|
+
throw notFound(input.target === "folder" ? "Skill folder not found" : "Skill file not found");
|
|
3200
|
+
}
|
|
3201
|
+
throw error;
|
|
3202
|
+
});
|
|
3203
|
+
await db
|
|
3204
|
+
.update(companySkills)
|
|
3205
|
+
.set({ updatedAt: new Date() })
|
|
3206
|
+
.where(eq(companySkills.id, skill.id));
|
|
3207
|
+
await createVersion(companyId, skillId, {
|
|
3208
|
+
label: input.target === "folder" ? `Deleted ${normalizedPath}/` : `Deleted ${normalizedPath}`,
|
|
3209
|
+
}, actor);
|
|
3210
|
+
return {
|
|
3211
|
+
skillId: skill.id,
|
|
3212
|
+
path: normalizedPath,
|
|
3213
|
+
target: input.target,
|
|
3214
|
+
deletedPaths,
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
2679
3217
|
async function installUpdate(companyId, skillId, options = {}) {
|
|
2680
3218
|
await ensureSkillInventoryCurrent(companyId);
|
|
2681
3219
|
const skill = await getById(companyId, skillId);
|
|
@@ -3690,6 +4228,10 @@ export function companySkillService(db) {
|
|
|
3690
4228
|
metadata,
|
|
3691
4229
|
updatedAt: new Date(),
|
|
3692
4230
|
};
|
|
4231
|
+
if (existing && importedSkillPersistValuesMatchExisting(existing, values)) {
|
|
4232
|
+
out.push(existing);
|
|
4233
|
+
continue;
|
|
4234
|
+
}
|
|
3693
4235
|
const row = existing
|
|
3694
4236
|
? await db
|
|
3695
4237
|
.update(companySkills)
|
|
@@ -3745,6 +4287,630 @@ export function companySkillService(db) {
|
|
|
3745
4287
|
const imported = await upsertImportedSkills(companyId, filteredSkills);
|
|
3746
4288
|
return { imported, warnings };
|
|
3747
4289
|
}
|
|
4290
|
+
async function listTestInputs(companyId, skillId) {
|
|
4291
|
+
const skill = await getById(companyId, skillId);
|
|
4292
|
+
if (!skill)
|
|
4293
|
+
throw notFound("Skill not found");
|
|
4294
|
+
const rows = await db
|
|
4295
|
+
.select()
|
|
4296
|
+
.from(companySkillTestInputs)
|
|
4297
|
+
.where(and(eq(companySkillTestInputs.companyId, companyId), eq(companySkillTestInputs.skillId, skillId), isNull(companySkillTestInputs.deletedAt)))
|
|
4298
|
+
.orderBy(asc(companySkillTestInputs.name), asc(companySkillTestInputs.createdAt));
|
|
4299
|
+
return rows.map(toCompanySkillTestInput);
|
|
4300
|
+
}
|
|
4301
|
+
async function createTestInput(companyId, skillId, input, actor = null) {
|
|
4302
|
+
const skill = await getById(companyId, skillId);
|
|
4303
|
+
if (!skill)
|
|
4304
|
+
throw notFound("Skill not found");
|
|
4305
|
+
const row = await db
|
|
4306
|
+
.insert(companySkillTestInputs)
|
|
4307
|
+
.values({
|
|
4308
|
+
companyId,
|
|
4309
|
+
skillId,
|
|
4310
|
+
name: input.name.trim(),
|
|
4311
|
+
content: input.content,
|
|
4312
|
+
createdBy: actor?.type === "agent"
|
|
4313
|
+
? actor.agentId ?? null
|
|
4314
|
+
: actor?.type === "user"
|
|
4315
|
+
? actor.userId ?? null
|
|
4316
|
+
: null,
|
|
4317
|
+
})
|
|
4318
|
+
.returning()
|
|
4319
|
+
.then((rows) => rows[0] ?? null);
|
|
4320
|
+
if (!row)
|
|
4321
|
+
throw notFound("Failed to persist test input");
|
|
4322
|
+
return toCompanySkillTestInput(row);
|
|
4323
|
+
}
|
|
4324
|
+
async function updateTestInput(companyId, skillId, inputId, input) {
|
|
4325
|
+
const patch = { updatedAt: new Date() };
|
|
4326
|
+
if (input.name !== undefined)
|
|
4327
|
+
patch.name = input.name.trim();
|
|
4328
|
+
if (input.content !== undefined)
|
|
4329
|
+
patch.content = input.content;
|
|
4330
|
+
const row = await db
|
|
4331
|
+
.update(companySkillTestInputs)
|
|
4332
|
+
.set(patch)
|
|
4333
|
+
.where(and(eq(companySkillTestInputs.companyId, companyId), eq(companySkillTestInputs.skillId, skillId), eq(companySkillTestInputs.id, inputId), isNull(companySkillTestInputs.deletedAt)))
|
|
4334
|
+
.returning()
|
|
4335
|
+
.then((rows) => rows[0] ?? null);
|
|
4336
|
+
return row ? toCompanySkillTestInput(row) : null;
|
|
4337
|
+
}
|
|
4338
|
+
async function deleteTestInput(companyId, skillId, inputId) {
|
|
4339
|
+
const row = await db
|
|
4340
|
+
.update(companySkillTestInputs)
|
|
4341
|
+
.set({ deletedAt: new Date(), updatedAt: new Date() })
|
|
4342
|
+
.where(and(eq(companySkillTestInputs.companyId, companyId), eq(companySkillTestInputs.skillId, skillId), eq(companySkillTestInputs.id, inputId), isNull(companySkillTestInputs.deletedAt)))
|
|
4343
|
+
.returning()
|
|
4344
|
+
.then((rows) => rows[0] ?? null);
|
|
4345
|
+
return row ? toCompanySkillTestInput(row) : null;
|
|
4346
|
+
}
|
|
4347
|
+
async function listTestRunTemplates(companyId) {
|
|
4348
|
+
const rows = await db
|
|
4349
|
+
.select()
|
|
4350
|
+
.from(companySkillTestRunTemplates)
|
|
4351
|
+
.where(and(eq(companySkillTestRunTemplates.companyId, companyId), isNull(companySkillTestRunTemplates.deletedAt)))
|
|
4352
|
+
.orderBy(asc(companySkillTestRunTemplates.name), asc(companySkillTestRunTemplates.createdAt));
|
|
4353
|
+
return [
|
|
4354
|
+
builtInSkillTestRunTemplate(companyId),
|
|
4355
|
+
...rows.map(toCompanySkillTestRunTemplate),
|
|
4356
|
+
];
|
|
4357
|
+
}
|
|
4358
|
+
async function createTestRunTemplate(companyId, input, actor = null) {
|
|
4359
|
+
validateSkillTestTemplatePlaceholders(input.body);
|
|
4360
|
+
const row = await db
|
|
4361
|
+
.insert(companySkillTestRunTemplates)
|
|
4362
|
+
.values({
|
|
4363
|
+
companyId,
|
|
4364
|
+
name: input.name.trim(),
|
|
4365
|
+
description: input.description?.trim() || null,
|
|
4366
|
+
body: input.body,
|
|
4367
|
+
createdByAgentId: actor?.type === "agent" ? actor.agentId ?? null : null,
|
|
4368
|
+
createdByUserId: actor?.type === "user" ? actor.userId ?? null : null,
|
|
4369
|
+
updatedByAgentId: actor?.type === "agent" ? actor.agentId ?? null : null,
|
|
4370
|
+
updatedByUserId: actor?.type === "user" ? actor.userId ?? null : null,
|
|
4371
|
+
})
|
|
4372
|
+
.returning()
|
|
4373
|
+
.then((rows) => rows[0] ?? null);
|
|
4374
|
+
if (!row)
|
|
4375
|
+
throw notFound("Failed to persist test run template");
|
|
4376
|
+
return toCompanySkillTestRunTemplate(row);
|
|
4377
|
+
}
|
|
4378
|
+
async function updateTestRunTemplate(companyId, templateId, input, actor = null) {
|
|
4379
|
+
if (templateId === BUILT_IN_SKILL_TEST_RUN_TEMPLATE_ID) {
|
|
4380
|
+
throw unprocessable("Built-in test run templates are read-only.");
|
|
4381
|
+
}
|
|
4382
|
+
if (input.body !== undefined) {
|
|
4383
|
+
validateSkillTestTemplatePlaceholders(input.body);
|
|
4384
|
+
}
|
|
4385
|
+
const patch = {
|
|
4386
|
+
updatedAt: new Date(),
|
|
4387
|
+
updatedByAgentId: actor?.type === "agent" ? actor.agentId ?? null : null,
|
|
4388
|
+
updatedByUserId: actor?.type === "user" ? actor.userId ?? null : null,
|
|
4389
|
+
};
|
|
4390
|
+
if (input.name !== undefined)
|
|
4391
|
+
patch.name = input.name.trim();
|
|
4392
|
+
if (input.description !== undefined)
|
|
4393
|
+
patch.description = input.description?.trim() || null;
|
|
4394
|
+
if (input.body !== undefined)
|
|
4395
|
+
patch.body = input.body;
|
|
4396
|
+
const row = await db
|
|
4397
|
+
.update(companySkillTestRunTemplates)
|
|
4398
|
+
.set(patch)
|
|
4399
|
+
.where(and(eq(companySkillTestRunTemplates.companyId, companyId), eq(companySkillTestRunTemplates.id, templateId), isNull(companySkillTestRunTemplates.deletedAt)))
|
|
4400
|
+
.returning()
|
|
4401
|
+
.then((rows) => rows[0] ?? null);
|
|
4402
|
+
return row ? toCompanySkillTestRunTemplate(row) : null;
|
|
4403
|
+
}
|
|
4404
|
+
async function deleteTestRunTemplate(companyId, templateId) {
|
|
4405
|
+
if (templateId === BUILT_IN_SKILL_TEST_RUN_TEMPLATE_ID) {
|
|
4406
|
+
throw unprocessable("Built-in test run templates are read-only.");
|
|
4407
|
+
}
|
|
4408
|
+
const row = await db
|
|
4409
|
+
.update(companySkillTestRunTemplates)
|
|
4410
|
+
.set({ deletedAt: new Date(), updatedAt: new Date() })
|
|
4411
|
+
.where(and(eq(companySkillTestRunTemplates.companyId, companyId), eq(companySkillTestRunTemplates.id, templateId), isNull(companySkillTestRunTemplates.deletedAt)))
|
|
4412
|
+
.returning()
|
|
4413
|
+
.then((rows) => rows[0] ?? null);
|
|
4414
|
+
return row ? toCompanySkillTestRunTemplate(row) : null;
|
|
4415
|
+
}
|
|
4416
|
+
async function resolveTestRunTemplateSnapshot(companyId, input) {
|
|
4417
|
+
if (input.templateSnapshot !== undefined) {
|
|
4418
|
+
const snapshot = input.templateSnapshot;
|
|
4419
|
+
if (!snapshot || snapshot.templateId === null)
|
|
4420
|
+
return null;
|
|
4421
|
+
validateSkillTestTemplatePlaceholders(snapshot.templateBody ?? "");
|
|
4422
|
+
return {
|
|
4423
|
+
templateId: snapshot.templateId,
|
|
4424
|
+
templateName: snapshot.templateName,
|
|
4425
|
+
templateBody: snapshot.templateBody,
|
|
4426
|
+
};
|
|
4427
|
+
}
|
|
4428
|
+
const templateId = input.templateId === undefined ? BUILT_IN_SKILL_TEST_RUN_TEMPLATE_ID : input.templateId;
|
|
4429
|
+
if (templateId === null)
|
|
4430
|
+
return null;
|
|
4431
|
+
if (templateId === BUILT_IN_SKILL_TEST_RUN_TEMPLATE_ID) {
|
|
4432
|
+
const template = builtInSkillTestRunTemplate(companyId);
|
|
4433
|
+
return {
|
|
4434
|
+
templateId: template.id,
|
|
4435
|
+
templateName: template.name,
|
|
4436
|
+
templateBody: template.body,
|
|
4437
|
+
};
|
|
4438
|
+
}
|
|
4439
|
+
const row = await db
|
|
4440
|
+
.select()
|
|
4441
|
+
.from(companySkillTestRunTemplates)
|
|
4442
|
+
.where(and(eq(companySkillTestRunTemplates.companyId, companyId), eq(companySkillTestRunTemplates.id, templateId), isNull(companySkillTestRunTemplates.deletedAt)))
|
|
4443
|
+
.then((rows) => rows[0] ?? null);
|
|
4444
|
+
if (!row)
|
|
4445
|
+
throw notFound("Test run template not found");
|
|
4446
|
+
return {
|
|
4447
|
+
templateId: row.id,
|
|
4448
|
+
templateName: row.name,
|
|
4449
|
+
templateBody: row.body,
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
async function ensureRunSkillVersion(companyId, skill, actor) {
|
|
4453
|
+
const currentSnapshot = serializeVersionFileInventory(await collectVersionFileInventory(companyId, skill));
|
|
4454
|
+
if (currentSnapshot.length === 0) {
|
|
4455
|
+
throw unprocessable("Cannot run a skill test for a skill with zero files.");
|
|
4456
|
+
}
|
|
4457
|
+
const currentVersion = await getCurrentVersion(skill);
|
|
4458
|
+
if (!currentVersion || !versionInventorySnapshotEqual(currentVersion.fileInventory, currentSnapshot)) {
|
|
4459
|
+
return createVersion(companyId, skill.id, { label: "Auto version for test run" }, actor);
|
|
4460
|
+
}
|
|
4461
|
+
return currentVersion;
|
|
4462
|
+
}
|
|
4463
|
+
function snapshotAgentConfig(agent) {
|
|
4464
|
+
if (!agent)
|
|
4465
|
+
return {};
|
|
4466
|
+
const adapterConfig = isPlainRecord(agent.adapterConfig) ? agent.adapterConfig : {};
|
|
4467
|
+
const runtimeConfig = isPlainRecord(agent.runtimeConfig) ? agent.runtimeConfig : {};
|
|
4468
|
+
return {
|
|
4469
|
+
agentId: agent.id,
|
|
4470
|
+
name: agent.name,
|
|
4471
|
+
role: agent.role,
|
|
4472
|
+
adapterType: agent.adapterType,
|
|
4473
|
+
model: asString(adapterConfig.model) ?? asString(runtimeConfig.model) ?? null,
|
|
4474
|
+
adapterConfig,
|
|
4475
|
+
runtimeConfig,
|
|
4476
|
+
assignedSkills: isPlainRecord(adapterConfig.paperclipSkillSync)
|
|
4477
|
+
? adapterConfig.paperclipSkillSync
|
|
4478
|
+
: null,
|
|
4479
|
+
instructionsRef: asString(adapterConfig.instructionsFilePath) ??
|
|
4480
|
+
asString(adapterConfig.instructionsPath) ??
|
|
4481
|
+
asString(adapterConfig.instructionsRef) ??
|
|
4482
|
+
null,
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
async function testRunCostByIssueIds(companyId, issueIds) {
|
|
4486
|
+
if (issueIds.length === 0)
|
|
4487
|
+
return new Map();
|
|
4488
|
+
const rows = await db
|
|
4489
|
+
.select({
|
|
4490
|
+
issueId: costEvents.issueId,
|
|
4491
|
+
costCents: sql `coalesce(sum(${costEvents.costCents}), 0)::int`,
|
|
4492
|
+
inputTokens: sql `coalesce(sum(${costEvents.inputTokens}), 0)::int`,
|
|
4493
|
+
cachedInputTokens: sql `coalesce(sum(${costEvents.cachedInputTokens}), 0)::int`,
|
|
4494
|
+
outputTokens: sql `coalesce(sum(${costEvents.outputTokens}), 0)::int`,
|
|
4495
|
+
})
|
|
4496
|
+
.from(costEvents)
|
|
4497
|
+
.where(and(eq(costEvents.companyId, companyId), inArray(costEvents.issueId, issueIds)))
|
|
4498
|
+
.groupBy(costEvents.issueId);
|
|
4499
|
+
return new Map(rows.flatMap((row) => row.issueId
|
|
4500
|
+
? [[row.issueId, {
|
|
4501
|
+
costCents: Number(row.costCents ?? 0),
|
|
4502
|
+
inputTokens: Number(row.inputTokens ?? 0),
|
|
4503
|
+
cachedInputTokens: Number(row.cachedInputTokens ?? 0),
|
|
4504
|
+
outputTokens: Number(row.outputTokens ?? 0),
|
|
4505
|
+
}]]
|
|
4506
|
+
: []));
|
|
4507
|
+
}
|
|
4508
|
+
async function hydrateTestRuns(companyId, rows) {
|
|
4509
|
+
const costByIssueId = await testRunCostByIssueIds(companyId, rows.map((row) => row.issueId));
|
|
4510
|
+
return rows.map((row) => toCompanySkillTestRun(row, costByIssueId.get(row.issueId) ?? emptyTestRunCost(), Boolean(row.harnessIssueDeletedAt)));
|
|
4511
|
+
}
|
|
4512
|
+
async function createTestRun(companyId, skillId, input, actor, deps) {
|
|
4513
|
+
const skill = await getById(companyId, skillId);
|
|
4514
|
+
if (!skill)
|
|
4515
|
+
throw notFound("Skill not found");
|
|
4516
|
+
const agent = await agents.getById(input.agentId);
|
|
4517
|
+
if (!agent || agent.companyId !== companyId)
|
|
4518
|
+
throw notFound("Agent not found");
|
|
4519
|
+
if (agent.status === "paused")
|
|
4520
|
+
throw unprocessable("Paused agents cannot run skill tests.");
|
|
4521
|
+
const sourceInput = input.inputId
|
|
4522
|
+
? await db
|
|
4523
|
+
.select()
|
|
4524
|
+
.from(companySkillTestInputs)
|
|
4525
|
+
.where(and(eq(companySkillTestInputs.companyId, companyId), eq(companySkillTestInputs.skillId, skillId), eq(companySkillTestInputs.id, input.inputId), isNull(companySkillTestInputs.deletedAt)))
|
|
4526
|
+
.then((rows) => rows[0] ?? null)
|
|
4527
|
+
: null;
|
|
4528
|
+
if (input.inputId && !sourceInput)
|
|
4529
|
+
throw notFound("Test input not found");
|
|
4530
|
+
const inputSnapshot = (sourceInput?.content ?? input.content ?? "").trim();
|
|
4531
|
+
if (!inputSnapshot)
|
|
4532
|
+
throw unprocessable("Test input content cannot be empty.");
|
|
4533
|
+
// Re-run pins the viewed run's version so the new run reproduces the same
|
|
4534
|
+
// snapshots; a plain run auto-snapshots the live head.
|
|
4535
|
+
const version = input.skillVersionId
|
|
4536
|
+
? await getVersion(companyId, skillId, input.skillVersionId)
|
|
4537
|
+
: await ensureRunSkillVersion(companyId, skill, actor);
|
|
4538
|
+
if (!version)
|
|
4539
|
+
throw notFound("Skill version not found");
|
|
4540
|
+
const runId = randomUUID();
|
|
4541
|
+
const issueId = randomUUID();
|
|
4542
|
+
const outputDocumentKey = "output";
|
|
4543
|
+
const templateSnapshot = await resolveTestRunTemplateSnapshot(companyId, input);
|
|
4544
|
+
const renderedTemplateBody = templateSnapshot?.templateBody
|
|
4545
|
+
? renderSkillTestTemplate(templateSnapshot.templateBody, {
|
|
4546
|
+
skillName: skill.name,
|
|
4547
|
+
skillKey: skill.key,
|
|
4548
|
+
skillInvocation: skill.key,
|
|
4549
|
+
skillVersion: String(version.revisionNumber),
|
|
4550
|
+
runId,
|
|
4551
|
+
issueId,
|
|
4552
|
+
outputDocumentKey,
|
|
4553
|
+
}).trim()
|
|
4554
|
+
: null;
|
|
4555
|
+
const harnessIssueDescription = buildHarnessIssueDescription(inputSnapshot, renderedTemplateBody);
|
|
4556
|
+
await deps.createHarnessIssue({
|
|
4557
|
+
id: issueId,
|
|
4558
|
+
title: `Skill test: ${skill.name}`,
|
|
4559
|
+
description: harnessIssueDescription,
|
|
4560
|
+
assigneeAgentId: agent.id,
|
|
4561
|
+
harnessKind: "skill_test",
|
|
4562
|
+
workMode: "skill_test",
|
|
4563
|
+
status: "todo",
|
|
4564
|
+
originKind: "skill_test",
|
|
4565
|
+
originId: runId,
|
|
4566
|
+
originFingerprint: `skill_test:${runId}`,
|
|
4567
|
+
});
|
|
4568
|
+
const now = new Date();
|
|
4569
|
+
const retentionDays = Math.max(0, deps.retentionDays ?? 7);
|
|
4570
|
+
const previousExpiresAt = new Date(now.getTime() + retentionDays * 24 * 60 * 60 * 1000);
|
|
4571
|
+
const cleanupCreatedHarnessIssue = async () => {
|
|
4572
|
+
await deps.cleanupHarnessIssue?.(issueId).catch(() => { });
|
|
4573
|
+
};
|
|
4574
|
+
const row = await db.transaction(async (tx) => {
|
|
4575
|
+
await tx
|
|
4576
|
+
.update(companySkillTestRuns)
|
|
4577
|
+
.set({
|
|
4578
|
+
supersededAt: now,
|
|
4579
|
+
status: sql `
|
|
4580
|
+
case when ${companySkillTestRuns.status} in ('queued', 'running')
|
|
4581
|
+
then 'cancelled'
|
|
4582
|
+
else ${companySkillTestRuns.status}
|
|
4583
|
+
end
|
|
4584
|
+
`,
|
|
4585
|
+
error: sql `
|
|
4586
|
+
case when ${companySkillTestRuns.status} in ('queued', 'running')
|
|
4587
|
+
then coalesce(${companySkillTestRuns.error}, 'Superseded by newer run')
|
|
4588
|
+
else ${companySkillTestRuns.error}
|
|
4589
|
+
end
|
|
4590
|
+
`,
|
|
4591
|
+
harnessIssueExpiresAt: previousExpiresAt,
|
|
4592
|
+
updatedAt: now,
|
|
4593
|
+
})
|
|
4594
|
+
.where(and(eq(companySkillTestRuns.companyId, companyId), eq(companySkillTestRuns.skillId, skillId), sourceInput?.id
|
|
4595
|
+
? eq(companySkillTestRuns.inputId, sourceInput.id)
|
|
4596
|
+
: isNull(companySkillTestRuns.inputId), isNull(companySkillTestRuns.supersededAt)));
|
|
4597
|
+
return await tx
|
|
4598
|
+
.insert(companySkillTestRuns)
|
|
4599
|
+
.values({
|
|
4600
|
+
id: runId,
|
|
4601
|
+
companyId,
|
|
4602
|
+
skillId,
|
|
4603
|
+
inputId: sourceInput?.id ?? null,
|
|
4604
|
+
inputSnapshot,
|
|
4605
|
+
skillVersionId: version.id,
|
|
4606
|
+
agentId: agent.id,
|
|
4607
|
+
agentConfigSnapshot: snapshotAgentConfig(agent),
|
|
4608
|
+
issueId,
|
|
4609
|
+
templateId: templateSnapshot?.templateId ?? null,
|
|
4610
|
+
templateName: templateSnapshot?.templateName ?? null,
|
|
4611
|
+
templateBody: templateSnapshot?.templateBody ?? null,
|
|
4612
|
+
renderedTemplateBody,
|
|
4613
|
+
harnessIssueDescription,
|
|
4614
|
+
status: "queued",
|
|
4615
|
+
outputDocumentKey,
|
|
4616
|
+
})
|
|
4617
|
+
.returning()
|
|
4618
|
+
.then((rows) => rows[0] ?? null);
|
|
4619
|
+
}).catch(async (error) => {
|
|
4620
|
+
await cleanupCreatedHarnessIssue();
|
|
4621
|
+
throw error;
|
|
4622
|
+
});
|
|
4623
|
+
if (!row) {
|
|
4624
|
+
await cleanupCreatedHarnessIssue();
|
|
4625
|
+
throw notFound("Failed to persist skill test run");
|
|
4626
|
+
}
|
|
4627
|
+
await deps.wakeHarnessIssue(issueId, agent.id);
|
|
4628
|
+
return (await hydrateTestRuns(companyId, [row]))[0];
|
|
4629
|
+
}
|
|
4630
|
+
async function listTestRuns(companyId, skillId, query = {}) {
|
|
4631
|
+
const skill = await getById(companyId, skillId);
|
|
4632
|
+
if (!skill)
|
|
4633
|
+
throw notFound("Skill not found");
|
|
4634
|
+
const conditions = [
|
|
4635
|
+
eq(companySkillTestRuns.companyId, companyId),
|
|
4636
|
+
eq(companySkillTestRuns.skillId, skillId),
|
|
4637
|
+
isNull(companySkillTestRuns.deletedAt),
|
|
4638
|
+
];
|
|
4639
|
+
if (query.inputId)
|
|
4640
|
+
conditions.push(eq(companySkillTestRuns.inputId, query.inputId));
|
|
4641
|
+
const rows = await db
|
|
4642
|
+
.select()
|
|
4643
|
+
.from(companySkillTestRuns)
|
|
4644
|
+
.where(and(...conditions))
|
|
4645
|
+
.orderBy(desc(companySkillTestRuns.createdAt), desc(companySkillTestRuns.id));
|
|
4646
|
+
return hydrateTestRuns(companyId, rows);
|
|
4647
|
+
}
|
|
4648
|
+
async function getTestRunDetail(companyId, skillId, runId) {
|
|
4649
|
+
const row = await db
|
|
4650
|
+
.select()
|
|
4651
|
+
.from(companySkillTestRuns)
|
|
4652
|
+
.where(and(eq(companySkillTestRuns.companyId, companyId), eq(companySkillTestRuns.skillId, skillId), eq(companySkillTestRuns.id, runId), isNull(companySkillTestRuns.deletedAt)))
|
|
4653
|
+
.then((rows) => rows[0] ?? null);
|
|
4654
|
+
if (!row)
|
|
4655
|
+
return null;
|
|
4656
|
+
const [run] = await hydrateTestRuns(companyId, [row]);
|
|
4657
|
+
if (!run)
|
|
4658
|
+
return null;
|
|
4659
|
+
const harnessIssueGone = Boolean(row.harnessIssueDeletedAt);
|
|
4660
|
+
const [version, issue, documentRows, interactionRows, attachmentRows, workProductRows] = await Promise.all([
|
|
4661
|
+
getVersion(companyId, skillId, row.skillVersionId),
|
|
4662
|
+
harnessIssueGone
|
|
4663
|
+
? Promise.resolve(null)
|
|
4664
|
+
: db
|
|
4665
|
+
.select({
|
|
4666
|
+
id: issues.id,
|
|
4667
|
+
identifier: issues.identifier,
|
|
4668
|
+
title: issues.title,
|
|
4669
|
+
status: issues.status,
|
|
4670
|
+
hiddenAt: issues.hiddenAt,
|
|
4671
|
+
})
|
|
4672
|
+
.from(issues)
|
|
4673
|
+
.where(and(eq(issues.companyId, companyId), eq(issues.id, row.issueId)))
|
|
4674
|
+
.then((rows) => rows[0] ?? null),
|
|
4675
|
+
harnessIssueGone
|
|
4676
|
+
? Promise.resolve([])
|
|
4677
|
+
: db
|
|
4678
|
+
.select(issueDocumentSelect)
|
|
4679
|
+
.from(issueDocuments)
|
|
4680
|
+
.innerJoin(documents, eq(issueDocuments.documentId, documents.id))
|
|
4681
|
+
.where(and(eq(issueDocuments.companyId, companyId), eq(issueDocuments.issueId, row.issueId)))
|
|
4682
|
+
.orderBy(asc(issueDocuments.key)),
|
|
4683
|
+
harnessIssueGone
|
|
4684
|
+
? Promise.resolve([])
|
|
4685
|
+
: db
|
|
4686
|
+
.select({
|
|
4687
|
+
id: issueThreadInteractions.id,
|
|
4688
|
+
kind: issueThreadInteractions.kind,
|
|
4689
|
+
status: issueThreadInteractions.status,
|
|
4690
|
+
title: issueThreadInteractions.title,
|
|
4691
|
+
createdAt: issueThreadInteractions.createdAt,
|
|
4692
|
+
updatedAt: issueThreadInteractions.updatedAt,
|
|
4693
|
+
})
|
|
4694
|
+
.from(issueThreadInteractions)
|
|
4695
|
+
.where(and(eq(issueThreadInteractions.companyId, companyId), eq(issueThreadInteractions.issueId, row.issueId)))
|
|
4696
|
+
.orderBy(desc(issueThreadInteractions.createdAt)),
|
|
4697
|
+
harnessIssueGone
|
|
4698
|
+
? Promise.resolve([])
|
|
4699
|
+
: db
|
|
4700
|
+
.select({
|
|
4701
|
+
id: issueAttachments.id,
|
|
4702
|
+
companyId: issueAttachments.companyId,
|
|
4703
|
+
issueId: issueAttachments.issueId,
|
|
4704
|
+
issueCommentId: issueAttachments.issueCommentId,
|
|
4705
|
+
assetId: issueAttachments.assetId,
|
|
4706
|
+
provider: assets.provider,
|
|
4707
|
+
objectKey: assets.objectKey,
|
|
4708
|
+
contentType: assets.contentType,
|
|
4709
|
+
byteSize: assets.byteSize,
|
|
4710
|
+
sha256: assets.sha256,
|
|
4711
|
+
originalFilename: assets.originalFilename,
|
|
4712
|
+
createdByAgentId: assets.createdByAgentId,
|
|
4713
|
+
createdByUserId: assets.createdByUserId,
|
|
4714
|
+
createdAt: issueAttachments.createdAt,
|
|
4715
|
+
updatedAt: issueAttachments.updatedAt,
|
|
4716
|
+
})
|
|
4717
|
+
.from(issueAttachments)
|
|
4718
|
+
.innerJoin(assets, eq(issueAttachments.assetId, assets.id))
|
|
4719
|
+
.where(and(eq(issueAttachments.companyId, companyId), eq(issueAttachments.issueId, row.issueId)))
|
|
4720
|
+
.orderBy(desc(issueAttachments.createdAt)),
|
|
4721
|
+
harnessIssueGone
|
|
4722
|
+
? Promise.resolve([])
|
|
4723
|
+
: db
|
|
4724
|
+
.select()
|
|
4725
|
+
.from(issueWorkProducts)
|
|
4726
|
+
.where(and(eq(issueWorkProducts.companyId, companyId), eq(issueWorkProducts.issueId, row.issueId)))
|
|
4727
|
+
.orderBy(desc(issueWorkProducts.isPrimary), desc(issueWorkProducts.updatedAt)),
|
|
4728
|
+
]);
|
|
4729
|
+
if (!version)
|
|
4730
|
+
throw notFound("Skill version not found");
|
|
4731
|
+
const harnessAvailable = !harnessIssueGone && issue !== null;
|
|
4732
|
+
const harnessDocuments = harnessAvailable
|
|
4733
|
+
? documentRows.map((doc) => ({
|
|
4734
|
+
...mapIssueDocumentRow(doc, false),
|
|
4735
|
+
format: doc.format,
|
|
4736
|
+
body: doc.latestBody,
|
|
4737
|
+
}))
|
|
4738
|
+
: [];
|
|
4739
|
+
const harnessAttachments = harnessAvailable
|
|
4740
|
+
? attachmentRows.map((attachment) => ({
|
|
4741
|
+
...attachment,
|
|
4742
|
+
contentPath: `/api/attachments/${attachment.id}/content`,
|
|
4743
|
+
openPath: `/api/attachments/${attachment.id}/content`,
|
|
4744
|
+
downloadPath: `/api/attachments/${attachment.id}/content?download=1`,
|
|
4745
|
+
}))
|
|
4746
|
+
: [];
|
|
4747
|
+
const harnessWorkProducts = harnessAvailable ? workProductRows.map(toIssueWorkProduct) : [];
|
|
4748
|
+
const harnessContent = {
|
|
4749
|
+
available: harnessAvailable,
|
|
4750
|
+
unavailableReason: harnessAvailable
|
|
4751
|
+
? null
|
|
4752
|
+
: harnessIssueGone
|
|
4753
|
+
? (row.harnessIssueExpiresAt && row.harnessIssueDeletedAt && row.harnessIssueExpiresAt <= row.harnessIssueDeletedAt
|
|
4754
|
+
? "expired"
|
|
4755
|
+
: "deleted")
|
|
4756
|
+
: "missing",
|
|
4757
|
+
documents: harnessDocuments,
|
|
4758
|
+
attachments: harnessAttachments,
|
|
4759
|
+
workProducts: harnessWorkProducts,
|
|
4760
|
+
};
|
|
4761
|
+
return {
|
|
4762
|
+
...run,
|
|
4763
|
+
skillVersion: version,
|
|
4764
|
+
outputBody: run.outputSnapshot,
|
|
4765
|
+
harnessContent,
|
|
4766
|
+
harnessIssue: issue ? {
|
|
4767
|
+
id: issue.id,
|
|
4768
|
+
identifier: issue.identifier ?? null,
|
|
4769
|
+
title: issue.title,
|
|
4770
|
+
status: issue.status,
|
|
4771
|
+
hiddenAt: issue.hiddenAt ?? null,
|
|
4772
|
+
} : null,
|
|
4773
|
+
documents: harnessDocuments.map((doc) => ({
|
|
4774
|
+
key: doc.key,
|
|
4775
|
+
title: doc.title ?? null,
|
|
4776
|
+
updatedAt: doc.updatedAt,
|
|
4777
|
+
body: doc.body,
|
|
4778
|
+
})),
|
|
4779
|
+
interactions: interactionRows.map((interaction) => ({
|
|
4780
|
+
id: interaction.id,
|
|
4781
|
+
kind: interaction.kind,
|
|
4782
|
+
status: interaction.status,
|
|
4783
|
+
title: interaction.title ?? interaction.kind,
|
|
4784
|
+
createdAt: interaction.createdAt,
|
|
4785
|
+
updatedAt: interaction.updatedAt,
|
|
4786
|
+
})),
|
|
4787
|
+
artifacts: [
|
|
4788
|
+
...harnessAttachments.map((attachment) => ({
|
|
4789
|
+
id: attachment.id,
|
|
4790
|
+
kind: "attachment",
|
|
4791
|
+
title: attachment.originalFilename ?? "Attachment",
|
|
4792
|
+
summary: null,
|
|
4793
|
+
createdAt: attachment.createdAt,
|
|
4794
|
+
})),
|
|
4795
|
+
...harnessWorkProducts.map((product) => ({
|
|
4796
|
+
id: product.id,
|
|
4797
|
+
kind: "work_product",
|
|
4798
|
+
title: product.title,
|
|
4799
|
+
summary: product.summary ?? null,
|
|
4800
|
+
createdAt: product.createdAt,
|
|
4801
|
+
})),
|
|
4802
|
+
].sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime()),
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
async function completeTestRunForIssue(input) {
|
|
4806
|
+
const row = await db
|
|
4807
|
+
.select()
|
|
4808
|
+
.from(companySkillTestRuns)
|
|
4809
|
+
.where(and(eq(companySkillTestRuns.companyId, input.companyId), eq(companySkillTestRuns.issueId, input.issueId), isNull(companySkillTestRuns.deletedAt), isNull(companySkillTestRuns.supersededAt)))
|
|
4810
|
+
.then((rows) => rows[0] ?? null);
|
|
4811
|
+
if (!row || ["succeeded", "failed", "cancelled"].includes(row.status))
|
|
4812
|
+
return row
|
|
4813
|
+
? (await hydrateTestRuns(input.companyId, [row]))[0] ?? null
|
|
4814
|
+
: null;
|
|
4815
|
+
const outputDocumentKey = row.outputDocumentKey || "output";
|
|
4816
|
+
const outputDocument = await db
|
|
4817
|
+
.select({ body: documents.latestBody })
|
|
4818
|
+
.from(issueDocuments)
|
|
4819
|
+
.innerJoin(documents, eq(issueDocuments.documentId, documents.id))
|
|
4820
|
+
.where(and(eq(issueDocuments.companyId, input.companyId), eq(issueDocuments.issueId, input.issueId), eq(issueDocuments.key, outputDocumentKey)))
|
|
4821
|
+
.then((rows) => rows[0] ?? null);
|
|
4822
|
+
const updated = await db
|
|
4823
|
+
.update(companySkillTestRuns)
|
|
4824
|
+
.set({
|
|
4825
|
+
status: input.outcome,
|
|
4826
|
+
outputSnapshot: outputDocument?.body ?? row.outputSnapshot ?? "",
|
|
4827
|
+
error: input.error ?? null,
|
|
4828
|
+
updatedAt: new Date(),
|
|
4829
|
+
})
|
|
4830
|
+
.where(and(eq(companySkillTestRuns.companyId, input.companyId), eq(companySkillTestRuns.id, row.id)))
|
|
4831
|
+
.returning()
|
|
4832
|
+
.then((rows) => rows[0] ?? null);
|
|
4833
|
+
return updated ? (await hydrateTestRuns(input.companyId, [updated]))[0] ?? null : null;
|
|
4834
|
+
}
|
|
4835
|
+
async function markTestRunRunning(companyId, issueId) {
|
|
4836
|
+
const row = await db
|
|
4837
|
+
.update(companySkillTestRuns)
|
|
4838
|
+
.set({ status: "running", updatedAt: new Date() })
|
|
4839
|
+
.where(and(eq(companySkillTestRuns.companyId, companyId), eq(companySkillTestRuns.issueId, issueId), eq(companySkillTestRuns.status, "queued"), isNull(companySkillTestRuns.deletedAt), isNull(companySkillTestRuns.supersededAt)))
|
|
4840
|
+
.returning()
|
|
4841
|
+
.then((rows) => rows[0] ?? null);
|
|
4842
|
+
return row ? (await hydrateTestRuns(companyId, [row]))[0] ?? null : null;
|
|
4843
|
+
}
|
|
4844
|
+
async function cancelTestRun(companyId, skillId, runId, deps) {
|
|
4845
|
+
const existing = await db
|
|
4846
|
+
.select()
|
|
4847
|
+
.from(companySkillTestRuns)
|
|
4848
|
+
.where(and(eq(companySkillTestRuns.companyId, companyId), eq(companySkillTestRuns.skillId, skillId), eq(companySkillTestRuns.id, runId), isNull(companySkillTestRuns.deletedAt), isNull(companySkillTestRuns.supersededAt)))
|
|
4849
|
+
.then((rows) => rows[0] ?? null);
|
|
4850
|
+
if (!existing)
|
|
4851
|
+
return null;
|
|
4852
|
+
if (["succeeded", "failed", "cancelled"].includes(existing.status)) {
|
|
4853
|
+
return (await hydrateTestRuns(companyId, [existing]))[0] ?? null;
|
|
4854
|
+
}
|
|
4855
|
+
await deps.cancelHarnessIssue(existing.issueId);
|
|
4856
|
+
return completeTestRunForIssue({
|
|
4857
|
+
companyId,
|
|
4858
|
+
issueId: existing.issueId,
|
|
4859
|
+
outcome: "cancelled",
|
|
4860
|
+
error: "Cancelled by operator",
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
async function deleteTestRun(companyId, skillId, runId, deps) {
|
|
4864
|
+
const existing = await db
|
|
4865
|
+
.select()
|
|
4866
|
+
.from(companySkillTestRuns)
|
|
4867
|
+
.where(and(eq(companySkillTestRuns.companyId, companyId), eq(companySkillTestRuns.skillId, skillId), eq(companySkillTestRuns.id, runId), isNull(companySkillTestRuns.deletedAt)))
|
|
4868
|
+
.then((rows) => rows[0] ?? null);
|
|
4869
|
+
if (!existing)
|
|
4870
|
+
return null;
|
|
4871
|
+
// Only terminal runs are deletable — an in-flight run must be cancelled first
|
|
4872
|
+
// so we never orphan a live harness task.
|
|
4873
|
+
if (!["succeeded", "failed", "cancelled"].includes(existing.status)) {
|
|
4874
|
+
throw unprocessable("Cancel the run before deleting it.");
|
|
4875
|
+
}
|
|
4876
|
+
const now = new Date();
|
|
4877
|
+
const updated = await db.transaction(async (tx) => {
|
|
4878
|
+
return await tx
|
|
4879
|
+
.update(companySkillTestRuns)
|
|
4880
|
+
.set({ deletedAt: now, harnessIssueDeletedAt: existing.harnessIssueDeletedAt ?? now, updatedAt: now })
|
|
4881
|
+
.where(and(eq(companySkillTestRuns.companyId, companyId), eq(companySkillTestRuns.id, runId)))
|
|
4882
|
+
.returning()
|
|
4883
|
+
.then((rows) => rows[0] ?? null);
|
|
4884
|
+
});
|
|
4885
|
+
// Hide the (already-terminal) harness task so the deleted run leaves nothing
|
|
4886
|
+
// dangling on the board; best-effort, run row is the source of truth.
|
|
4887
|
+
if (!existing.harnessIssueDeletedAt) {
|
|
4888
|
+
await deps.hideHarnessIssue(existing.issueId).catch(() => { });
|
|
4889
|
+
}
|
|
4890
|
+
return updated ? (await hydrateTestRuns(companyId, [updated]))[0] ?? null : null;
|
|
4891
|
+
}
|
|
4892
|
+
async function pruneExpiredTestHarnessIssues(companyId, now = new Date()) {
|
|
4893
|
+
const rows = await db
|
|
4894
|
+
.select({
|
|
4895
|
+
id: companySkillTestRuns.id,
|
|
4896
|
+
issueId: companySkillTestRuns.issueId,
|
|
4897
|
+
})
|
|
4898
|
+
.from(companySkillTestRuns)
|
|
4899
|
+
.where(and(eq(companySkillTestRuns.companyId, companyId), lt(companySkillTestRuns.harnessIssueExpiresAt, now), isNull(companySkillTestRuns.harnessIssueDeletedAt)));
|
|
4900
|
+
for (const row of rows) {
|
|
4901
|
+
await db.transaction(async (tx) => {
|
|
4902
|
+
await tx
|
|
4903
|
+
.update(issues)
|
|
4904
|
+
.set({ hiddenAt: now, updatedAt: now })
|
|
4905
|
+
.where(and(eq(issues.companyId, companyId), eq(issues.id, row.issueId), eq(issues.harnessKind, "skill_test")));
|
|
4906
|
+
await tx
|
|
4907
|
+
.update(companySkillTestRuns)
|
|
4908
|
+
.set({ harnessIssueDeletedAt: now, updatedAt: now })
|
|
4909
|
+
.where(and(eq(companySkillTestRuns.companyId, companyId), eq(companySkillTestRuns.id, row.id)));
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
4912
|
+
return { pruned: rows.length };
|
|
4913
|
+
}
|
|
3748
4914
|
async function deleteSkill(companyId, skillId) {
|
|
3749
4915
|
const row = await db
|
|
3750
4916
|
.select()
|
|
@@ -3781,16 +4947,18 @@ export function companySkillService(db) {
|
|
|
3781
4947
|
listFull,
|
|
3782
4948
|
getById,
|
|
3783
4949
|
getByKey,
|
|
4950
|
+
getByRouteRef,
|
|
3784
4951
|
resolveRequestedSkillKeys: async (companyId, requestedReferences) => {
|
|
3785
4952
|
const skills = await listFull(companyId);
|
|
3786
4953
|
return resolveRequestedSkillKeysOrThrow(skills, requestedReferences);
|
|
3787
4954
|
},
|
|
3788
|
-
resolveRequestedSkillEntries: async (companyId, requestedSelections) => {
|
|
4955
|
+
resolveRequestedSkillEntries: async (companyId, requestedSelections, options) => {
|
|
3789
4956
|
const skills = await listFull(companyId);
|
|
3790
|
-
return resolveRequestedSkillEntriesOrThrow(db, companyId, skills, requestedSelections);
|
|
4957
|
+
return resolveRequestedSkillEntriesOrThrow(db, companyId, skills, requestedSelections, options);
|
|
3791
4958
|
},
|
|
3792
4959
|
categoryCounts,
|
|
3793
4960
|
detail,
|
|
4961
|
+
forkPrecheck,
|
|
3794
4962
|
listVersions,
|
|
3795
4963
|
getVersion,
|
|
3796
4964
|
createVersion,
|
|
@@ -3805,8 +4973,25 @@ export function companySkillService(db) {
|
|
|
3805
4973
|
readFile,
|
|
3806
4974
|
updateSkill,
|
|
3807
4975
|
updateFile,
|
|
4976
|
+
deleteFile,
|
|
3808
4977
|
createLocalSkill,
|
|
3809
4978
|
deleteSkill,
|
|
4979
|
+
listTestInputs,
|
|
4980
|
+
createTestInput,
|
|
4981
|
+
updateTestInput,
|
|
4982
|
+
deleteTestInput,
|
|
4983
|
+
listTestRunTemplates,
|
|
4984
|
+
createTestRunTemplate,
|
|
4985
|
+
updateTestRunTemplate,
|
|
4986
|
+
deleteTestRunTemplate,
|
|
4987
|
+
createTestRun,
|
|
4988
|
+
listTestRuns,
|
|
4989
|
+
getTestRunDetail,
|
|
4990
|
+
completeTestRunForIssue,
|
|
4991
|
+
markTestRunRunning,
|
|
4992
|
+
cancelTestRun,
|
|
4993
|
+
deleteTestRun,
|
|
4994
|
+
pruneExpiredTestHarnessIssues,
|
|
3810
4995
|
importFromSource,
|
|
3811
4996
|
installFromCatalog,
|
|
3812
4997
|
scanProjectWorkspaces,
|