@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.
Files changed (485) hide show
  1. package/dist/adapters/index.d.ts +1 -1
  2. package/dist/adapters/index.d.ts.map +1 -1
  3. package/dist/adapters/index.js.map +1 -1
  4. package/dist/adapters/process/execute.d.ts.map +1 -1
  5. package/dist/adapters/process/execute.js +1 -0
  6. package/dist/adapters/process/execute.js.map +1 -1
  7. package/dist/adapters/registry.d.ts.map +1 -1
  8. package/dist/adapters/registry.js +79 -51
  9. package/dist/adapters/registry.js.map +1 -1
  10. package/dist/adapters/utils.d.ts +5 -0
  11. package/dist/adapters/utils.d.ts.map +1 -1
  12. package/dist/adapters/utils.js.map +1 -1
  13. package/dist/agent-auth-jwt.d.ts +5 -1
  14. package/dist/agent-auth-jwt.d.ts.map +1 -1
  15. package/dist/agent-auth-jwt.js +72 -20
  16. package/dist/agent-auth-jwt.js.map +1 -1
  17. package/dist/app.d.ts +2 -0
  18. package/dist/app.d.ts.map +1 -1
  19. package/dist/app.js +10 -0
  20. package/dist/app.js.map +1 -1
  21. package/dist/built-ins/agents/reflection-coach/AGENTS.md +47 -0
  22. package/dist/built-ins/agents/reflection-coach/routines/recent-agent-reflection.md +77 -0
  23. package/dist/errors.d.ts +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +2 -2
  26. package/dist/errors.js.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +208 -116
  29. package/dist/index.js.map +1 -1
  30. package/dist/middleware/api-compression.d.ts +8 -0
  31. package/dist/middleware/api-compression.d.ts.map +1 -0
  32. package/dist/middleware/api-compression.js +207 -0
  33. package/dist/middleware/api-compression.js.map +1 -0
  34. package/dist/middleware/auth.d.ts.map +1 -1
  35. package/dist/middleware/auth.js +130 -3
  36. package/dist/middleware/auth.js.map +1 -1
  37. package/dist/middleware/error-handler.d.ts.map +1 -1
  38. package/dist/middleware/error-handler.js +27 -0
  39. package/dist/middleware/error-handler.js.map +1 -1
  40. package/dist/onboarding-assets/default/AGENTS.md +1 -0
  41. package/dist/redaction.d.ts.map +1 -1
  42. package/dist/redaction.js +11 -0
  43. package/dist/redaction.js.map +1 -1
  44. package/dist/routes/access.d.ts.map +1 -1
  45. package/dist/routes/access.js +4 -3
  46. package/dist/routes/access.js.map +1 -1
  47. package/dist/routes/adapters.d.ts.map +1 -1
  48. package/dist/routes/adapters.js +2 -0
  49. package/dist/routes/adapters.js.map +1 -1
  50. package/dist/routes/agents.d.ts.map +1 -1
  51. package/dist/routes/agents.js +222 -87
  52. package/dist/routes/agents.js.map +1 -1
  53. package/dist/routes/attention.d.ts +3 -0
  54. package/dist/routes/attention.d.ts.map +1 -0
  55. package/dist/routes/attention.js +24 -0
  56. package/dist/routes/attention.js.map +1 -0
  57. package/dist/routes/authz.d.ts.map +1 -1
  58. package/dist/routes/authz.js +29 -1
  59. package/dist/routes/authz.js.map +1 -1
  60. package/dist/routes/board-chat.d.ts.map +1 -1
  61. package/dist/routes/board-chat.js +4 -1
  62. package/dist/routes/board-chat.js.map +1 -1
  63. package/dist/routes/built-in-agents.d.ts +3 -0
  64. package/dist/routes/built-in-agents.d.ts.map +1 -0
  65. package/dist/routes/built-in-agents.js +267 -0
  66. package/dist/routes/built-in-agents.js.map +1 -0
  67. package/dist/routes/cases.d.ts +46 -0
  68. package/dist/routes/cases.d.ts.map +1 -0
  69. package/dist/routes/cases.js +1395 -0
  70. package/dist/routes/cases.js.map +1 -0
  71. package/dist/routes/companies.d.ts.map +1 -1
  72. package/dist/routes/companies.js +4 -1
  73. package/dist/routes/companies.js.map +1 -1
  74. package/dist/routes/company-skills.d.ts.map +1 -1
  75. package/dist/routes/company-skills.js +453 -33
  76. package/dist/routes/company-skills.js.map +1 -1
  77. package/dist/routes/environments.d.ts.map +1 -1
  78. package/dist/routes/environments.js +71 -8
  79. package/dist/routes/environments.js.map +1 -1
  80. package/dist/routes/execution-workspaces.d.ts.map +1 -1
  81. package/dist/routes/execution-workspaces.js +89 -3
  82. package/dist/routes/execution-workspaces.js.map +1 -1
  83. package/dist/routes/health.d.ts +2 -0
  84. package/dist/routes/health.d.ts.map +1 -1
  85. package/dist/routes/health.js +30 -0
  86. package/dist/routes/health.js.map +1 -1
  87. package/dist/routes/inbox-dismissals.d.ts.map +1 -1
  88. package/dist/routes/inbox-dismissals.js +80 -19
  89. package/dist/routes/inbox-dismissals.js.map +1 -1
  90. package/dist/routes/index.d.ts +2 -0
  91. package/dist/routes/index.d.ts.map +1 -1
  92. package/dist/routes/index.js +2 -0
  93. package/dist/routes/index.js.map +1 -1
  94. package/dist/routes/issues.d.ts +25 -0
  95. package/dist/routes/issues.d.ts.map +1 -1
  96. package/dist/routes/issues.js +1455 -46
  97. package/dist/routes/issues.js.map +1 -1
  98. package/dist/routes/openapi.d.ts.map +1 -1
  99. package/dist/routes/openapi.js +448 -11
  100. package/dist/routes/openapi.js.map +1 -1
  101. package/dist/routes/pipelines.js +2 -2
  102. package/dist/routes/pipelines.js.map +1 -1
  103. package/dist/routes/resource-memberships.d.ts.map +1 -1
  104. package/dist/routes/resource-memberships.js +13 -5
  105. package/dist/routes/resource-memberships.js.map +1 -1
  106. package/dist/routes/secrets.d.ts.map +1 -1
  107. package/dist/routes/secrets.js +261 -1
  108. package/dist/routes/secrets.js.map +1 -1
  109. package/dist/routes/sidebar-badges.d.ts.map +1 -1
  110. package/dist/routes/sidebar-badges.js +18 -2
  111. package/dist/routes/sidebar-badges.js.map +1 -1
  112. package/dist/routes/user-profiles.d.ts.map +1 -1
  113. package/dist/routes/user-profiles.js +5 -4
  114. package/dist/routes/user-profiles.js.map +1 -1
  115. package/dist/server-info.d.ts +2 -0
  116. package/dist/server-info.d.ts.map +1 -1
  117. package/dist/server-info.js +20 -5
  118. package/dist/server-info.js.map +1 -1
  119. package/dist/services/access.d.ts +4 -4
  120. package/dist/services/access.d.ts.map +1 -1
  121. package/dist/services/activity.d.ts +3 -3
  122. package/dist/services/activity.d.ts.map +1 -1
  123. package/dist/services/activity.js +5 -4
  124. package/dist/services/activity.js.map +1 -1
  125. package/dist/services/adapter-registry-bootstrap.reconcile.test.js +1 -0
  126. package/dist/services/adapter-registry-bootstrap.reconcile.test.js.map +1 -1
  127. package/dist/services/agent-secret-bindings.d.ts +15 -0
  128. package/dist/services/agent-secret-bindings.d.ts.map +1 -1
  129. package/dist/services/agent-secret-bindings.js +43 -0
  130. package/dist/services/agent-secret-bindings.js.map +1 -1
  131. package/dist/services/agents.d.ts +194 -133
  132. package/dist/services/agents.d.ts.map +1 -1
  133. package/dist/services/agents.js +107 -4
  134. package/dist/services/agents.js.map +1 -1
  135. package/dist/services/approvals.d.ts +10 -10
  136. package/dist/services/approvals.d.ts.map +1 -1
  137. package/dist/services/approvals.js +9 -1
  138. package/dist/services/approvals.js.map +1 -1
  139. package/dist/services/assets.d.ts +5 -5
  140. package/dist/services/attention.d.ts +11 -0
  141. package/dist/services/attention.d.ts.map +1 -0
  142. package/dist/services/attention.js +1024 -0
  143. package/dist/services/attention.js.map +1 -0
  144. package/dist/services/authorization.d.ts +15 -2
  145. package/dist/services/authorization.d.ts.map +1 -1
  146. package/dist/services/authorization.js +324 -8
  147. package/dist/services/authorization.js.map +1 -1
  148. package/dist/services/board-auth.d.ts +2 -2
  149. package/dist/services/budgets.d.ts.map +1 -1
  150. package/dist/services/budgets.js +8 -7
  151. package/dist/services/budgets.js.map +1 -1
  152. package/dist/services/built-in-agent-metadata.d.ts +9 -0
  153. package/dist/services/built-in-agent-metadata.d.ts.map +1 -0
  154. package/dist/services/built-in-agent-metadata.js +39 -0
  155. package/dist/services/built-in-agent-metadata.js.map +1 -0
  156. package/dist/services/built-in-agents.d.ts +394 -0
  157. package/dist/services/built-in-agents.d.ts.map +1 -0
  158. package/dist/services/built-in-agents.js +1426 -0
  159. package/dist/services/built-in-agents.js.map +1 -0
  160. package/dist/services/change-consent-gate.d.ts +18 -0
  161. package/dist/services/change-consent-gate.d.ts.map +1 -0
  162. package/dist/services/change-consent-gate.js +170 -0
  163. package/dist/services/change-consent-gate.js.map +1 -0
  164. package/dist/services/companies.d.ts +14 -8
  165. package/dist/services/companies.d.ts.map +1 -1
  166. package/dist/services/companies.js +6 -1
  167. package/dist/services/companies.js.map +1 -1
  168. package/dist/services/company-artifacts.d.ts +4 -1
  169. package/dist/services/company-artifacts.d.ts.map +1 -1
  170. package/dist/services/company-artifacts.js +9 -1
  171. package/dist/services/company-artifacts.js.map +1 -1
  172. package/dist/services/company-member-roles.d.ts.map +1 -1
  173. package/dist/services/company-member-roles.js +2 -0
  174. package/dist/services/company-member-roles.js.map +1 -1
  175. package/dist/services/company-portability.d.ts +1 -1
  176. package/dist/services/company-portability.d.ts.map +1 -1
  177. package/dist/services/company-portability.js +113 -8
  178. package/dist/services/company-portability.js.map +1 -1
  179. package/dist/services/company-search.d.ts.map +1 -1
  180. package/dist/services/company-search.js +843 -294
  181. package/dist/services/company-search.js.map +1 -1
  182. package/dist/services/company-skills.d.ts +64 -3
  183. package/dist/services/company-skills.d.ts.map +1 -1
  184. package/dist/services/company-skills.js +1205 -20
  185. package/dist/services/company-skills.js.map +1 -1
  186. package/dist/services/costs.d.ts +9 -8
  187. package/dist/services/costs.d.ts.map +1 -1
  188. package/dist/services/costs.js +9 -2
  189. package/dist/services/costs.js.map +1 -1
  190. package/dist/services/dashboard.d.ts +2 -0
  191. package/dist/services/dashboard.d.ts.map +1 -1
  192. package/dist/services/dashboard.js +60 -17
  193. package/dist/services/dashboard.js.map +1 -1
  194. package/dist/services/database-backup-health.d.ts +34 -0
  195. package/dist/services/database-backup-health.d.ts.map +1 -0
  196. package/dist/services/database-backup-health.js +104 -0
  197. package/dist/services/database-backup-health.js.map +1 -0
  198. package/dist/services/document-annotations.d.ts +169 -1
  199. package/dist/services/document-annotations.d.ts.map +1 -1
  200. package/dist/services/document-annotations.js +272 -1
  201. package/dist/services/document-annotations.js.map +1 -1
  202. package/dist/services/documents.d.ts +368 -0
  203. package/dist/services/documents.d.ts.map +1 -1
  204. package/dist/services/documents.js +2 -2
  205. package/dist/services/documents.js.map +1 -1
  206. package/dist/services/environment-config.d.ts.map +1 -1
  207. package/dist/services/environment-config.js +6 -0
  208. package/dist/services/environment-config.js.map +1 -1
  209. package/dist/services/environment-custom-image-runtime.d.ts +28 -0
  210. package/dist/services/environment-custom-image-runtime.d.ts.map +1 -1
  211. package/dist/services/environment-custom-image-runtime.js +106 -7
  212. package/dist/services/environment-custom-image-runtime.js.map +1 -1
  213. package/dist/services/environment-custom-images.d.ts +31 -1
  214. package/dist/services/environment-custom-images.d.ts.map +1 -1
  215. package/dist/services/environment-custom-images.js +110 -4
  216. package/dist/services/environment-custom-images.js.map +1 -1
  217. package/dist/services/environment-execution-target.d.ts.map +1 -1
  218. package/dist/services/environment-execution-target.js +1 -3
  219. package/dist/services/environment-execution-target.js.map +1 -1
  220. package/dist/services/environment-probe.d.ts +1 -0
  221. package/dist/services/environment-probe.d.ts.map +1 -1
  222. package/dist/services/environment-probe.js +99 -5
  223. package/dist/services/environment-probe.js.map +1 -1
  224. package/dist/services/environments.d.ts +3 -1
  225. package/dist/services/environments.d.ts.map +1 -1
  226. package/dist/services/environments.js +98 -3
  227. package/dist/services/environments.js.map +1 -1
  228. package/dist/services/execution-workspace-policy.d.ts +29 -3
  229. package/dist/services/execution-workspace-policy.d.ts.map +1 -1
  230. package/dist/services/execution-workspace-policy.js +44 -1
  231. package/dist/services/execution-workspace-policy.js.map +1 -1
  232. package/dist/services/execution-workspaces.d.ts +64 -1
  233. package/dist/services/execution-workspaces.d.ts.map +1 -1
  234. package/dist/services/execution-workspaces.js +634 -3
  235. package/dist/services/execution-workspaces.js.map +1 -1
  236. package/dist/services/external-objects.d.ts +26 -26
  237. package/dist/services/feedback.d.ts +2 -2
  238. package/dist/services/finance.d.ts +5 -5
  239. package/dist/services/goals.d.ts +8 -8
  240. package/dist/services/heartbeat-stop-metadata.d.ts +2 -2
  241. package/dist/services/heartbeat-stop-metadata.d.ts.map +1 -1
  242. package/dist/services/heartbeat-stop-metadata.js +4 -0
  243. package/dist/services/heartbeat-stop-metadata.js.map +1 -1
  244. package/dist/services/heartbeat-stop-metadata.test.js +9 -0
  245. package/dist/services/heartbeat-stop-metadata.test.js.map +1 -1
  246. package/dist/services/heartbeat.d.ts +133 -28
  247. package/dist/services/heartbeat.d.ts.map +1 -1
  248. package/dist/services/heartbeat.js +1963 -239
  249. package/dist/services/heartbeat.js.map +1 -1
  250. package/dist/services/inbox-dismissals.d.ts +26 -0
  251. package/dist/services/inbox-dismissals.d.ts.map +1 -1
  252. package/dist/services/inbox-dismissals.js +33 -18
  253. package/dist/services/inbox-dismissals.js.map +1 -1
  254. package/dist/services/index.d.ts +3 -1
  255. package/dist/services/index.d.ts.map +1 -1
  256. package/dist/services/index.js +3 -1
  257. package/dist/services/index.js.map +1 -1
  258. package/dist/services/instance-settings.d.ts +25 -1
  259. package/dist/services/instance-settings.d.ts.map +1 -1
  260. package/dist/services/instance-settings.js +103 -5
  261. package/dist/services/instance-settings.js.map +1 -1
  262. package/dist/services/issue-continuation-summary.js +1 -1
  263. package/dist/services/issue-continuation-summary.js.map +1 -1
  264. package/dist/services/issue-recovery-actions.d.ts +3 -0
  265. package/dist/services/issue-recovery-actions.d.ts.map +1 -1
  266. package/dist/services/issue-recovery-actions.js +9 -0
  267. package/dist/services/issue-recovery-actions.js.map +1 -1
  268. package/dist/services/issue-rewake-throttle.d.ts +92 -0
  269. package/dist/services/issue-rewake-throttle.d.ts.map +1 -0
  270. package/dist/services/issue-rewake-throttle.js +139 -0
  271. package/dist/services/issue-rewake-throttle.js.map +1 -0
  272. package/dist/services/issue-thread-interactions.d.ts +8 -1
  273. package/dist/services/issue-thread-interactions.d.ts.map +1 -1
  274. package/dist/services/issue-thread-interactions.js +231 -16
  275. package/dist/services/issue-thread-interactions.js.map +1 -1
  276. package/dist/services/issue-visibility.d.ts +4 -0
  277. package/dist/services/issue-visibility.d.ts.map +1 -0
  278. package/dist/services/issue-visibility.js +9 -0
  279. package/dist/services/issue-visibility.js.map +1 -0
  280. package/dist/services/issues.d.ts +225 -105
  281. package/dist/services/issues.d.ts.map +1 -1
  282. package/dist/services/issues.js +540 -18
  283. package/dist/services/issues.js.map +1 -1
  284. package/dist/services/local-service-supervisor.d.ts +3 -0
  285. package/dist/services/local-service-supervisor.d.ts.map +1 -1
  286. package/dist/services/local-service-supervisor.js +51 -0
  287. package/dist/services/local-service-supervisor.js.map +1 -1
  288. package/dist/services/pipeline-case-outputs.d.ts.map +1 -1
  289. package/dist/services/pipeline-case-outputs.js +2 -1
  290. package/dist/services/pipeline-case-outputs.js.map +1 -1
  291. package/dist/services/pipelines-aggregation.d.ts.map +1 -1
  292. package/dist/services/pipelines-aggregation.js +3 -2
  293. package/dist/services/pipelines-aggregation.js.map +1 -1
  294. package/dist/services/pipelines.d.ts +165 -165
  295. package/dist/services/pipelines.d.ts.map +1 -1
  296. package/dist/services/pipelines.js +6 -4
  297. package/dist/services/pipelines.js.map +1 -1
  298. package/dist/services/plugin-database.d.ts +2 -2
  299. package/dist/services/plugin-environment-driver.d.ts +1 -1
  300. package/dist/services/plugin-host-services.d.ts.map +1 -1
  301. package/dist/services/plugin-host-services.js +3 -1
  302. package/dist/services/plugin-host-services.js.map +1 -1
  303. package/dist/services/plugin-managed-routines.d.ts +1 -0
  304. package/dist/services/plugin-managed-routines.d.ts.map +1 -1
  305. package/dist/services/plugin-registry.d.ts +21 -21
  306. package/dist/services/productivity-review.d.ts +1 -0
  307. package/dist/services/productivity-review.d.ts.map +1 -1
  308. package/dist/services/productivity-review.js +6 -5
  309. package/dist/services/productivity-review.js.map +1 -1
  310. package/dist/services/projects.d.ts +9 -9
  311. package/dist/services/recovery/service.d.ts +82 -54
  312. package/dist/services/recovery/service.d.ts.map +1 -1
  313. package/dist/services/recovery/service.js +200 -35
  314. package/dist/services/recovery/service.js.map +1 -1
  315. package/dist/services/recovery/successful-run-handoff.d.ts +1 -0
  316. package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
  317. package/dist/services/recovery/successful-run-handoff.js +2 -0
  318. package/dist/services/recovery/successful-run-handoff.js.map +1 -1
  319. package/dist/services/recovery/successful-run-handoff.test.js +20 -0
  320. package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
  321. package/dist/services/resource-memberships.d.ts +13 -10
  322. package/dist/services/resource-memberships.d.ts.map +1 -1
  323. package/dist/services/resource-memberships.js +89 -20
  324. package/dist/services/resource-memberships.js.map +1 -1
  325. package/dist/services/responsible-user-denial-run-outcomes.d.ts +60 -0
  326. package/dist/services/responsible-user-denial-run-outcomes.d.ts.map +1 -0
  327. package/dist/services/responsible-user-denial-run-outcomes.js +56 -0
  328. package/dist/services/responsible-user-denial-run-outcomes.js.map +1 -0
  329. package/dist/services/responsible-user-denial-run-outcomes.test.d.ts +2 -0
  330. package/dist/services/responsible-user-denial-run-outcomes.test.d.ts.map +1 -0
  331. package/dist/services/responsible-user-denial-run-outcomes.test.js +75 -0
  332. package/dist/services/responsible-user-denial-run-outcomes.test.js.map +1 -0
  333. package/dist/services/routines.d.ts +30 -8
  334. package/dist/services/routines.d.ts.map +1 -1
  335. package/dist/services/routines.js +241 -18
  336. package/dist/services/routines.js.map +1 -1
  337. package/dist/services/run-liveness.d.ts.map +1 -1
  338. package/dist/services/run-liveness.js +21 -0
  339. package/dist/services/run-liveness.js.map +1 -1
  340. package/dist/services/run-log-store.d.ts +1 -0
  341. package/dist/services/run-log-store.d.ts.map +1 -1
  342. package/dist/services/run-log-store.js +4 -0
  343. package/dist/services/run-log-store.js.map +1 -1
  344. package/dist/services/run-scratch.d.ts +42 -0
  345. package/dist/services/run-scratch.d.ts.map +1 -0
  346. package/dist/services/run-scratch.js +111 -0
  347. package/dist/services/run-scratch.js.map +1 -0
  348. package/dist/services/run-scratch.test.d.ts +2 -0
  349. package/dist/services/run-scratch.test.d.ts.map +1 -0
  350. package/dist/services/run-scratch.test.js +98 -0
  351. package/dist/services/run-scratch.test.js.map +1 -0
  352. package/dist/services/secrets.d.ts +1261 -64
  353. package/dist/services/secrets.d.ts.map +1 -1
  354. package/dist/services/secrets.js +1089 -96
  355. package/dist/services/secrets.js.map +1 -1
  356. package/dist/services/task-watchdogs.d.ts +1 -0
  357. package/dist/services/task-watchdogs.d.ts.map +1 -1
  358. package/dist/services/task-watchdogs.js +78 -9
  359. package/dist/services/task-watchdogs.js.map +1 -1
  360. package/dist/services/work-timeline.d.ts.map +1 -1
  361. package/dist/services/work-timeline.js +34 -2
  362. package/dist/services/work-timeline.js.map +1 -1
  363. package/dist/services/workspace-runtime-read-model.d.ts +30 -29
  364. package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
  365. package/dist/services/workspace-runtime-read-model.js.map +1 -1
  366. package/dist/services/workspace-runtime.d.ts +59 -13
  367. package/dist/services/workspace-runtime.d.ts.map +1 -1
  368. package/dist/services/workspace-runtime.js +1121 -102
  369. package/dist/services/workspace-runtime.js.map +1 -1
  370. package/dist/skills-catalog/catalog/bundled/paperclip-operations/reflection-coach/SKILL.md +202 -0
  371. package/dist/skills-catalog/catalog/bundled/product/paperclip-capsules/SKILL.md +1 -1
  372. package/dist/skills-catalog/catalog/bundled/product/wireframe/SKILL.md +1 -1
  373. package/dist/skills-catalog/catalog/optional/content/release-announcement/SKILL.md +90 -0
  374. package/dist/skills-catalog/catalog/optional/finance/ramp/SKILL.md +98 -0
  375. package/dist/skills-catalog/generated/catalog.json +84 -12
  376. package/dist/ui-branding.d.ts +7 -0
  377. package/dist/ui-branding.d.ts.map +1 -1
  378. package/dist/ui-branding.js +8 -2
  379. package/dist/ui-branding.js.map +1 -1
  380. package/dist/version.d.ts +14 -0
  381. package/dist/version.d.ts.map +1 -1
  382. package/dist/version.js +114 -3
  383. package/dist/version.js.map +1 -1
  384. package/package.json +19 -20
  385. package/skills/paperclip/SKILL.md +45 -10
  386. package/skills/paperclip/references/api-reference.md +246 -1
  387. package/skills/paperclip/references/cases.md +295 -0
  388. package/skills/paperclip/references/company-skills.md +1 -1
  389. package/skills/paperclip-board/SKILL.md +3 -4
  390. package/skills/paperclip-converting-plans-to-tasks/SKILL.md +3 -7
  391. package/skills/para-memory-files/SKILL.md +3 -7
  392. package/ui-dist/assets/{abnfDiagram-VRR7QNED-BsFz2IiG.js → abnfDiagram-VRR7QNED-Dtgfy1PK.js} +1 -1
  393. package/ui-dist/assets/{arc-HZhAwbOs.js → arc-BGdvNvs8.js} +1 -1
  394. package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-BteMjuOn.js → architectureDiagram-ZJ3FMSHR-CK19SQDx.js} +1 -1
  395. package/ui-dist/assets/{blockDiagram-677ZJIJ3-Iq66uZjf.js → blockDiagram-677ZJIJ3-Kh4N3XG4.js} +1 -1
  396. package/ui-dist/assets/{browser-ponyfill-BBsuyZJt.js → browser-ponyfill-B3hXtEFW.js} +1 -1
  397. package/ui-dist/assets/{c4Diagram-LMCZKHZV-_nlglj7I.js → c4Diagram-LMCZKHZV-CBHpGrgU.js} +1 -1
  398. package/ui-dist/assets/channel-4fjB8e8N.js +1 -0
  399. package/ui-dist/assets/{chunk-2Q5K7J3B-Cvz7WwSQ.js → chunk-2Q5K7J3B-BdHbqi7k.js} +1 -1
  400. package/ui-dist/assets/{chunk-32BRIVSS-LLubniCS.js → chunk-32BRIVSS-DxtzZzpt.js} +1 -1
  401. package/ui-dist/assets/{chunk-5VM5RSS4-BOst5hcr.js → chunk-5VM5RSS4-DMKxBbgu.js} +1 -1
  402. package/ui-dist/assets/{chunk-EX3LRPZG-D5WNShVm.js → chunk-EX3LRPZG-BaIOPi4v.js} +1 -1
  403. package/ui-dist/assets/{chunk-JWPE2WC7-CA07x4av.js → chunk-JWPE2WC7-DGo3MPmr.js} +1 -1
  404. package/ui-dist/assets/{chunk-MOJQB5TN-Clfi83rK.js → chunk-MOJQB5TN-BEhQg0uD.js} +1 -1
  405. package/ui-dist/assets/{chunk-RYQCIY6F-BhZ6p7YJ.js → chunk-RYQCIY6F-HPJ6zRpJ.js} +1 -1
  406. package/ui-dist/assets/{chunk-V7JOEXUC-CdexJGoV.js → chunk-V7JOEXUC-Bl_6cAJD.js} +1 -1
  407. package/ui-dist/assets/{chunk-VR4S4FIN-8vjmBhr1.js → chunk-VR4S4FIN-KMjgGCsw.js} +1 -1
  408. package/ui-dist/assets/{chunk-XXDRQBXY-CqmCaplj.js → chunk-XXDRQBXY-oWGAIKjr.js} +1 -1
  409. package/ui-dist/assets/classDiagram-OUVF2IWQ-CTXxO5l9.js +1 -0
  410. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CTXxO5l9.js +1 -0
  411. package/ui-dist/assets/{cose-bilkent-JH36ORCC-CY-zAygY.js → cose-bilkent-JH36ORCC-Ckqse1pH.js} +1 -1
  412. package/ui-dist/assets/{cynefin-VYW2F7L2-BQSQ1HKr.js → cynefin-VYW2F7L2-DfZ8Hzqi.js} +1 -1
  413. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-NJCuFrZL.js → cynefinDiagram-TSTJHNR4-BkDQo9oi.js} +1 -1
  414. package/ui-dist/assets/{dagre-VKFMJZFB-DAUPIc8g.js → dagre-VKFMJZFB-ClghXQ6E.js} +1 -1
  415. package/ui-dist/assets/{diagram-FQU43EPY-DvMwJlCi.js → diagram-FQU43EPY-D5a0CwpV.js} +1 -1
  416. package/ui-dist/assets/{diagram-G47NLZAW-DPY0Jz_y.js → diagram-G47NLZAW-hVjYR_xl.js} +1 -1
  417. package/ui-dist/assets/{diagram-NH7WQ7WH-BpopeEhp.js → diagram-NH7WQ7WH-BlDPgD5Z.js} +1 -1
  418. package/ui-dist/assets/{diagram-OA4YK3LP-Qpu05z9X.js → diagram-OA4YK3LP-D0iZKIS3.js} +1 -1
  419. package/ui-dist/assets/{diagram-WEI45ONY-DHnvw9XU.js → diagram-WEI45ONY-m6hrfD0J.js} +1 -1
  420. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-D61S54_7.js → ebnfDiagram-CCIWWBDH-D4SQYLod.js} +1 -1
  421. package/ui-dist/assets/{erDiagram-Q63AITRT-pplAN_x2.js → erDiagram-Q63AITRT-Byzb6-I5.js} +1 -1
  422. package/ui-dist/assets/{flowDiagram-23GEKE2U-BaBcbyDq.js → flowDiagram-23GEKE2U-CaTUkFO6.js} +1 -1
  423. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-BOJSII2b.js → ganttDiagram-NO4QXBWP-DmdMQ3ES.js} +1 -1
  424. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-aYAaO4a2.js → gitGraphDiagram-IHSO6WYX-Bt-1g2CS.js} +1 -1
  425. package/ui-dist/assets/{index-CZxYajxF.js → index-2Dk9yO0W.js} +1 -1
  426. package/ui-dist/assets/{index-B8H0SHw1.js → index-8jLpgFNU.js} +1 -1
  427. package/ui-dist/assets/{index-3EKATXgc.js → index-AP_iimxs.js} +1 -1
  428. package/ui-dist/assets/{index-C2JgdjPa.js → index-BNJasQY9.js} +1 -1
  429. package/ui-dist/assets/{index-BaiskB3o.js → index-BO-aNmyX.js} +1 -1
  430. package/ui-dist/assets/{index-B5YenxAQ.js → index-BPCzCn1Y.js} +1 -1
  431. package/ui-dist/assets/{index-Da2kqT1M.js → index-BSLnLTb2.js} +1 -1
  432. package/ui-dist/assets/{index-By0oeEvz.js → index-CCZ2qgHN.js} +1 -1
  433. package/ui-dist/assets/{index-bUFCSEgv.js → index-CLZXR1k9.js} +1 -1
  434. package/ui-dist/assets/{index-CJlw4l9L.js → index-CTNfrE1s.js} +1 -1
  435. package/ui-dist/assets/{index-TmpY6Xh_.js → index-ChNd25s7.js} +1 -1
  436. package/ui-dist/assets/{index-BRgdHUNR.js → index-CjHiMOEO.js} +1 -1
  437. package/ui-dist/assets/{index-DrL6pHSJ.js → index-CmeJQpXZ.js} +1 -1
  438. package/ui-dist/assets/index-CyuYtn7U.css +1 -0
  439. package/ui-dist/assets/{index-CKKytnOd.js → index-CzZjM8eh.js} +1 -1
  440. package/ui-dist/assets/{index-Dceamza_.js → index-D-6T0Aft.js} +1 -1
  441. package/ui-dist/assets/{index-CjDqB1SX.js → index-DG_7V1pG.js} +1 -1
  442. package/ui-dist/assets/index-DIKcgxCt.js +676 -0
  443. package/ui-dist/assets/{index-BKVeJ3Jh.js → index-DiUerHmi.js} +1 -1
  444. package/ui-dist/assets/{index-C-nKjFZh.js → index-NZ7v5Fc2.js} +1 -1
  445. package/ui-dist/assets/{index-CvPPuNs1.js → index-Py70EvBg.js} +1 -1
  446. package/ui-dist/assets/{index-LHkZdu0h.js → index-R3aYn0qM.js} +1 -1
  447. package/ui-dist/assets/{index-BT8b79Bd.js → index-YXpm7gW-.js} +1 -1
  448. package/ui-dist/assets/{index-CemenVOf.js → index-n4q3gzki.js} +1 -1
  449. package/ui-dist/assets/{index-D3ODiUup.js → index-vlZ3GoYY.js} +1 -1
  450. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-C7_90qGq.js → infoDiagram-FWYZ7A6U-jdi5ciVg.js} +1 -1
  451. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-T-nPPdTt.js → ishikawaDiagram-FXEZZL3T-CMz5VvYp.js} +1 -1
  452. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-C3hqQN0R.js → journeyDiagram-5HDEW3XC-DK6dk2R-.js} +1 -1
  453. package/ui-dist/assets/{kanban-definition-HUTT4EX6-CPoD9DUm.js → kanban-definition-HUTT4EX6-DX5cZL-D.js} +1 -1
  454. package/ui-dist/assets/{linear-D-S8yr3Z.js → linear-CI96ABKp.js} +1 -1
  455. package/ui-dist/assets/{mermaid.core-DhvH8shK.js → mermaid.core-DnsEVfbd.js} +4 -4
  456. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-CxCIKrKo.js → mindmap-definition-LN4V7U3C-BdDCY7hf.js} +1 -1
  457. package/ui-dist/assets/{pegDiagram-2B236MQR-D4G-PC6N.js → pegDiagram-2B236MQR-DuY4TF-f.js} +1 -1
  458. package/ui-dist/assets/{pieDiagram-ENE6RG2P-N4X8UvqY.js → pieDiagram-ENE6RG2P-bgFw7dm3.js} +1 -1
  459. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CZohoDln.js → quadrantDiagram-ABIIQ3AL-BCZVWyWS.js} +1 -1
  460. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-fMGQVhwF.js → railroadDiagram-RFXS5EU6-D9V5_Cgc.js} +1 -1
  461. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-CK0IuUmk.js → requirementDiagram-TGXJPOKE-CItsya2r.js} +1 -1
  462. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-pSfb3CqG.js → sankeyDiagram-HTMAVEWB-Cz9IHsZO.js} +1 -1
  463. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CbhkqIeq.js → sequenceDiagram-DBY2YBRQ-hMSqTiwK.js} +1 -1
  464. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-B6vqqPx-.js → sizeCapture-X5ZJPWSS-0g9M5wMf.js} +1 -1
  465. package/ui-dist/assets/{stateDiagram-2N3HPSRC-DIJf9h5C.js → stateDiagram-2N3HPSRC-F8_YhgWl.js} +1 -1
  466. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-fZImUBtq.js +1 -0
  467. package/ui-dist/assets/{swimlanes-5IMT3BWC-DIX_LV8O.js → swimlanes-5IMT3BWC-CCZqPbpE.js} +2 -2
  468. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-9C1UT51R.js +8 -0
  469. package/ui-dist/assets/{timeline-definition-FHXFAJF6-CD-Pxb_g.js → timeline-definition-FHXFAJF6-BeUIr18x.js} +1 -1
  470. package/ui-dist/assets/{vennDiagram-L72KCM5P-BAZsWL0R.js → vennDiagram-L72KCM5P-D3d5h_G4.js} +1 -1
  471. package/ui-dist/assets/{wardleyDiagram-EHGQE667-CnNyfwX5.js → wardleyDiagram-EHGQE667-Bzt-As9A.js} +1 -1
  472. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-j9YgsD3Y.js → xychartDiagram-FW5EYKEG-C2mly0xf.js} +1 -1
  473. package/ui-dist/fonts/InterVariable-Italic.woff2 +0 -0
  474. package/ui-dist/fonts/InterVariable.woff2 +0 -0
  475. package/ui-dist/fonts/NOTICE.md +17 -0
  476. package/ui-dist/index.html +2 -2
  477. package/ui-dist/locales/en/common.json +1142 -5
  478. package/ui-dist/locales/zh-CN/common.json +1140 -3
  479. package/ui-dist/assets/channel-CsiI2LYw.js +0 -1
  480. package/ui-dist/assets/classDiagram-OUVF2IWQ-CBn1Jqo8.js +0 -1
  481. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CBn1Jqo8.js +0 -1
  482. package/ui-dist/assets/index-BsL8K1vL.css +0 -1
  483. package/ui-dist/assets/index-D__fTsb2.js +0 -643
  484. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DpwLD7YL.js +0 -1
  485. 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 normalizeCategorySlug(value) {
1077
+ function normalizeCategoryName(value) {
1003
1078
  if (typeof value !== "string")
1004
1079
  return null;
1005
- return normalizeSkillSlug(value);
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
- return Array.from(new Set(values.map(normalizeCategorySlug).filter((value) => Boolean(value))));
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 = JSON.stringify(metadata ?? {}) !== JSON.stringify(skill.metadata ?? {});
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 (JSON.stringify(metadata) !== JSON.stringify(skill.metadata ?? {})) {
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 ?? []).map(normalizeCategorySlug).filter((value) => Boolean(value)));
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 getById(companyId, id);
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
- return enrichSkill(skill, usedByAgents.length, usedByAgents, await getCurrentVersion(skill), await isStarredByActor(companyId, id, actor));
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: `company/${companyId}/${forkSlug}`,
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
- return getById(companyId, forked.id).then((skill) => {
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,