@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
@@ -5,13 +5,14 @@ import net from "node:net";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import path from "node:path";
7
7
  import { setTimeout as delay } from "node:timers/promises";
8
- import { executionWorkspaces, projectWorkspaces, workspaceRuntimeServices } from "@penclipai/db";
8
+ import { executionWorkspaces, issueComments, issues, projectWorkspaces, workspaceRuntimeServices } from "@penclipai/db";
9
9
  import { listWorkspaceServiceCommandDefinitions, } from "@penclipai/shared";
10
- import { and, desc, eq, inArray } from "drizzle-orm";
10
+ import { and, desc, eq, inArray, ne } from "drizzle-orm";
11
11
  import { asNumber, asString, parseObject, renderTemplate } from "../adapters/utils.js";
12
12
  import { resolveHomeAwarePath } from "../home-paths.js";
13
- import { createLocalServiceKey, findLocalServiceRegistryRecordByRuntimeServiceId, findAdoptableLocalService, readLocalServicePortOwner, removeLocalServiceRegistryRecord, terminateLocalService, touchLocalServiceRegistryRecord, writeLocalServiceRegistryRecord, } from "./local-service-supervisor.js";
14
- import { readExecutionWorkspaceConfig } from "./execution-workspaces.js";
13
+ import { createLocalServiceKey, findLocalServiceRegistryRecordByRuntimeServiceId, findAdoptableLocalService, isLocalServiceProcessInWorkspace, readLocalServiceProcessCwd, readLocalServicePortOwner, removeLocalServiceRegistryRecord, terminateLocalService, touchLocalServiceRegistryRecord, writeLocalServiceRegistryRecord, } from "./local-service-supervisor.js";
14
+ import { executionWorkspaceService, readExecutionWorkspaceConfig } from "./execution-workspaces.js";
15
+ import { logActivity } from "./activity-log.js";
15
16
  import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js";
16
17
  export function resolveShell() {
17
18
  const windowsGitBash = "C:\\Program Files\\Git\\bin\\bash.exe";
@@ -466,6 +467,150 @@ const GIT_WORKTREE_BRANCH_INCOHERENCE_REASON = "git_worktree_branch_incoherence"
466
467
  function formatBranchForMessage(branch) {
467
468
  return branch && branch.length > 0 ? branch : "<detached>";
468
469
  }
470
+ const GIT_IN_PROGRESS_OPERATION_MARKERS = [
471
+ { operation: "rebase", marker: "rebase-merge" },
472
+ { operation: "rebase", marker: "rebase-apply" },
473
+ { operation: "merge", marker: "MERGE_HEAD" },
474
+ { operation: "cherry_pick", marker: "CHERRY_PICK_HEAD" },
475
+ { operation: "revert", marker: "REVERT_HEAD" },
476
+ { operation: "bisect", marker: "BISECT_LOG" },
477
+ ];
478
+ const GIT_IN_PROGRESS_OPERATION_LABELS = {
479
+ rebase: "rebase",
480
+ merge: "merge",
481
+ cherry_pick: "cherry-pick",
482
+ revert: "revert",
483
+ bisect: "bisect",
484
+ };
485
+ // `--quit` clears the interrupted operation's state directory without touching
486
+ // the working tree or moving HEAD, unlike `--abort` which resets both.
487
+ const GIT_IN_PROGRESS_OPERATION_QUIT_ARGS = {
488
+ rebase: ["rebase", "--quit"],
489
+ merge: ["merge", "--quit"],
490
+ cherry_pick: ["cherry-pick", "--quit"],
491
+ revert: ["revert", "--quit"],
492
+ bisect: ["bisect", "reset", "HEAD"],
493
+ };
494
+ async function detectGitWorktreeInProgressOperation(worktreePath) {
495
+ for (const { operation, marker } of GIT_IN_PROGRESS_OPERATION_MARKERS) {
496
+ const markerPath = await runGit(["rev-parse", "--git-path", marker], worktreePath).catch(() => null);
497
+ if (!markerPath)
498
+ continue;
499
+ if (existsSync(path.resolve(worktreePath, markerPath)))
500
+ return operation;
501
+ }
502
+ return null;
503
+ }
504
+ const DIRTY_PATH_SAMPLE_LIMIT = 5;
505
+ function parseGitPorcelainPath(line) {
506
+ const raw = line.trimEnd();
507
+ if (raw.trim().length <= 3)
508
+ return raw.trim();
509
+ if (raw[1] === " " && raw[2] !== " ")
510
+ return raw.slice(2).trim();
511
+ return raw.slice(3).trim();
512
+ }
513
+ function sampleDirtyStatusPaths(statusLines) {
514
+ return (statusLines ?? [])
515
+ .map(parseGitPorcelainPath)
516
+ .filter((value) => value.length > 0)
517
+ .slice(0, DIRTY_PATH_SAMPLE_LIMIT);
518
+ }
519
+ function formatUtcBranchTimestamp(date = new Date()) {
520
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
521
+ }
522
+ function buildDirtyQuarantineRescueBranch(sourceIssue) {
523
+ const issueComponent = sanitizeBranchName(sourceIssue?.identifier ?? sourceIssue?.id ?? "issue");
524
+ return sanitizeBranchName(`paperclip/rescue/${issueComponent}/${formatUtcBranchTimestamp()}`);
525
+ }
526
+ function formatIssueReference(issueId, identifier) {
527
+ if (!identifier)
528
+ return issueId ? `\`${issueId}\`` : "`unknown`";
529
+ const match = identifier.match(/^([A-Z]+)-\d+$/);
530
+ if (!match)
531
+ return `\`${identifier}\``;
532
+ return `[${identifier}](/${match[1]}/issues/${identifier})`;
533
+ }
534
+ async function readIssueCompanyId(db, issueId) {
535
+ if (!issueId)
536
+ return null;
537
+ return db
538
+ .select({ companyId: issues.companyId })
539
+ .from(issues)
540
+ .where(eq(issues.id, issueId))
541
+ .then((rows) => rows[0]?.companyId ?? null);
542
+ }
543
+ async function findGitWorktreeBranchContention(input) {
544
+ if (!input.db)
545
+ return null;
546
+ const companyId = await readIssueCompanyId(input.db, input.sourceIssue?.id);
547
+ if (!companyId)
548
+ return null;
549
+ return executionWorkspaceService(input.db).findGitWorktreeContention({
550
+ companyId,
551
+ worktreePath: input.worktreePath,
552
+ liveBranchName: input.actualBranchName,
553
+ excludingExecutionWorkspaceId: input.executionWorkspaceId,
554
+ });
555
+ }
556
+ function executionWorkspaceUsesInheritedProjectRuntimeServices(row) {
557
+ if (row.mode !== "shared_workspace" || !row.projectWorkspaceId)
558
+ return false;
559
+ return !readExecutionWorkspaceConfig(row.metadata ?? null)?.workspaceRuntime;
560
+ }
561
+ async function findActiveRuntimeServiceBlockingDirtyQuarantine(input) {
562
+ const inheritedProjectWorkspaceId = executionWorkspaceUsesInheritedProjectRuntimeServices(input.workspace)
563
+ ? input.workspace.projectWorkspaceId
564
+ : null;
565
+ const serviceScopeCondition = inheritedProjectWorkspaceId
566
+ ? and(eq(workspaceRuntimeServices.companyId, input.workspace.companyId), eq(workspaceRuntimeServices.projectWorkspaceId, inheritedProjectWorkspaceId), eq(workspaceRuntimeServices.scopeType, "project_workspace"))
567
+ : and(eq(workspaceRuntimeServices.companyId, input.workspace.companyId), eq(workspaceRuntimeServices.executionWorkspaceId, input.workspace.id));
568
+ const [service] = await input.db
569
+ .select({
570
+ id: workspaceRuntimeServices.id,
571
+ serviceName: workspaceRuntimeServices.serviceName,
572
+ status: workspaceRuntimeServices.status,
573
+ scopeType: workspaceRuntimeServices.scopeType,
574
+ })
575
+ .from(workspaceRuntimeServices)
576
+ .where(and(serviceScopeCondition, ne(workspaceRuntimeServices.status, "stopped")))
577
+ .orderBy(desc(workspaceRuntimeServices.updatedAt), desc(workspaceRuntimeServices.createdAt))
578
+ .limit(1);
579
+ return service ?? null;
580
+ }
581
+ async function assertDirtyQuarantineRuntimeServicesStopped(input) {
582
+ if (!input.executionWorkspaceId) {
583
+ input.evidence.safeRepair.eligible = false;
584
+ input.evidence.safeRepair.reason = "dirty quarantine repair requires an execution workspace id for runtime-service checks";
585
+ throw branchIncoherenceValidationFailure(input.evidence);
586
+ }
587
+ const [workspace] = await input.db
588
+ .select()
589
+ .from(executionWorkspaces)
590
+ .where(eq(executionWorkspaces.id, input.executionWorkspaceId));
591
+ if (!workspace) {
592
+ input.evidence.safeRepair.eligible = false;
593
+ input.evidence.safeRepair.reason = "dirty quarantine repair requires a persisted execution workspace for runtime-service checks";
594
+ throw branchIncoherenceValidationFailure(input.evidence);
595
+ }
596
+ const activeService = await findActiveRuntimeServiceBlockingDirtyQuarantine({
597
+ db: input.db,
598
+ workspace,
599
+ });
600
+ if (!activeService)
601
+ return;
602
+ input.evidence.safeRepair.eligible = false;
603
+ input.evidence.safeRepair.reason =
604
+ `dirty quarantine repair requires runtime service "${activeService.serviceName}" (${activeService.id}) to be stopped; current status is ${activeService.status}`;
605
+ throw branchIncoherenceValidationFailure(input.evidence);
606
+ }
607
+ async function assertGitIndexIsUnlocked(worktreePath) {
608
+ const indexLockPath = await runGit(["rev-parse", "--git-path", "index.lock"], worktreePath)
609
+ .catch(() => null);
610
+ if (indexLockPath && existsSync(indexLockPath)) {
611
+ throw new Error(`git index lock exists at ${indexLockPath}`);
612
+ }
613
+ }
469
614
  function fingerprintWorkspaceBranchIncoherence(input) {
470
615
  const digest = createHash("sha256")
471
616
  .update(stableStringify({
@@ -483,12 +628,46 @@ function fingerprintWorkspaceBranchIncoherence(input) {
483
628
  .digest("hex");
484
629
  return `workspace_incoherence:v1:sha256:${digest}`;
485
630
  }
631
+ async function getGitWorktreeBranchAncestryVerdict(input) {
632
+ if (!input.expectedHeadSha || !input.actualHeadSha)
633
+ return "unknown";
634
+ const proc = await executeProcess({
635
+ command: "git",
636
+ args: ["merge-base", "--is-ancestor", input.expectedHeadSha, input.actualHeadSha],
637
+ cwd: input.repoRoot,
638
+ }).catch(() => null);
639
+ if (!proc)
640
+ return "unknown";
641
+ if (proc.code === 0)
642
+ return "ancestor";
643
+ if (proc.code === 1)
644
+ return "diverged";
645
+ return "unknown";
646
+ }
647
+ function explainGitWorktreeBranchIncoherence(input) {
648
+ const actualBranch = formatBranchForMessage(input.actualBranchName);
649
+ if (!input.expectedHeadSha || !input.actualHeadSha) {
650
+ return `Paperclip could not determine branch ancestry because the recorded branch "${input.expectedBranchName}" or checked-out branch "${actualBranch}" is missing a resolvable HEAD commit.`;
651
+ }
652
+ if (input.sameHead) {
653
+ return `The recorded branch "${input.expectedBranchName}" and checked-out branch "${actualBranch}" resolve to the same commit, so the mismatch is branch metadata rather than commit divergence.`;
654
+ }
655
+ if (input.ancestryVerdict === "ancestor") {
656
+ return `The recorded branch "${input.expectedBranchName}" is an ancestor of the checked-out branch "${actualBranch}", so the checked-out branch is forward of the recorded branch.`;
657
+ }
658
+ if (input.ancestryVerdict === "diverged") {
659
+ return `The recorded branch "${input.expectedBranchName}" is not an ancestor of the checked-out branch "${actualBranch}", so Paperclip cannot prove a forward-only reconciliation.`;
660
+ }
661
+ return `Paperclip could not determine whether the checked-out branch "${actualBranch}" is forward of the recorded branch "${input.expectedBranchName}".`;
662
+ }
486
663
  async function inspectGitWorktreeBranchIncoherence(input) {
487
664
  const status = await runGit(["status", "--porcelain", "--untracked-files=all"], input.worktreePath).catch(() => null);
488
665
  const statusLines = status === null
489
666
  ? null
490
- : status.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
667
+ : status.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
668
+ const dirtyPathSample = sampleDirtyStatusPaths(statusLines);
491
669
  const cleanliness = status === null ? "unknown" : status.trim().length > 0 ? "dirty" : "clean";
670
+ const inProgressOperation = await detectGitWorktreeInProgressOperation(input.worktreePath);
492
671
  const expectedHeadSha = await runGit(["rev-parse", "--verify", `refs/heads/${input.expectedBranchName}^{commit}`], input.repoRoot).catch(() => null);
493
672
  const actualHeadSha = await runGit(["rev-parse", "HEAD"], input.worktreePath).catch(() => null);
494
673
  const actualBranchExists = input.actualBranchName
@@ -500,11 +679,46 @@ async function inspectGitWorktreeBranchIncoherence(input) {
500
679
  const registeredBranchMatchesHead = Boolean(registered && registeredBranchRef === actualBranchRef);
501
680
  const sameHead = Boolean(expectedHeadSha && actualHeadSha && expectedHeadSha === actualHeadSha);
502
681
  const expectedBranchExists = Boolean(expectedHeadSha);
503
- const eligible = cleanliness === "clean" && expectedBranchExists && sameHead && registeredBranchMatchesHead;
682
+ const ancestryVerdict = await getGitWorktreeBranchAncestryVerdict({
683
+ repoRoot: input.repoRoot,
684
+ expectedHeadSha,
685
+ actualHeadSha,
686
+ });
687
+ const basePlainLanguageReason = explainGitWorktreeBranchIncoherence({
688
+ expectedBranchName: input.expectedBranchName,
689
+ actualBranchName: input.actualBranchName,
690
+ expectedHeadSha,
691
+ actualHeadSha,
692
+ sameHead,
693
+ ancestryVerdict,
694
+ });
695
+ const plainLanguageReason = inProgressOperation
696
+ ? `${basePlainLanguageReason} An interrupted git ${GIT_IN_PROGRESS_OPERATION_LABELS[inProgressOperation]} is still in progress in this worktree.`
697
+ : basePlainLanguageReason;
698
+ const canCheckoutRecordedBranch = cleanliness === "clean" && expectedBranchExists && sameHead && registeredBranchMatchesHead;
699
+ const canAdoptForwardActualBranch = cleanliness === "clean" &&
700
+ expectedBranchExists &&
701
+ actualBranchExists === true &&
702
+ ancestryVerdict === "ancestor" &&
703
+ !sameHead &&
704
+ registeredBranchMatchesHead;
705
+ const canAttachRecordedBranchToDetachedHead = cleanliness === "clean" &&
706
+ expectedBranchExists &&
707
+ input.actualBranchName === null &&
708
+ ancestryVerdict === "ancestor" &&
709
+ !sameHead &&
710
+ registeredBranchMatchesHead;
711
+ const eligible = canCheckoutRecordedBranch || canAdoptForwardActualBranch || canAttachRecordedBranchToDetachedHead;
504
712
  const safeRepairReason = eligible
505
- ? "clean worktree and expected branch points at the current HEAD"
713
+ ? canCheckoutRecordedBranch
714
+ ? "clean worktree and expected branch points at the current HEAD"
715
+ : canAdoptForwardActualBranch
716
+ ? "clean worktree and checked-out branch is forward of the recorded branch"
717
+ : "clean detached worktree HEAD is forward of the recorded branch"
506
718
  : cleanliness !== "clean"
507
- ? "worktree is not clean"
719
+ ? inProgressOperation
720
+ ? `worktree is not clean and a git ${GIT_IN_PROGRESS_OPERATION_LABELS[inProgressOperation]} is in progress`
721
+ : "worktree is not clean"
508
722
  : !registered
509
723
  ? "worktree path is not registered"
510
724
  : !registeredBranchMatchesHead
@@ -524,6 +738,13 @@ async function inspectGitWorktreeBranchIncoherence(input) {
524
738
  expectedHeadSha,
525
739
  actualHeadSha,
526
740
  });
741
+ const contention = await findGitWorktreeBranchContention({
742
+ db: input.db ?? null,
743
+ sourceIssue: input.sourceIssue,
744
+ executionWorkspaceId: input.executionWorkspaceId ?? null,
745
+ worktreePath: input.worktreePath,
746
+ actualBranchName: input.actualBranchName,
747
+ });
527
748
  return {
528
749
  reason: GIT_WORKTREE_BRANCH_INCOHERENCE_REASON,
529
750
  fingerprint,
@@ -535,7 +756,10 @@ async function inspectGitWorktreeBranchIncoherence(input) {
535
756
  expectedBranch: input.expectedBranchName,
536
757
  actualBranch: input.actualBranchName,
537
758
  cleanliness,
759
+ inProgressOperation,
538
760
  statusEntryCount: statusLines?.length ?? null,
761
+ dirtyPathSample,
762
+ contention,
539
763
  provenance: {
540
764
  expectedBranchRef: `refs/heads/${input.expectedBranchName}`,
541
765
  actualBranchRef,
@@ -547,6 +771,8 @@ async function inspectGitWorktreeBranchIncoherence(input) {
547
771
  expectedHeadSha,
548
772
  actualHeadSha,
549
773
  sameHead,
774
+ ancestryVerdict,
775
+ plainLanguageReason,
550
776
  },
551
777
  safeRepair: {
552
778
  eligible,
@@ -561,16 +787,465 @@ function branchIncoherenceValidationFailure(evidence) {
561
787
  workspaceValidation: evidence,
562
788
  });
563
789
  }
790
+ function formatDirtyQuarantineContentionRefusal(contention) {
791
+ const activeRunText = contention.activeRun
792
+ ? ` with active run ${contention.activeRun.id}`
793
+ : " with no active run";
794
+ return `dirty quarantine repair refused because workspace ${contention.claimedByWorkspaceId} already claims the live branch${activeRunText}`;
795
+ }
796
+ function formatDirtyQuarantineFailure(error) {
797
+ const message = error instanceof Error ? error.message : String(error);
798
+ if (gitErrorIncludes(error, "index.lock") ||
799
+ gitErrorIncludes(error, "index lock") ||
800
+ gitErrorIncludes(error, "another git process") ||
801
+ gitErrorIncludes(error, "Unable to create")) {
802
+ return `dirty quarantine repair aborted because git reported index contention: ${message}`;
803
+ }
804
+ return `dirty quarantine repair failed: ${message}`;
805
+ }
806
+ function formatDirtyQuarantineAuditComment(input) {
807
+ const dirtySample = input.evidence.dirtyPathSample.length > 0
808
+ ? input.evidence.dirtyPathSample.map((entry) => `\`${entry}\``).join(", ")
809
+ : "`none captured`";
810
+ return [
811
+ "Execution workspace dirty worktree quarantined before restore.",
812
+ "",
813
+ `- Source issue: ${formatIssueReference(input.evidence.sourceIssueId, input.evidence.sourceIdentifier ?? input.sourceIssue?.identifier ?? null)}`,
814
+ `- Workspace: \`${input.evidence.executionWorkspaceId ?? "unpersisted"}\``,
815
+ `- Worktree: \`${input.evidence.worktreePath}\``,
816
+ `- Recorded branch: \`${input.evidence.expectedBranch}\``,
817
+ `- Live branch: \`${formatBranchForMessage(input.evidence.actualBranch)}\``,
818
+ `- Rescue branch: \`${input.rescueBranch}\``,
819
+ `- Rescue commit: \`${input.rescueCommitSha}\``,
820
+ `- Dirty file count: \`${input.fileCount}\``,
821
+ `- Dirty path sample: ${dirtySample}`,
822
+ ...(input.evidence.inProgressOperation
823
+ ? [`- Interrupted operation: \`git ${GIT_IN_PROGRESS_OPERATION_LABELS[input.evidence.inProgressOperation]}\` (state cleared after rescue; resolution preserved on the rescue branch)`]
824
+ : []),
825
+ `- Fingerprint: \`${input.evidence.fingerprint}\``,
826
+ input.claimant
827
+ ? `- Claimant: workspace \`${input.claimant.claimedByWorkspaceId}\` on issue ${formatIssueReference(input.claimant.claimedByIssueId, input.claimant.claimedByIssueIdentifier)}${input.claimant.activeRun ? ` with active run \`${input.claimant.activeRun.id}\`` : " with no active run"}`
828
+ : "- Claimant: none",
829
+ ].join("\n");
830
+ }
831
+ async function writeDirtyQuarantineAuditComments(input) {
832
+ const body = formatDirtyQuarantineAuditComment({
833
+ evidence: input.evidence,
834
+ rescueBranch: input.rescueBranch,
835
+ rescueCommitSha: input.rescueCommitSha,
836
+ fileCount: input.fileCount,
837
+ sourceIssue: input.sourceIssue,
838
+ claimant: input.evidence.contention,
839
+ });
840
+ let sourceAuditCommentId = null;
841
+ let claimantAuditCommentId = null;
842
+ const now = new Date();
843
+ if (input.evidence.sourceIssueId) {
844
+ const [sourceComment] = await input.db
845
+ .insert(issueComments)
846
+ .values({
847
+ companyId: input.companyId,
848
+ issueId: input.evidence.sourceIssueId,
849
+ authorAgentId: null,
850
+ authorUserId: null,
851
+ authorType: "system",
852
+ createdByRunId: input.heartbeatRunId,
853
+ body,
854
+ })
855
+ .returning({ id: issueComments.id });
856
+ sourceAuditCommentId = sourceComment?.id ?? null;
857
+ await input.db
858
+ .update(issues)
859
+ .set({ updatedAt: now })
860
+ .where(eq(issues.id, input.evidence.sourceIssueId));
861
+ }
862
+ const claimantIssueId = input.evidence.contention?.claimedByIssueId ?? null;
863
+ if (claimantIssueId && claimantIssueId !== input.evidence.sourceIssueId) {
864
+ const [claimantComment] = await input.db
865
+ .insert(issueComments)
866
+ .values({
867
+ companyId: input.companyId,
868
+ issueId: claimantIssueId,
869
+ authorAgentId: null,
870
+ authorUserId: null,
871
+ authorType: "system",
872
+ createdByRunId: input.heartbeatRunId,
873
+ body,
874
+ })
875
+ .returning({ id: issueComments.id });
876
+ claimantAuditCommentId = claimantComment?.id ?? null;
877
+ await input.db
878
+ .update(issues)
879
+ .set({ updatedAt: now })
880
+ .where(eq(issues.id, claimantIssueId));
881
+ }
882
+ return { sourceAuditCommentId, claimantAuditCommentId };
883
+ }
884
+ async function logDirtyQuarantineActivity(input) {
885
+ await logActivity(input.db, {
886
+ companyId: input.companyId,
887
+ actorType: "system",
888
+ actorId: "workspace_runtime",
889
+ runId: input.heartbeatRunId,
890
+ action: "execution_workspace.dirty_worktree_quarantined",
891
+ entityType: input.evidence.executionWorkspaceId ? "execution_workspace" : "issue",
892
+ entityId: input.evidence.executionWorkspaceId ?? input.evidence.sourceIssueId ?? input.companyId,
893
+ details: {
894
+ reason: GIT_WORKTREE_BRANCH_INCOHERENCE_REASON,
895
+ sourceIssueId: input.evidence.sourceIssueId,
896
+ executionWorkspaceId: input.evidence.executionWorkspaceId,
897
+ worktreePath: input.evidence.worktreePath,
898
+ expectedBranch: input.evidence.expectedBranch,
899
+ actualBranch: input.evidence.actualBranch,
900
+ rescueBranch: input.rescueBranch,
901
+ rescueCommitSha: input.rescueCommitSha,
902
+ fileCount: input.fileCount,
903
+ dirtyPathSample: input.evidence.dirtyPathSample,
904
+ fingerprint: input.evidence.fingerprint,
905
+ contention: input.evidence.contention,
906
+ sourceAuditCommentId: input.sourceAuditCommentId,
907
+ claimantAuditCommentId: input.claimantAuditCommentId,
908
+ actor: {
909
+ type: "system",
910
+ id: "workspace_runtime",
911
+ source: "workspace_runtime",
912
+ },
913
+ },
914
+ });
915
+ }
916
+ async function recordDirtyQuarantineOperation(input) {
917
+ if (!input.recorder)
918
+ return;
919
+ await input.recorder.recordOperation({
920
+ phase: input.phase ?? "worktree_prepare",
921
+ cwd: input.cwd,
922
+ metadata: {
923
+ repoRoot: input.evidence.repoRoot,
924
+ worktreePath: input.evidence.worktreePath,
925
+ expectedBranchName: input.evidence.expectedBranch,
926
+ actualBranchName: input.evidence.actualBranch,
927
+ branchIncoherenceDirtyQuarantineRepair: true,
928
+ rescueBranch: input.rescueBranch,
929
+ rescueCommitSha: input.rescueCommitSha,
930
+ fileCount: input.fileCount,
931
+ dirtyPathSample: input.evidence.dirtyPathSample,
932
+ fingerprint: input.evidence.fingerprint,
933
+ sourceIssueId: input.evidence.sourceIssueId,
934
+ executionWorkspaceId: input.evidence.executionWorkspaceId,
935
+ sourceAuditCommentId: input.sourceAuditCommentId,
936
+ claimantAuditCommentId: input.claimantAuditCommentId,
937
+ },
938
+ run: async () => ({
939
+ status: "succeeded",
940
+ system: `Quarantined dirty git worktree state on ${input.rescueBranch} (${formatShortSha(input.rescueCommitSha)}) and restored recorded branch ${input.evidence.expectedBranch}.\n`,
941
+ }),
942
+ });
943
+ }
944
+ async function quarantineDirtyWorktreeBranchIncoherence(input) {
945
+ const companyId = await readIssueCompanyId(input.db, input.evidence.sourceIssueId);
946
+ if (!companyId) {
947
+ input.evidence.safeRepair.eligible = false;
948
+ input.evidence.safeRepair.reason = "dirty quarantine repair requires a source issue company for audit";
949
+ throw branchIncoherenceValidationFailure(input.evidence);
950
+ }
951
+ const freshContention = await findGitWorktreeBranchContention({
952
+ db: input.db,
953
+ sourceIssue: input.sourceIssue,
954
+ executionWorkspaceId: input.executionWorkspaceId,
955
+ worktreePath: input.worktreePath,
956
+ actualBranchName: input.evidence.actualBranch,
957
+ });
958
+ input.evidence.contention = freshContention;
959
+ if (freshContention) {
960
+ input.evidence.safeRepair.eligible = false;
961
+ input.evidence.safeRepair.reason = formatDirtyQuarantineContentionRefusal(freshContention);
962
+ throw branchIncoherenceValidationFailure(input.evidence);
963
+ }
964
+ const rescueBranch = buildDirtyQuarantineRescueBranch(input.sourceIssue);
965
+ const fileCount = input.evidence.statusEntryCount ?? input.evidence.dirtyPathSample.length;
966
+ const baseMetadata = {
967
+ repoRoot: input.repoRoot,
968
+ worktreePath: input.worktreePath,
969
+ expectedBranchName: input.expectedBranchName,
970
+ actualBranchName: input.evidence.actualBranch,
971
+ branchIncoherenceDirtyQuarantineRepair: true,
972
+ rescueBranch,
973
+ fingerprint: input.evidence.fingerprint,
974
+ sourceIssueId: input.evidence.sourceIssueId,
975
+ executionWorkspaceId: input.evidence.executionWorkspaceId,
976
+ fileCount,
977
+ dirtyPathSample: input.evidence.dirtyPathSample,
978
+ contention: input.evidence.contention,
979
+ };
980
+ let rescueBranchCreated = false;
981
+ let expectedBranchRestored = false;
982
+ try {
983
+ await assertGitIndexIsUnlocked(input.worktreePath);
984
+ await recordGitOperation(input.recorder, {
985
+ phase: input.phase ?? "worktree_prepare",
986
+ args: ["checkout", "-b", rescueBranch],
987
+ cwd: input.worktreePath,
988
+ metadata: baseMetadata,
989
+ successMessage: `Created rescue branch ${rescueBranch} for dirty git worktree state at ${input.worktreePath}\n`,
990
+ failureLabel: `git checkout -b ${rescueBranch}`,
991
+ });
992
+ rescueBranchCreated = true;
993
+ await recordGitOperation(input.recorder, {
994
+ phase: input.phase ?? "worktree_prepare",
995
+ args: ["add", "-A"],
996
+ cwd: input.worktreePath,
997
+ metadata: baseMetadata,
998
+ successMessage: `Staged dirty git worktree state for rescue branch ${rescueBranch}\n`,
999
+ failureLabel: "git add -A",
1000
+ });
1001
+ await recordGitOperation(input.recorder, {
1002
+ phase: input.phase ?? "worktree_prepare",
1003
+ args: [
1004
+ "commit",
1005
+ "-m",
1006
+ "Paperclip dirty workspace rescue",
1007
+ "-m",
1008
+ [
1009
+ `Source-Issue: ${input.evidence.sourceIdentifier ?? input.evidence.sourceIssueId ?? "unknown"}`,
1010
+ `Run-Id: ${input.heartbeatRunId ?? "unknown"}`,
1011
+ `Recorded-Branch: ${input.expectedBranchName}`,
1012
+ `Live-Branch: ${formatBranchForMessage(input.evidence.actualBranch)}`,
1013
+ `Fingerprint: ${input.evidence.fingerprint}`,
1014
+ ].join("\n"),
1015
+ ],
1016
+ cwd: input.worktreePath,
1017
+ metadata: baseMetadata,
1018
+ successMessage: `Committed dirty git worktree state to rescue branch ${rescueBranch}\n`,
1019
+ failureLabel: "git commit dirty workspace rescue",
1020
+ });
1021
+ const rescueCommitSha = await runGit(["rev-parse", "HEAD"], input.worktreePath);
1022
+ await recordGitOperation(input.recorder, {
1023
+ phase: input.phase ?? "worktree_prepare",
1024
+ args: ["checkout", input.expectedBranchName],
1025
+ cwd: input.worktreePath,
1026
+ metadata: {
1027
+ ...baseMetadata,
1028
+ rescueCommitSha,
1029
+ },
1030
+ successMessage: `Restored recorded branch ${input.expectedBranchName} after dirty workspace rescue ${rescueBranch}\n`,
1031
+ failureLabel: `git checkout ${input.expectedBranchName}`,
1032
+ });
1033
+ expectedBranchRestored = true;
1034
+ // A run that died mid-rebase (or mid-merge/cherry-pick/revert/bisect)
1035
+ // leaves the operation's state directory behind even after the recorded
1036
+ // branch is checked out, which wedges the next git command in the
1037
+ // worktree. The rescue commit above already preserved the in-flight
1038
+ // resolution, so clearing the state metadata here loses nothing.
1039
+ let clearedInProgressOperation = null;
1040
+ const lingeringOperation = await detectGitWorktreeInProgressOperation(input.worktreePath);
1041
+ if (lingeringOperation) {
1042
+ const operationLabel = GIT_IN_PROGRESS_OPERATION_LABELS[lingeringOperation];
1043
+ const quitArgs = GIT_IN_PROGRESS_OPERATION_QUIT_ARGS[lingeringOperation];
1044
+ await recordGitOperation(input.recorder, {
1045
+ phase: input.phase ?? "worktree_prepare",
1046
+ args: quitArgs,
1047
+ cwd: input.worktreePath,
1048
+ metadata: {
1049
+ ...baseMetadata,
1050
+ clearedInProgressOperation: lingeringOperation,
1051
+ },
1052
+ successMessage: `Cleared interrupted git ${operationLabel} state after dirty workspace rescue ${rescueBranch}\n`,
1053
+ failureLabel: `git ${quitArgs.join(" ")}`,
1054
+ });
1055
+ const stillInProgress = await detectGitWorktreeInProgressOperation(input.worktreePath);
1056
+ if (stillInProgress) {
1057
+ input.evidence.safeRepair.succeeded = false;
1058
+ input.evidence.safeRepair.reason =
1059
+ `dirty quarantine repair could not clear the interrupted git ${GIT_IN_PROGRESS_OPERATION_LABELS[stillInProgress]} state`;
1060
+ throw branchIncoherenceValidationFailure(input.evidence);
1061
+ }
1062
+ clearedInProgressOperation = lingeringOperation;
1063
+ }
1064
+ const repairedBranch = await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], input.worktreePath)
1065
+ .catch(() => null);
1066
+ if (repairedBranch !== input.expectedBranchName) {
1067
+ input.evidence.safeRepair.succeeded = false;
1068
+ input.evidence.safeRepair.reason =
1069
+ `dirty quarantine repair checked out ${formatBranchForMessage(repairedBranch)} instead of ${input.expectedBranchName}`;
1070
+ throw branchIncoherenceValidationFailure(input.evidence);
1071
+ }
1072
+ const repairedStatus = await runGit(["status", "--porcelain", "--untracked-files=all"], input.worktreePath);
1073
+ if (repairedStatus.trim().length > 0) {
1074
+ input.evidence.safeRepair.succeeded = false;
1075
+ input.evidence.safeRepair.reason = "dirty quarantine repair completed but the worktree is still dirty";
1076
+ throw branchIncoherenceValidationFailure(input.evidence);
1077
+ }
1078
+ const comments = await writeDirtyQuarantineAuditComments({
1079
+ db: input.db,
1080
+ companyId,
1081
+ evidence: input.evidence,
1082
+ sourceIssue: input.sourceIssue,
1083
+ rescueBranch,
1084
+ rescueCommitSha,
1085
+ fileCount,
1086
+ heartbeatRunId: input.heartbeatRunId,
1087
+ });
1088
+ await logDirtyQuarantineActivity({
1089
+ db: input.db,
1090
+ companyId,
1091
+ evidence: input.evidence,
1092
+ rescueBranch,
1093
+ rescueCommitSha,
1094
+ fileCount,
1095
+ heartbeatRunId: input.heartbeatRunId,
1096
+ sourceAuditCommentId: comments.sourceAuditCommentId,
1097
+ claimantAuditCommentId: comments.claimantAuditCommentId,
1098
+ });
1099
+ await recordDirtyQuarantineOperation({
1100
+ recorder: input.recorder,
1101
+ phase: input.phase,
1102
+ cwd: input.worktreePath,
1103
+ evidence: input.evidence,
1104
+ rescueBranch,
1105
+ rescueCommitSha,
1106
+ fileCount,
1107
+ sourceAuditCommentId: comments.sourceAuditCommentId,
1108
+ claimantAuditCommentId: comments.claimantAuditCommentId,
1109
+ });
1110
+ return {
1111
+ rescueBranch,
1112
+ rescueCommitSha,
1113
+ fileCount,
1114
+ clearedInProgressOperation,
1115
+ ...comments,
1116
+ };
1117
+ }
1118
+ catch (error) {
1119
+ if (rescueBranchCreated && !expectedBranchRestored) {
1120
+ await runGit(["checkout", input.expectedBranchName], input.worktreePath).catch(() => null);
1121
+ }
1122
+ if (error instanceof WorkspaceRuntimeValidationFailure)
1123
+ throw error;
1124
+ input.evidence.safeRepair.succeeded = false;
1125
+ input.evidence.safeRepair.reason = formatDirtyQuarantineFailure(error);
1126
+ throw branchIncoherenceValidationFailure(input.evidence);
1127
+ }
1128
+ }
1129
+ async function recordForwardBranchReconcileOperation(input) {
1130
+ if (!input.recorder)
1131
+ return;
1132
+ await input.recorder.recordOperation({
1133
+ phase: input.phase ?? "worktree_prepare",
1134
+ cwd: input.cwd,
1135
+ metadata: {
1136
+ repoRoot: input.repoRoot,
1137
+ worktreePath: input.worktreePath,
1138
+ expectedBranchName: input.expectedBranchName,
1139
+ actualBranchName: input.actualBranchName,
1140
+ branchIncoherenceReconcileForward: true,
1141
+ reconcileMode: input.mode,
1142
+ fingerprint: input.fingerprint,
1143
+ sourceIssueId: input.sourceIssueId,
1144
+ executionWorkspaceId: input.executionWorkspaceId,
1145
+ expectedHeadSha: input.expectedHeadSha,
1146
+ actualHeadSha: input.actualHeadSha,
1147
+ ancestryVerdict: input.ancestryVerdict,
1148
+ auditCommentId: input.auditCommentId ?? null,
1149
+ recoveryActionId: input.recoveryActionId ?? null,
1150
+ },
1151
+ run: async () => ({
1152
+ status: "succeeded",
1153
+ system: input.mode === "record_updated"
1154
+ ? `Reconciled execution workspace branch record from ${input.expectedBranchName} to ${input.actualBranchName}; worktree left unchanged.\n`
1155
+ : `Adopted live git worktree branch ${input.actualBranchName} for this execution workspace realization; worktree left unchanged.\n`,
1156
+ }),
1157
+ });
1158
+ }
1159
+ async function logForwardBranchReconcileActivity(input) {
1160
+ await logActivity(input.db, {
1161
+ companyId: input.companyId,
1162
+ actorType: "system",
1163
+ actorId: "workspace_runtime",
1164
+ runId: input.runId,
1165
+ action: "execution_workspace.branch_reconciled",
1166
+ entityType: "execution_workspace",
1167
+ entityId: input.executionWorkspaceId,
1168
+ details: {
1169
+ mode: input.mode,
1170
+ reason: input.reason,
1171
+ fromBranch: input.fromBranch,
1172
+ toBranch: input.toBranch,
1173
+ fromSha: input.fromSha,
1174
+ toSha: input.toSha,
1175
+ ancestryVerdict: input.ancestryVerdict,
1176
+ fingerprint: input.fingerprint,
1177
+ sourceIssueId: input.sourceIssueId,
1178
+ auditCommentId: input.auditCommentId,
1179
+ recoveryActionId: input.recoveryActionId,
1180
+ actor: {
1181
+ type: "system",
1182
+ id: "workspace_runtime",
1183
+ source: "workspace_runtime",
1184
+ },
1185
+ },
1186
+ });
1187
+ }
1188
+ export async function reconcilePendingForwardBranchAfterPersistence(input) {
1189
+ const result = await executionWorkspaceService(input.db).reconcileExecutionWorkspaceBranch(input.executionWorkspaceId, {
1190
+ mode: "forward",
1191
+ reason: input.pending.reason,
1192
+ alternateRecoveryFingerprints: [input.pending.prePersistenceFingerprint],
1193
+ actor: {
1194
+ actorType: "system",
1195
+ actorId: "workspace_runtime",
1196
+ agentId: null,
1197
+ runId: input.heartbeatRunId ?? null,
1198
+ },
1199
+ });
1200
+ await logForwardBranchReconcileActivity({
1201
+ db: input.db,
1202
+ companyId: result.workspace.companyId,
1203
+ executionWorkspaceId: result.workspace.id,
1204
+ sourceIssueId: result.workspace.sourceIssueId,
1205
+ runId: input.heartbeatRunId ?? null,
1206
+ mode: "forward",
1207
+ reason: input.pending.reason,
1208
+ fromBranch: result.inspection.fromBranch,
1209
+ toBranch: result.inspection.toBranch,
1210
+ fromSha: result.inspection.fromSha,
1211
+ toSha: result.inspection.toSha,
1212
+ ancestryVerdict: result.inspection.ancestryVerdict,
1213
+ fingerprint: result.inspection.fingerprint,
1214
+ auditCommentId: result.auditCommentId,
1215
+ recoveryActionId: result.recoveryAction?.id ?? null,
1216
+ });
1217
+ await recordForwardBranchReconcileOperation({
1218
+ recorder: input.recorder,
1219
+ phase: input.reconcileOperationPhase,
1220
+ cwd: result.inspection.worktreePath,
1221
+ repoRoot: result.inspection.repoRoot,
1222
+ worktreePath: result.inspection.worktreePath,
1223
+ expectedBranchName: result.inspection.fromBranch,
1224
+ actualBranchName: result.inspection.toBranch,
1225
+ executionWorkspaceId: result.workspace.id,
1226
+ sourceIssueId: result.workspace.sourceIssueId,
1227
+ fingerprint: result.inspection.fingerprint,
1228
+ expectedHeadSha: result.inspection.fromSha,
1229
+ actualHeadSha: result.inspection.toSha,
1230
+ ancestryVerdict: result.inspection.ancestryVerdict,
1231
+ mode: "adopt_for_realize",
1232
+ auditCommentId: result.auditCommentId,
1233
+ recoveryActionId: result.recoveryAction?.id ?? null,
1234
+ });
1235
+ return result;
1236
+ }
564
1237
  export async function ensureGitWorktreeBranchCoherent(input) {
565
1238
  const expectedBranchName = input.expectedBranchName?.trim();
566
1239
  if (!expectedBranchName)
567
- return;
1240
+ return { branchName: null, reconciledForward: false, warnings: [] };
568
1241
  const currentBranch = input.actualBranchName !== undefined
569
1242
  ? input.actualBranchName
570
1243
  : await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], input.worktreePath).catch(() => null);
571
- if (currentBranch === expectedBranchName)
572
- return;
1244
+ if (currentBranch === expectedBranchName) {
1245
+ return { branchName: expectedBranchName, reconciledForward: false, warnings: [] };
1246
+ }
573
1247
  const evidence = await inspectGitWorktreeBranchIncoherence({
1248
+ db: input.db ?? null,
574
1249
  repoRoot: input.repoRoot,
575
1250
  worktreePath: input.worktreePath,
576
1251
  expectedBranchName,
@@ -578,10 +1253,203 @@ export async function ensureGitWorktreeBranchCoherent(input) {
578
1253
  sourceIssue: input.sourceIssue,
579
1254
  executionWorkspaceId: input.executionWorkspaceId ?? null,
580
1255
  });
1256
+ if (evidence.cleanliness === "dirty" && input.enableWorkspaceDirtyQuarantineRepair === true) {
1257
+ if (!input.db) {
1258
+ evidence.safeRepair.reason = "dirty quarantine repair requires database access for claimant checks and audit";
1259
+ throw branchIncoherenceValidationFailure(evidence);
1260
+ }
1261
+ if (!evidence.provenance.registeredPathFound) {
1262
+ evidence.safeRepair.reason = "dirty quarantine repair requires a registered git worktree path";
1263
+ throw branchIncoherenceValidationFailure(evidence);
1264
+ }
1265
+ if (!evidence.provenance.expectedBranchExists) {
1266
+ evidence.safeRepair.reason = "dirty quarantine repair requires the recorded branch to exist";
1267
+ throw branchIncoherenceValidationFailure(evidence);
1268
+ }
1269
+ if (evidence.contention) {
1270
+ evidence.safeRepair.eligible = false;
1271
+ evidence.safeRepair.reason = formatDirtyQuarantineContentionRefusal(evidence.contention);
1272
+ throw branchIncoherenceValidationFailure(evidence);
1273
+ }
1274
+ await assertDirtyQuarantineRuntimeServicesStopped({
1275
+ db: input.db,
1276
+ executionWorkspaceId: input.executionWorkspaceId ?? null,
1277
+ evidence,
1278
+ });
1279
+ evidence.safeRepair.eligible = true;
1280
+ evidence.safeRepair.attempted = true;
1281
+ evidence.safeRepair.reason = "dirty worktree can be quarantined on a rescue branch before restoring the recorded branch";
1282
+ const result = await quarantineDirtyWorktreeBranchIncoherence({
1283
+ db: input.db,
1284
+ repoRoot: input.repoRoot,
1285
+ worktreePath: input.worktreePath,
1286
+ expectedBranchName,
1287
+ sourceIssue: input.sourceIssue,
1288
+ executionWorkspaceId: input.executionWorkspaceId ?? null,
1289
+ heartbeatRunId: input.heartbeatRunId ?? null,
1290
+ evidence,
1291
+ phase: input.reconcileOperationPhase,
1292
+ recorder: input.recorder ?? null,
1293
+ });
1294
+ evidence.safeRepair.succeeded = true;
1295
+ evidence.safeRepair.reason = result.clearedInProgressOperation
1296
+ ? `dirty worktree quarantined on ${result.rescueBranch} at ${formatShortSha(result.rescueCommitSha)}; interrupted git ${GIT_IN_PROGRESS_OPERATION_LABELS[result.clearedInProgressOperation]} state cleared`
1297
+ : `dirty worktree quarantined on ${result.rescueBranch} at ${formatShortSha(result.rescueCommitSha)}`;
1298
+ return {
1299
+ branchName: expectedBranchName,
1300
+ reconciledForward: false,
1301
+ dirtyQuarantineRepair: result,
1302
+ warnings: [
1303
+ `Execution workspace dirty worktree state was quarantined on rescue branch "${result.rescueBranch}" (${formatShortSha(result.rescueCommitSha)}; ${result.fileCount} ${result.fileCount === 1 ? "file" : "files"}) before restoring recorded branch "${expectedBranchName}".${result.clearedInProgressOperation ? ` An interrupted git ${GIT_IN_PROGRESS_OPERATION_LABELS[result.clearedInProgressOperation]} was also cleared; its in-flight state is preserved on the rescue branch.` : ""}`,
1304
+ ],
1305
+ };
1306
+ }
1307
+ if (input.enableWorkspaceBranchReconcileForward === true &&
1308
+ evidence.provenance.ancestryVerdict === "ancestor" &&
1309
+ !evidence.provenance.sameHead &&
1310
+ evidence.cleanliness === "clean" &&
1311
+ currentBranch) {
1312
+ const reason = "Automatic forward reconciliation: recorded branch is an ancestor of the checked-out branch.";
1313
+ if (input.executionWorkspaceId && input.persistForwardReconcile !== false) {
1314
+ if (!input.db) {
1315
+ evidence.safeRepair.reason = "forward reconciliation requires database access to update the execution workspace record";
1316
+ throw branchIncoherenceValidationFailure(evidence);
1317
+ }
1318
+ try {
1319
+ const result = await executionWorkspaceService(input.db).reconcileExecutionWorkspaceBranch(input.executionWorkspaceId, {
1320
+ mode: "forward",
1321
+ reason,
1322
+ actor: {
1323
+ actorType: "system",
1324
+ actorId: "workspace_runtime",
1325
+ agentId: null,
1326
+ runId: input.heartbeatRunId ?? null,
1327
+ },
1328
+ });
1329
+ await logForwardBranchReconcileActivity({
1330
+ db: input.db,
1331
+ companyId: result.workspace.companyId,
1332
+ executionWorkspaceId: result.workspace.id,
1333
+ sourceIssueId: result.workspace.sourceIssueId ?? evidence.sourceIssueId ?? null,
1334
+ runId: input.heartbeatRunId ?? null,
1335
+ mode: "forward",
1336
+ reason,
1337
+ fromBranch: result.inspection.fromBranch,
1338
+ toBranch: result.inspection.toBranch,
1339
+ fromSha: result.inspection.fromSha,
1340
+ toSha: result.inspection.toSha,
1341
+ ancestryVerdict: result.inspection.ancestryVerdict,
1342
+ fingerprint: result.inspection.fingerprint,
1343
+ auditCommentId: result.auditCommentId,
1344
+ recoveryActionId: result.recoveryAction?.id ?? null,
1345
+ });
1346
+ await recordForwardBranchReconcileOperation({
1347
+ recorder: input.recorder,
1348
+ phase: input.reconcileOperationPhase,
1349
+ cwd: input.worktreePath,
1350
+ repoRoot: result.inspection.repoRoot,
1351
+ worktreePath: result.inspection.worktreePath,
1352
+ expectedBranchName: result.inspection.fromBranch,
1353
+ actualBranchName: result.inspection.toBranch,
1354
+ executionWorkspaceId: result.workspace.id,
1355
+ sourceIssueId: result.workspace.sourceIssueId ?? evidence.sourceIssueId ?? null,
1356
+ fingerprint: result.inspection.fingerprint,
1357
+ expectedHeadSha: result.inspection.fromSha,
1358
+ actualHeadSha: result.inspection.toSha,
1359
+ ancestryVerdict: result.inspection.ancestryVerdict,
1360
+ mode: "record_updated",
1361
+ auditCommentId: result.auditCommentId,
1362
+ recoveryActionId: result.recoveryAction?.id ?? null,
1363
+ });
1364
+ return { branchName: result.inspection.toBranch, reconciledForward: true, warnings: [] };
1365
+ }
1366
+ catch (error) {
1367
+ evidence.safeRepair.reason =
1368
+ `forward reconciliation failed: ${error instanceof Error ? error.message : String(error)}`;
1369
+ throw branchIncoherenceValidationFailure(evidence);
1370
+ }
1371
+ }
1372
+ if (!input.db) {
1373
+ evidence.safeRepair.reason = "forward reconciliation adoption requires database access to audit after workspace realization";
1374
+ throw branchIncoherenceValidationFailure(evidence);
1375
+ }
1376
+ return {
1377
+ branchName: currentBranch,
1378
+ reconciledForward: true,
1379
+ warnings: [],
1380
+ pendingForwardBranchReconcile: {
1381
+ recordedBranchName: expectedBranchName,
1382
+ adoptedBranchName: currentBranch,
1383
+ prePersistenceFingerprint: evidence.fingerprint,
1384
+ reason,
1385
+ },
1386
+ };
1387
+ }
581
1388
  if (!evidence.safeRepair.eligible) {
582
1389
  throw branchIncoherenceValidationFailure(evidence);
583
1390
  }
584
1391
  evidence.safeRepair.attempted = true;
1392
+ const warningPrefix = `Execution workspace branch metadata was self-healed from "${expectedBranchName}" to "${formatBranchForMessage(currentBranch)}" at ${input.worktreePath}.`;
1393
+ if (currentBranch &&
1394
+ evidence.provenance.actualBranchExists === true &&
1395
+ evidence.provenance.ancestryVerdict === "ancestor" &&
1396
+ !evidence.provenance.sameHead) {
1397
+ evidence.safeRepair.succeeded = true;
1398
+ evidence.safeRepair.reason = "clean worktree adopted the checked-out branch because it is forward of the recorded branch";
1399
+ return {
1400
+ branchName: currentBranch,
1401
+ reconciledForward: false,
1402
+ warnings: [
1403
+ `${warningPrefix} The checked-out branch contains the recorded branch plus newer commits, so Paperclip adopted it for subsequent runs.`,
1404
+ ],
1405
+ };
1406
+ }
1407
+ if (currentBranch === null &&
1408
+ evidence.provenance.ancestryVerdict === "ancestor" &&
1409
+ !evidence.provenance.sameHead &&
1410
+ evidence.provenance.actualHeadSha) {
1411
+ try {
1412
+ await recordGitOperation(input.recorder, {
1413
+ phase: "worktree_prepare",
1414
+ args: ["checkout", "-B", expectedBranchName, evidence.provenance.actualHeadSha],
1415
+ cwd: input.worktreePath,
1416
+ metadata: {
1417
+ repoRoot: input.repoRoot,
1418
+ worktreePath: input.worktreePath,
1419
+ expectedBranchName,
1420
+ actualBranchName: currentBranch,
1421
+ branchIncoherenceRepair: true,
1422
+ detachedHeadRepair: true,
1423
+ fingerprint: evidence.fingerprint,
1424
+ sourceIssueId: evidence.sourceIssueId,
1425
+ executionWorkspaceId: evidence.executionWorkspaceId,
1426
+ },
1427
+ successMessage: `Reattached detached git worktree HEAD at ${input.worktreePath} to ${expectedBranchName}\n`,
1428
+ failureLabel: `git checkout -B ${expectedBranchName} ${formatShortSha(evidence.provenance.actualHeadSha)}`,
1429
+ });
1430
+ }
1431
+ catch (error) {
1432
+ evidence.safeRepair.succeeded = false;
1433
+ evidence.safeRepair.reason = `safe detached HEAD reattachment failed: ${error instanceof Error ? error.message : String(error)}`;
1434
+ throw branchIncoherenceValidationFailure(evidence);
1435
+ }
1436
+ const repairedBranch = await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], input.worktreePath)
1437
+ .catch(() => null);
1438
+ if (repairedBranch !== expectedBranchName) {
1439
+ evidence.safeRepair.succeeded = false;
1440
+ evidence.safeRepair.reason = `reattach completed but HEAD is ${formatBranchForMessage(repairedBranch)}`;
1441
+ throw branchIncoherenceValidationFailure(evidence);
1442
+ }
1443
+ evidence.safeRepair.succeeded = true;
1444
+ evidence.safeRepair.reason = "clean detached worktree HEAD was reattached to the recorded branch";
1445
+ return {
1446
+ branchName: expectedBranchName,
1447
+ reconciledForward: false,
1448
+ warnings: [
1449
+ `${warningPrefix} The detached HEAD contained the recorded branch plus newer commits, so Paperclip moved the recorded branch to that HEAD.`,
1450
+ ],
1451
+ };
1452
+ }
585
1453
  try {
586
1454
  await recordGitOperation(input.recorder, {
587
1455
  phase: "worktree_prepare",
@@ -615,6 +1483,13 @@ export async function ensureGitWorktreeBranchCoherent(input) {
615
1483
  }
616
1484
  evidence.safeRepair.succeeded = true;
617
1485
  evidence.safeRepair.reason = "clean worktree checked out the recorded branch";
1486
+ return {
1487
+ branchName: expectedBranchName,
1488
+ reconciledForward: false,
1489
+ warnings: [
1490
+ `Execution workspace branch metadata was self-healed by checking out recorded branch "${expectedBranchName}" at ${input.worktreePath}.`,
1491
+ ],
1492
+ };
618
1493
  }
619
1494
  // Resolve the authoritative base ref for a fresh worktree. A configured local
620
1495
  // branch is mapped to its `origin/<branch>` counterpart so unpushed local
@@ -1165,12 +2040,13 @@ export async function realizeExecutionWorkspace(input) {
1165
2040
  projectId: input.base.projectId,
1166
2041
  repoRef: input.base.repoRef,
1167
2042
  });
1168
- const branchName = sanitizeBranchName(renderedBranch);
2043
+ let branchName = sanitizeBranchName(renderedBranch);
1169
2044
  const configuredParentDir = asString(rawStrategy.worktreeParentDir, "");
1170
2045
  const worktreeParentDir = configuredParentDir
1171
2046
  ? resolveConfiguredPath(configuredParentDir, repoRoot)
1172
2047
  : path.join(repoRoot, ".paperclip", "worktrees");
1173
2048
  const worktreePath = path.join(worktreeParentDir, branchName);
2049
+ let pendingForwardBranchReconcile = null;
1174
2050
  const configuredBaseRef = typeof rawStrategy.baseRef === "string" && rawStrategy.baseRef.length > 0
1175
2051
  ? rawStrategy.baseRef
1176
2052
  : input.base.repoRef ?? null;
@@ -1181,12 +2057,12 @@ export async function realizeExecutionWorkspace(input) {
1181
2057
  ];
1182
2058
  const currentBaseRefSha = await resolveBaseRefSha(repoRoot, baseRef);
1183
2059
  await fs.mkdir(worktreeParentDir, { recursive: true });
1184
- async function reuseExistingWorktree(reusablePath) {
2060
+ async function reuseExistingWorktree(reusablePath, effectiveBranchName = branchName, extraWarnings = []) {
1185
2061
  const refresh = currentBaseRefSha
1186
2062
  ? await refreshUnstartedWorktreeToBase({
1187
2063
  repoRoot,
1188
2064
  worktreePath: reusablePath,
1189
- branchName,
2065
+ branchName: effectiveBranchName,
1190
2066
  baseRef,
1191
2067
  currentBaseRefSha,
1192
2068
  recorder: input.recorder ?? null,
@@ -1207,7 +2083,7 @@ export async function realizeExecutionWorkspace(input) {
1207
2083
  metadata: {
1208
2084
  repoRoot,
1209
2085
  worktreePath: reusablePath,
1210
- branchName,
2086
+ branchName: effectiveBranchName,
1211
2087
  baseRef,
1212
2088
  currentBaseRefSha: baseDrift.currentBaseRefSha,
1213
2089
  branchBaseRefSha: baseDrift.branchBaseRefSha,
@@ -1226,7 +2102,7 @@ export async function realizeExecutionWorkspace(input) {
1226
2102
  base: input.base,
1227
2103
  repoRoot,
1228
2104
  worktreePath: reusablePath,
1229
- branchName,
2105
+ branchName: effectiveBranchName,
1230
2106
  issue: input.issue,
1231
2107
  agent: input.agent,
1232
2108
  created: false,
@@ -1237,11 +2113,12 @@ export async function realizeExecutionWorkspace(input) {
1237
2113
  repoRef: baseRef,
1238
2114
  strategy: "git_worktree",
1239
2115
  cwd: reusablePath,
1240
- branchName,
2116
+ branchName: effectiveBranchName,
1241
2117
  worktreePath: reusablePath,
1242
- warnings: [...baseRefreshWarnings, ...baseDrift.warnings],
2118
+ warnings: [...extraWarnings, ...baseRefreshWarnings, ...baseDrift.warnings],
1243
2119
  created: false,
1244
2120
  baseRefSha: refresh.baseRefSha ?? baseDrift.branchBaseRefSha ?? baseDrift.currentBaseRefSha,
2121
+ pendingForwardBranchReconcile,
1245
2122
  };
1246
2123
  }
1247
2124
  async function validateReusableWorktree(reusablePath) {
@@ -1251,38 +2128,55 @@ export async function realizeExecutionWorkspace(input) {
1251
2128
  expectedBranchName: branchName,
1252
2129
  }).catch(() => null);
1253
2130
  if (validation && !validation.valid && validation.reasonCode === "branch_mismatch") {
1254
- await ensureGitWorktreeBranchCoherent({
2131
+ const coherence = await ensureGitWorktreeBranchCoherent({
2132
+ db: input.db ?? null,
1255
2133
  repoRoot,
1256
2134
  worktreePath: reusablePath,
1257
2135
  expectedBranchName: branchName,
1258
2136
  actualBranchName: validation.actualBranchName ?? null,
1259
2137
  sourceIssue: input.issue,
1260
2138
  executionWorkspaceId: null,
2139
+ heartbeatRunId: input.heartbeatRunId ?? null,
2140
+ enableWorkspaceBranchReconcileForward: input.enableWorkspaceBranchReconcileForward === true,
2141
+ enableWorkspaceDirtyQuarantineRepair: input.enableWorkspaceDirtyQuarantineRepair === true,
2142
+ reconcileOperationPhase: "worktree_prepare",
1261
2143
  recorder: input.recorder ?? null,
1262
2144
  });
1263
- return await validateLinkedGitWorktree({
2145
+ const effectiveBranchName = coherence.branchName ?? branchName;
2146
+ if (coherence.reconciledForward) {
2147
+ branchName = effectiveBranchName;
2148
+ pendingForwardBranchReconcile = coherence.pendingForwardBranchReconcile ?? null;
2149
+ }
2150
+ const nextValidation = await validateLinkedGitWorktree({
1264
2151
  repoRoot,
1265
2152
  worktreePath: reusablePath,
1266
- expectedBranchName: branchName,
2153
+ expectedBranchName: effectiveBranchName,
1267
2154
  }).catch(() => null);
2155
+ return {
2156
+ validation: nextValidation,
2157
+ branchName: effectiveBranchName,
2158
+ warnings: coherence.warnings,
2159
+ };
1268
2160
  }
1269
- return validation;
2161
+ return { validation, branchName, warnings: [] };
1270
2162
  }
1271
2163
  const existingWorktree = await directoryExists(worktreePath);
1272
2164
  if (existingWorktree) {
1273
- const validation = await validateReusableWorktree(worktreePath);
1274
- if (validation?.valid) {
1275
- return await reuseExistingWorktree(worktreePath);
2165
+ const reusable = await validateReusableWorktree(worktreePath);
2166
+ if (reusable.validation?.valid) {
2167
+ return await reuseExistingWorktree(worktreePath, reusable.branchName, reusable.warnings);
1276
2168
  }
2169
+ const validation = reusable.validation;
1277
2170
  const reason = validation && !validation.valid ? ` (${validation.reason})` : "";
1278
2171
  throw new Error(`Configured worktree path "${worktreePath}" already exists and is not a reusable git worktree${reason}.`);
1279
2172
  }
1280
2173
  const registeredBranchWorktree = await findRegisteredGitWorktreeByBranch(repoRoot, branchName);
1281
2174
  if (registeredBranchWorktree) {
1282
- const validation = await validateReusableWorktree(registeredBranchWorktree);
1283
- if (validation?.valid) {
1284
- return await reuseExistingWorktree(registeredBranchWorktree);
2175
+ const reusable = await validateReusableWorktree(registeredBranchWorktree);
2176
+ if (reusable.validation?.valid) {
2177
+ return await reuseExistingWorktree(registeredBranchWorktree, reusable.branchName, reusable.warnings);
1285
2178
  }
2179
+ const validation = reusable.validation;
1286
2180
  const reason = validation && !validation.valid ? ` (${validation.reason})` : "";
1287
2181
  throw new Error(`Registered worktree for branch "${branchName}" at "${registeredBranchWorktree}" is not reusable${reason}.`);
1288
2182
  }
@@ -1391,15 +2285,29 @@ export async function ensurePersistedExecutionWorkspaceAvailable(input) {
1391
2285
  if (await directoryExists(cwd)) {
1392
2286
  const reuseBaseRef = input.workspace.baseRef ?? input.base.repoRef ?? null;
1393
2287
  const reuseWorktreePath = realized.worktreePath ?? cwd;
2288
+ const repairWarnings = [];
1394
2289
  if (await isGitCheckout(reuseWorktreePath)) {
1395
- await ensureGitWorktreeBranchCoherent({
2290
+ const coherence = await ensureGitWorktreeBranchCoherent({
2291
+ db: input.db ?? null,
1396
2292
  repoRoot,
1397
2293
  worktreePath: reuseWorktreePath,
1398
2294
  expectedBranchName: realized.branchName,
1399
2295
  sourceIssue: input.issue,
1400
2296
  executionWorkspaceId: input.workspace.id ?? null,
2297
+ heartbeatRunId: input.heartbeatRunId ?? null,
2298
+ enableWorkspaceBranchReconcileForward: input.enableWorkspaceBranchReconcileForward === true,
2299
+ enableWorkspaceDirtyQuarantineRepair: input.enableWorkspaceDirtyQuarantineRepair === true,
2300
+ persistForwardReconcile: false,
2301
+ reconcileOperationPhase: "worktree_prepare",
1401
2302
  recorder: input.recorder ?? null,
1402
2303
  });
2304
+ if (coherence.branchName) {
2305
+ realized.branchName = coherence.branchName;
2306
+ }
2307
+ if (coherence.reconciledForward) {
2308
+ realized.pendingForwardBranchReconcile = coherence.pendingForwardBranchReconcile ?? null;
2309
+ }
2310
+ repairWarnings.push(...coherence.warnings);
1403
2311
  }
1404
2312
  const validation = await validateLinkedGitWorktree({
1405
2313
  repoRoot,
@@ -1438,7 +2346,7 @@ export async function ensurePersistedExecutionWorkspaceAvailable(input) {
1438
2346
  recordedBaseRefSha,
1439
2347
  skipRefresh: true,
1440
2348
  });
1441
- realized.warnings = [...baseRefreshWarnings, ...baseDrift.warnings];
2349
+ realized.warnings = [...repairWarnings, ...baseRefreshWarnings, ...baseDrift.warnings];
1442
2350
  realized.baseRefSha = refresh.baseRefSha ?? recordedBaseRefSha ?? baseDrift.branchBaseRefSha ?? baseDrift.currentBaseRefSha;
1443
2351
  if (provisionCommand) {
1444
2352
  await provisionExecutionWorktree({
@@ -1870,13 +2778,20 @@ async function waitForReadiness(input) {
1870
2778
  const readinessType = asString(readiness.type, "");
1871
2779
  if (readinessType !== "http" || !input.url)
1872
2780
  return;
2781
+ const readinessUrl = resolveRuntimeServiceHealthUrl(input.url, {
2782
+ serviceName: input.serviceName,
2783
+ command: input.command,
2784
+ });
2785
+ if (!readinessUrl) {
2786
+ throw new Error(`Readiness check failed: could not resolve health URL for ${input.url}`);
2787
+ }
1873
2788
  const timeoutSec = resolveWorkspaceRuntimeReadinessTimeoutSec(input.service);
1874
2789
  const intervalMs = Math.max(100, asNumber(readiness.intervalMs, 500));
1875
2790
  const deadline = Date.now() + timeoutSec * 1000;
1876
2791
  let lastError = "service did not become ready";
1877
2792
  while (Date.now() < deadline) {
1878
2793
  try {
1879
- const response = await fetch(input.url);
2794
+ const response = await fetch(readinessUrl);
1880
2795
  if (response.ok)
1881
2796
  return;
1882
2797
  lastError = `received HTTP ${response.status}`;
@@ -1886,7 +2801,7 @@ async function waitForReadiness(input) {
1886
2801
  }
1887
2802
  await delay(intervalMs);
1888
2803
  }
1889
- throw new Error(`Readiness check failed for ${input.url}: ${lastError}`);
2804
+ throw new Error(`Readiness check failed for ${readinessUrl}: ${lastError}`);
1890
2805
  }
1891
2806
  function isPaperclipDevRuntimeService(input) {
1892
2807
  const serviceName = (input.serviceName ?? "").trim().toLowerCase();
@@ -1917,7 +2832,7 @@ async function isRuntimeServiceUrlHealthy(url, input) {
1917
2832
  return true;
1918
2833
  const healthUrl = resolveRuntimeServiceHealthUrl(url, input);
1919
2834
  if (!healthUrl)
1920
- return true;
2835
+ return false;
1921
2836
  try {
1922
2837
  const response = await fetch(healthUrl, { signal: AbortSignal.timeout(2_000) });
1923
2838
  return response.ok;
@@ -2071,7 +2986,7 @@ export function normalizeAdapterManagedRuntimeServices(input) {
2071
2986
  };
2072
2987
  });
2073
2988
  }
2074
- async function startLocalRuntimeService(input) {
2989
+ async function spawnLocalRuntimeService(input) {
2075
2990
  const leaseRunId = input.leaseRunId === undefined ? input.runId : input.leaseRunId;
2076
2991
  const startedByRunId = input.startedByRunId === undefined ? input.runId : input.startedByRunId;
2077
2992
  const identity = resolveRuntimeServiceReuseIdentity({
@@ -2168,47 +3083,58 @@ async function startLocalRuntimeService(input) {
2168
3083
  }
2169
3084
  else {
2170
3085
  return {
2171
- id: adoptedRecord.runtimeServiceId ?? randomUUID(),
2172
- companyId: input.agent.companyId,
2173
- projectId: input.workspace.projectId,
2174
- projectWorkspaceId: input.workspace.workspaceId,
2175
- executionWorkspaceId: input.executionWorkspaceId ?? null,
2176
- issueId: input.issue?.id ?? null,
2177
- serviceName,
2178
- status: "running",
2179
- lifecycle,
2180
- scopeType: input.scopeType,
2181
- scopeId: input.scopeId,
2182
- reuseKey: input.reuseKey,
2183
- command,
2184
- cwd: serviceCwd,
2185
- port: adoptedRecord.port ?? port,
2186
- url: adoptedRecord.url ?? url,
2187
- provider: "local_process",
2188
- providerRef: String(adoptedRecord.pid),
2189
- ownerAgentId: input.agent.id ?? null,
2190
- startedByRunId,
2191
- lastUsedAt: new Date().toISOString(),
2192
- startedAt: adoptedRecord.startedAt,
2193
- stoppedAt: null,
2194
- stopPolicy,
2195
- healthStatus: "healthy",
2196
- reused: true,
2197
- db: input.db,
2198
- child: null,
2199
- leaseRunIds: leaseRunId ? new Set([leaseRunId]) : new Set(),
2200
- idleTimer: null,
2201
- envFingerprint,
2202
- serviceKey,
2203
- profileKind: "workspace-runtime",
2204
- processGroupId: adoptedRecord.processGroupId ?? null,
3086
+ record: {
3087
+ id: adoptedRecord.runtimeServiceId ?? randomUUID(),
3088
+ companyId: input.agent.companyId,
3089
+ projectId: input.workspace.projectId,
3090
+ projectWorkspaceId: input.workspace.workspaceId,
3091
+ executionWorkspaceId: input.executionWorkspaceId ?? null,
3092
+ issueId: input.issue?.id ?? null,
3093
+ serviceName,
3094
+ status: "running",
3095
+ lifecycle,
3096
+ scopeType: input.scopeType,
3097
+ scopeId: input.scopeId,
3098
+ reuseKey: input.reuseKey,
3099
+ command,
3100
+ cwd: serviceCwd,
3101
+ port: adoptedRecord.port ?? port,
3102
+ url: adoptedRecord.url ?? url,
3103
+ provider: "local_process",
3104
+ providerRef: String(adoptedRecord.pid),
3105
+ ownerAgentId: input.agent.id ?? null,
3106
+ startedByRunId,
3107
+ lastUsedAt: new Date().toISOString(),
3108
+ startedAt: adoptedRecord.startedAt,
3109
+ stoppedAt: null,
3110
+ stopPolicy,
3111
+ healthStatus: "healthy",
3112
+ reused: true,
3113
+ db: input.db,
3114
+ child: null,
3115
+ leaseRunIds: leaseRunId ? new Set([leaseRunId]) : new Set(),
3116
+ idleTimer: null,
3117
+ envFingerprint,
3118
+ serviceKey,
3119
+ profileKind: "workspace-runtime",
3120
+ processGroupId: adoptedRecord.processGroupId ?? null,
3121
+ },
3122
+ readiness: Promise.resolve(),
2205
3123
  };
2206
3124
  }
2207
3125
  }
2208
3126
  if (identityPort) {
2209
3127
  const ownerPid = await readLocalServicePortOwner(identityPort);
2210
3128
  if (ownerPid) {
2211
- throw new Error(`Runtime service "${serviceName}" could not start because port ${identityPort} is already in use by pid ${ownerPid}`);
3129
+ const ownerCwd = await readLocalServiceProcessCwd(ownerPid);
3130
+ const ownerIsInWorkspace = ownerCwd
3131
+ ? await isLocalServiceProcessInWorkspace(ownerCwd, serviceCwd)
3132
+ : null;
3133
+ const ownerDescription = ownerCwd ? `pid ${ownerPid} (cwd: ${ownerCwd})` : `pid ${ownerPid} (cwd unavailable)`;
3134
+ if (ownerIsInWorkspace === false) {
3135
+ throw new Error(`Runtime service "${serviceName}" could not start because port ${identityPort} has a cross-workspace port conflict with ${ownerDescription}; requested workspace: ${serviceCwd}. Stop the other service or configure a different port.`);
3136
+ }
3137
+ throw new Error(`Runtime service "${serviceName}" could not start because port ${identityPort} is already in use by ${ownerDescription}`);
2212
3138
  }
2213
3139
  }
2214
3140
  await ensureServerWorkspaceLinksCurrent(serviceCwd, {
@@ -2240,16 +3166,7 @@ async function startLocalRuntimeService(input) {
2240
3166
  if (input.onLog)
2241
3167
  await input.onLog("stderr", `[service:${serviceName}] ${text}`);
2242
3168
  });
2243
- try {
2244
- await Promise.race([
2245
- waitForReadiness({ service: input.service, url }),
2246
- spawnErrorPromise,
2247
- ]);
2248
- }
2249
- catch (err) {
2250
- terminateChildProcess(child);
2251
- throw new Error(`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`);
2252
- }
3169
+ const nowIso = new Date().toISOString();
2253
3170
  const record = {
2254
3171
  id: stoppedReuseCandidate?.id ?? randomUUID(),
2255
3172
  companyId: input.agent.companyId,
@@ -2258,7 +3175,7 @@ async function startLocalRuntimeService(input) {
2258
3175
  executionWorkspaceId: input.executionWorkspaceId ?? null,
2259
3176
  issueId: input.issue?.id ?? null,
2260
3177
  serviceName,
2261
- status: "running",
3178
+ status: "starting",
2262
3179
  lifecycle,
2263
3180
  scopeType: input.scopeType,
2264
3181
  scopeId: input.scopeId,
@@ -2271,11 +3188,11 @@ async function startLocalRuntimeService(input) {
2271
3188
  providerRef: child.pid ? String(child.pid) : null,
2272
3189
  ownerAgentId: input.agent.id ?? null,
2273
3190
  startedByRunId,
2274
- lastUsedAt: new Date().toISOString(),
2275
- startedAt: new Date().toISOString(),
3191
+ lastUsedAt: nowIso,
3192
+ startedAt: nowIso,
2276
3193
  stoppedAt: null,
2277
3194
  stopPolicy,
2278
- healthStatus: "healthy",
3195
+ healthStatus: "unknown",
2279
3196
  reused: false,
2280
3197
  db: input.db,
2281
3198
  child,
@@ -2314,7 +3231,33 @@ async function startLocalRuntimeService(input) {
2314
3231
  },
2315
3232
  });
2316
3233
  }
2317
- return record;
3234
+ const readinessPromise = Promise.race([
3235
+ waitForReadiness({ service: input.service, serviceName, command, url }),
3236
+ spawnErrorPromise,
3237
+ ]).then(async () => {
3238
+ record.status = "running";
3239
+ record.healthStatus = "healthy";
3240
+ record.lastUsedAt = new Date().toISOString();
3241
+ record.stoppedAt = null;
3242
+ await touchLocalServiceRegistryRecord(record.serviceKey, {
3243
+ runtimeServiceId: record.id,
3244
+ lastSeenAt: record.lastUsedAt,
3245
+ });
3246
+ }).catch(async (err) => {
3247
+ terminateChildProcess(child);
3248
+ record.status = "stopped";
3249
+ record.healthStatus = "unhealthy";
3250
+ record.lastUsedAt = new Date().toISOString();
3251
+ record.stoppedAt = new Date().toISOString();
3252
+ await removeLocalServiceRegistryRecord(record.serviceKey).catch(() => undefined);
3253
+ throw new Error(`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`);
3254
+ });
3255
+ return { record, readiness: readinessPromise };
3256
+ }
3257
+ async function startLocalRuntimeService(input) {
3258
+ const started = await spawnLocalRuntimeService(input);
3259
+ await started.readiness;
3260
+ return started.record;
2318
3261
  }
2319
3262
  function scheduleIdleStop(record) {
2320
3263
  clearIdleTimer(record);
@@ -2533,16 +3476,10 @@ export async function ensureRuntimeServicesForRun(input) {
2533
3476
  }
2534
3477
  return refs;
2535
3478
  }
2536
- export async function startRuntimeServicesForWorkspaceControl(input) {
2537
- const rawServices = selectRuntimeServiceEntries({
2538
- config: input.config,
2539
- serviceIndex: input.serviceIndex,
2540
- respectDesiredStates: input.respectDesiredStates,
2541
- defaultDesiredState: readDesiredRuntimeState(input.config.desiredState) ?? "stopped",
2542
- serviceStates: readConfiguredServiceStates(input.config),
2543
- });
3479
+ async function startRuntimeServicesForWorkspaceControlUnlocked(input, rawServices, invocationId, persistenceDb = input.db, registryDb = input.db, options) {
2544
3480
  const refs = [];
2545
- const invocationId = input.invocationId ?? randomUUID();
3481
+ const pendingReadiness = [];
3482
+ const startedServiceIds = [];
2546
3483
  for (const service of rawServices) {
2547
3484
  const { scopeType, scopeId } = resolveServiceScopeId({
2548
3485
  service,
@@ -2572,15 +3509,13 @@ export async function startRuntimeServicesForWorkspaceControl(input) {
2572
3509
  runtimeServiceId: existing.id,
2573
3510
  lastSeenAt: existing.lastUsedAt,
2574
3511
  });
2575
- await persistRuntimeServiceRecord(input.db, existing);
3512
+ await persistRuntimeServiceRecord(persistenceDb, existing);
2576
3513
  refs.push(toRuntimeServiceRef(existing, { reused: true }));
2577
3514
  continue;
2578
3515
  }
2579
3516
  }
2580
- // Manually controlled services are not tied to a heartbeat run lifecycle, so they do not
2581
- // retain a run lease and never persist a startedByRunId foreign key.
2582
- const record = await startLocalRuntimeService({
2583
- db: input.db,
3517
+ const startInput = {
3518
+ db: persistenceDb,
2584
3519
  runId: invocationId,
2585
3520
  leaseRunId: null,
2586
3521
  startedByRunId: null,
@@ -2594,12 +3529,93 @@ export async function startRuntimeServicesForWorkspaceControl(input) {
2594
3529
  reuseKey,
2595
3530
  scopeType,
2596
3531
  scopeId,
3532
+ };
3533
+ // Manually controlled services are not tied to a heartbeat run lifecycle, so they do not
3534
+ // retain a run lease and never persist a startedByRunId foreign key.
3535
+ const started = options?.deferReadiness
3536
+ ? await spawnLocalRuntimeService(startInput)
3537
+ : {
3538
+ record: await startLocalRuntimeService(startInput),
3539
+ readiness: Promise.resolve(),
3540
+ };
3541
+ registerRuntimeService(registryDb, started.record);
3542
+ await persistRuntimeServiceRecord(persistenceDb, started.record);
3543
+ refs.push(toRuntimeServiceRef(started.record));
3544
+ if (options?.deferReadiness && !started.record.reused) {
3545
+ // Attach a rejection handler immediately; the caller awaits the same promise after
3546
+ // the DB transaction commits, but transaction failures may skip that wait path.
3547
+ started.readiness.catch(() => undefined);
3548
+ pendingReadiness.push(started);
3549
+ startedServiceIds.push(started.record.id);
3550
+ }
3551
+ }
3552
+ return { refs, pendingReadiness, startedServiceIds };
3553
+ }
3554
+ export async function startRuntimeServicesForWorkspaceControl(input) {
3555
+ const rawServices = selectRuntimeServiceEntries({
3556
+ config: input.config,
3557
+ serviceIndex: input.serviceIndex,
3558
+ respectDesiredStates: input.respectDesiredStates,
3559
+ defaultDesiredState: readDesiredRuntimeState(input.config.desiredState) ?? "stopped",
3560
+ serviceStates: readConfiguredServiceStates(input.config),
3561
+ });
3562
+ const invocationId = input.invocationId ?? randomUUID();
3563
+ if (rawServices.length === 0 || !input.db || (!input.executionWorkspaceId && !input.workspace.workspaceId)) {
3564
+ const batch = await startRuntimeServicesForWorkspaceControlUnlocked(input, rawServices, invocationId);
3565
+ return batch.refs;
3566
+ }
3567
+ let startBatch = {
3568
+ refs: [],
3569
+ pendingReadiness: [],
3570
+ startedServiceIds: [],
3571
+ };
3572
+ try {
3573
+ await input.db.transaction(async (tx) => {
3574
+ const txDb = tx;
3575
+ if (input.executionWorkspaceId) {
3576
+ const [lockedExecutionWorkspace] = await tx
3577
+ .select({ id: executionWorkspaces.id })
3578
+ .from(executionWorkspaces)
3579
+ .where(and(eq(executionWorkspaces.id, input.executionWorkspaceId), eq(executionWorkspaces.companyId, input.actor.companyId)))
3580
+ .for("update");
3581
+ if (!lockedExecutionWorkspace)
3582
+ throw new Error("Execution workspace not found before starting runtime services");
3583
+ }
3584
+ if (input.workspace.workspaceId) {
3585
+ const [lockedProjectWorkspace] = await tx
3586
+ .select({ id: projectWorkspaces.id })
3587
+ .from(projectWorkspaces)
3588
+ .where(and(eq(projectWorkspaces.id, input.workspace.workspaceId), eq(projectWorkspaces.companyId, input.actor.companyId)))
3589
+ .for("update");
3590
+ if (!lockedProjectWorkspace)
3591
+ throw new Error("Project workspace not found before starting runtime services");
3592
+ }
3593
+ // Branch reconciliation takes these same parent row locks before mutating
3594
+ // a recorded branch. Persisting a `starting` service row before commit closes
3595
+ // the process-start window without holding the DB transaction for readiness.
3596
+ startBatch = await startRuntimeServicesForWorkspaceControlUnlocked({ ...input, db: txDb }, rawServices, invocationId, txDb, input.db, { deferReadiness: true });
3597
+ });
3598
+ for (const pending of startBatch.pendingReadiness) {
3599
+ try {
3600
+ await pending.readiness;
3601
+ await persistRuntimeServiceRecord(input.db, pending.record);
3602
+ }
3603
+ catch (error) {
3604
+ await persistRuntimeServiceRecord(input.db, pending.record).catch(() => undefined);
3605
+ throw error;
3606
+ }
3607
+ }
3608
+ return startBatch.refs.map((ref) => {
3609
+ const record = runtimeServicesById.get(ref.id);
3610
+ return record ? toRuntimeServiceRef(record, { reused: ref.reused }) : ref;
2597
3611
  });
2598
- registerRuntimeService(input.db, record);
2599
- await persistRuntimeServiceRecord(input.db, record);
2600
- refs.push(toRuntimeServiceRef(record));
2601
3612
  }
2602
- return refs;
3613
+ catch (error) {
3614
+ for (const serviceId of startBatch.startedServiceIds) {
3615
+ await stopRuntimeService(serviceId).catch(() => undefined);
3616
+ }
3617
+ throw error;
3618
+ }
2603
3619
  }
2604
3620
  export async function releaseRuntimeServicesForRun(runId) {
2605
3621
  const acquired = runtimeServiceLeasesByRun.get(runId) ?? [];
@@ -3006,6 +4022,9 @@ export function buildWorkspaceReadyComment(input) {
3006
4022
  if (input.workspace.worktreePath && input.workspace.worktreePath !== input.workspace.cwd) {
3007
4023
  lines.push(`- Worktree: \`${input.workspace.worktreePath}\``);
3008
4024
  }
4025
+ for (const warning of input.workspace.warnings) {
4026
+ lines.push(`- Warning: ${warning}`);
4027
+ }
3009
4028
  for (const service of input.runtimeServices) {
3010
4029
  const detail = service.url ? `${service.serviceName}: ${service.url}` : `${service.serviceName}: running`;
3011
4030
  const suffix = service.reused ? " (reused)" : "";