@paperclipai/server 0.3.0 → 0.3.1-canary.1

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 (178) hide show
  1. package/dist/adapters/registry.d.ts.map +1 -1
  2. package/dist/adapters/registry.js +12 -0
  3. package/dist/adapters/registry.js.map +1 -1
  4. package/dist/app.d.ts +1 -0
  5. package/dist/app.d.ts.map +1 -1
  6. package/dist/app.js +9 -2
  7. package/dist/app.js.map +1 -1
  8. package/dist/attachment-types.d.ts +33 -0
  9. package/dist/attachment-types.d.ts.map +1 -0
  10. package/dist/attachment-types.js +61 -0
  11. package/dist/attachment-types.js.map +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +12 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/log-redaction.d.ts +10 -0
  16. package/dist/log-redaction.d.ts.map +1 -0
  17. package/dist/log-redaction.js +110 -0
  18. package/dist/log-redaction.js.map +1 -0
  19. package/dist/middleware/logger.d.ts.map +1 -1
  20. package/dist/middleware/logger.js +1 -0
  21. package/dist/middleware/logger.js.map +1 -1
  22. package/dist/routes/activity.d.ts.map +1 -1
  23. package/dist/routes/activity.js +12 -21
  24. package/dist/routes/activity.js.map +1 -1
  25. package/dist/routes/agents.d.ts.map +1 -1
  26. package/dist/routes/agents.js +102 -3
  27. package/dist/routes/agents.js.map +1 -1
  28. package/dist/routes/approvals.d.ts.map +1 -1
  29. package/dist/routes/approvals.js +87 -83
  30. package/dist/routes/approvals.js.map +1 -1
  31. package/dist/routes/assets.d.ts.map +1 -1
  32. package/dist/routes/assets.js +5 -12
  33. package/dist/routes/assets.js.map +1 -1
  34. package/dist/routes/issues.d.ts.map +1 -1
  35. package/dist/routes/issues.js +16 -11
  36. package/dist/routes/issues.js.map +1 -1
  37. package/dist/routes/sidebar-badges.d.ts.map +1 -1
  38. package/dist/routes/sidebar-badges.js +1 -4
  39. package/dist/routes/sidebar-badges.js.map +1 -1
  40. package/dist/services/activity-log.d.ts.map +1 -1
  41. package/dist/services/activity-log.js +4 -2
  42. package/dist/services/activity-log.js.map +1 -1
  43. package/dist/services/approvals.d.ts +30 -24
  44. package/dist/services/approvals.d.ts.map +1 -1
  45. package/dist/services/approvals.js +51 -42
  46. package/dist/services/approvals.js.map +1 -1
  47. package/dist/services/company-portability.d.ts.map +1 -1
  48. package/dist/services/company-portability.js +5 -1
  49. package/dist/services/company-portability.js.map +1 -1
  50. package/dist/services/dashboard.d.ts +0 -1
  51. package/dist/services/dashboard.d.ts.map +1 -1
  52. package/dist/services/dashboard.js +0 -7
  53. package/dist/services/dashboard.js.map +1 -1
  54. package/dist/services/execution-workspace-policy.d.ts +19 -0
  55. package/dist/services/execution-workspace-policy.d.ts.map +1 -0
  56. package/dist/services/execution-workspace-policy.js +117 -0
  57. package/dist/services/execution-workspace-policy.js.map +1 -0
  58. package/dist/services/goals.d.ts +26 -0
  59. package/dist/services/goals.d.ts.map +1 -1
  60. package/dist/services/goals.js +26 -1
  61. package/dist/services/goals.js.map +1 -1
  62. package/dist/services/heartbeat-run-summary.d.ts +2 -0
  63. package/dist/services/heartbeat-run-summary.d.ts.map +1 -0
  64. package/dist/services/heartbeat-run-summary.js +30 -0
  65. package/dist/services/heartbeat-run-summary.js.map +1 -0
  66. package/dist/services/heartbeat.d.ts +30 -996
  67. package/dist/services/heartbeat.d.ts.map +1 -1
  68. package/dist/services/heartbeat.js +236 -47
  69. package/dist/services/heartbeat.js.map +1 -1
  70. package/dist/services/index.d.ts +1 -0
  71. package/dist/services/index.d.ts.map +1 -1
  72. package/dist/services/index.js +1 -0
  73. package/dist/services/index.js.map +1 -1
  74. package/dist/services/issue-goal-fallback.d.ts +15 -0
  75. package/dist/services/issue-goal-fallback.d.ts.map +1 -0
  76. package/dist/services/issue-goal-fallback.js +15 -0
  77. package/dist/services/issue-goal-fallback.js.map +1 -0
  78. package/dist/services/issues.d.ts +5 -278
  79. package/dist/services/issues.d.ts.map +1 -1
  80. package/dist/services/issues.js +52 -14
  81. package/dist/services/issues.js.map +1 -1
  82. package/dist/services/projects.d.ts +4 -2
  83. package/dist/services/projects.d.ts.map +1 -1
  84. package/dist/services/projects.js +49 -6
  85. package/dist/services/projects.js.map +1 -1
  86. package/dist/services/workspace-runtime.d.ts +126 -0
  87. package/dist/services/workspace-runtime.d.ts.map +1 -0
  88. package/dist/services/workspace-runtime.js +852 -0
  89. package/dist/services/workspace-runtime.js.map +1 -0
  90. package/dist/ui-branding.d.ts +4 -0
  91. package/dist/ui-branding.d.ts.map +1 -0
  92. package/dist/ui-branding.js +37 -0
  93. package/dist/ui-branding.js.map +1 -0
  94. package/package.json +11 -10
  95. package/skills/paperclip/SKILL.md +23 -19
  96. package/ui-dist/assets/{_basePickBy-BYrQlacK.js → _basePickBy-B0xbZITw.js} +1 -1
  97. package/ui-dist/assets/{_baseUniq-DOSawgF3.js → _baseUniq-Cfd5u3qc.js} +1 -1
  98. package/ui-dist/assets/{arc-CG7T0hfG.js → arc-7Xbu8tBF.js} +1 -1
  99. package/ui-dist/assets/{architectureDiagram-VXUJARFQ-Bcn7ytDO.js → architectureDiagram-VXUJARFQ-C8F7ZRYc.js} +1 -1
  100. package/ui-dist/assets/{blockDiagram-VD42YOAC-BQGrx2lv.js → blockDiagram-VD42YOAC-Dp08a65A.js} +1 -1
  101. package/ui-dist/assets/{c4Diagram-YG6GDRKO-owH9Kb3t.js → c4Diagram-YG6GDRKO-BJfslTgZ.js} +1 -1
  102. package/ui-dist/assets/channel-BViQDbSq.js +1 -0
  103. package/ui-dist/assets/{chunk-4BX2VUAB-DY1UIe4g.js → chunk-4BX2VUAB-BhaYWH7e.js} +1 -1
  104. package/ui-dist/assets/{chunk-55IACEB6-CnWFPfPQ.js → chunk-55IACEB6-BOzJUsYW.js} +1 -1
  105. package/ui-dist/assets/{chunk-B4BG7PRW-DhlLW80l.js → chunk-B4BG7PRW-CbVcziyE.js} +1 -1
  106. package/ui-dist/assets/{chunk-DI55MBZ5-DPt7dj6c.js → chunk-DI55MBZ5-DghPtP3y.js} +1 -1
  107. package/ui-dist/assets/{chunk-FMBD7UC4-GQwzgYa4.js → chunk-FMBD7UC4-CB095Kfn.js} +1 -1
  108. package/ui-dist/assets/{chunk-QN33PNHL-BFHLVk5s.js → chunk-QN33PNHL-DfpzfDqJ.js} +1 -1
  109. package/ui-dist/assets/{chunk-QZHKN3VN-CLe3KEAf.js → chunk-QZHKN3VN-Df3d5z1y.js} +1 -1
  110. package/ui-dist/assets/{chunk-TZMSLE5B-BhccYB4e.js → chunk-TZMSLE5B-B6tX6bZI.js} +1 -1
  111. package/ui-dist/assets/classDiagram-2ON5EDUG-CjMCmxMT.js +1 -0
  112. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-CjMCmxMT.js +1 -0
  113. package/ui-dist/assets/clone-Dbn9wtE1.js +1 -0
  114. package/ui-dist/assets/{cose-bilkent-S5V4N54A-DqECYL1w.js → cose-bilkent-S5V4N54A-38wg_s9V.js} +1 -1
  115. package/ui-dist/assets/{dagre-6UL2VRFP-DXeQqIJ2.js → dagre-6UL2VRFP-DPVj3XLS.js} +1 -1
  116. package/ui-dist/assets/{diagram-PSM6KHXK-DNu3Ctuy.js → diagram-PSM6KHXK-BJh89zUA.js} +1 -1
  117. package/ui-dist/assets/{diagram-QEK2KX5R-1wUR_z9S.js → diagram-QEK2KX5R-Bw0m_j10.js} +1 -1
  118. package/ui-dist/assets/{diagram-S2PKOQOG-D3IK8rZb.js → diagram-S2PKOQOG-D7_M2F3u.js} +1 -1
  119. package/ui-dist/assets/{erDiagram-Q2GNP2WA-DU3L0RbU.js → erDiagram-Q2GNP2WA-CLsjqTWP.js} +1 -1
  120. package/ui-dist/assets/{flowDiagram-NV44I4VS-CN46A5Ez.js → flowDiagram-NV44I4VS-BZZ7ezVB.js} +1 -1
  121. package/ui-dist/assets/{ganttDiagram-JELNMOA3-DGbOi1Wz.js → ganttDiagram-JELNMOA3-KzsFuwBt.js} +1 -1
  122. package/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-D98N7SOj.js → gitGraphDiagram-V2S2FVAM-DSHtY7Vu.js} +1 -1
  123. package/ui-dist/assets/{graph-Cf7LCNJy.js → graph-luIG1UAS.js} +1 -1
  124. package/ui-dist/assets/{index-CP1BgxcV.js → index-00kuG4sI.js} +1 -1
  125. package/ui-dist/assets/{index-DiXE2gv-.js → index-1gX09-Fl.js} +1 -1
  126. package/ui-dist/assets/{index-CqG5WZHq.js → index-B1ZMzzs0.js} +1 -1
  127. package/ui-dist/assets/index-BHP9dico.js +1006 -0
  128. package/ui-dist/assets/{index-cx0y6-1h.js → index-B_3g3Rie.js} +1 -1
  129. package/ui-dist/assets/{index-CI56poQD.js → index-BeeKMqNU.js} +1 -1
  130. package/ui-dist/assets/{index-BfG2u5u0.js → index-BfB4lKJN.js} +1 -1
  131. package/ui-dist/assets/index-BfNaDZnn.css +1 -0
  132. package/ui-dist/assets/{index-DZdNKByU.js → index-Bv4xCjxl.js} +1 -1
  133. package/ui-dist/assets/{index--K1VLoF-.js → index-C2SZYIDA.js} +1 -1
  134. package/ui-dist/assets/{index-DB5nKqAA.js → index-C8XyGAr9.js} +1 -1
  135. package/ui-dist/assets/{index-C2-SE7P0.js → index-CaR9XM4h.js} +1 -1
  136. package/ui-dist/assets/{index-DujThSls.js → index-CbGtsjW7.js} +1 -1
  137. package/ui-dist/assets/{index-D0EsfNYg.js → index-CeDCs_2i.js} +1 -1
  138. package/ui-dist/assets/{index-OkxoZoQy.js → index-DBy0vJy3.js} +1 -1
  139. package/ui-dist/assets/{index-BoAYxRAO.js → index-DJf8diAA.js} +1 -1
  140. package/ui-dist/assets/{index-N1SX_i0z.js → index-DcfLFstG.js} +1 -1
  141. package/ui-dist/assets/{index-DXgtGequ.js → index-DnIPDZLp.js} +1 -1
  142. package/ui-dist/assets/{index-Cick_QSL.js → index-DoTq-BeR.js} +1 -1
  143. package/ui-dist/assets/{index-0BSerEC2.js → index-Ds7vLTSK.js} +1 -1
  144. package/ui-dist/assets/{index-Beb2ZlSv.js → index-RUBFVv6t.js} +1 -1
  145. package/ui-dist/assets/{index-TFF7cXd7.js → index-itc7BfMy.js} +1 -1
  146. package/ui-dist/assets/{index-BD6My-aI.js → index-nFjLambq.js} +1 -1
  147. package/ui-dist/assets/{index-B-xuGUs-.js → index-uR3zjYaD.js} +1 -1
  148. package/ui-dist/assets/{infoDiagram-HS3SLOUP-D_b1CK0Y.js → infoDiagram-HS3SLOUP-CDEfWpme.js} +1 -1
  149. package/ui-dist/assets/{journeyDiagram-XKPGCS4Q-4oCVXUve.js → journeyDiagram-XKPGCS4Q-i7nsbg_Y.js} +1 -1
  150. package/ui-dist/assets/{kanban-definition-3W4ZIXB7-0VcjP_qf.js → kanban-definition-3W4ZIXB7-DDlx1qVE.js} +1 -1
  151. package/ui-dist/assets/{layout-BQcYXlNv.js → layout-C7AtmJzX.js} +1 -1
  152. package/ui-dist/assets/{linear-nz0Lfiys.js → linear-C5jHT-WP.js} +1 -1
  153. package/ui-dist/assets/{mermaid.core-BaxvgwjG.js → mermaid.core-C8YQ4fcY.js} +4 -4
  154. package/ui-dist/assets/{mindmap-definition-VGOIOE7T-BnW6nEhl.js → mindmap-definition-VGOIOE7T-B9m9PuUg.js} +1 -1
  155. package/ui-dist/assets/{pieDiagram-ADFJNKIX-O1tvU_18.js → pieDiagram-ADFJNKIX-SvKywCSE.js} +1 -1
  156. package/ui-dist/assets/{quadrantDiagram-AYHSOK5B-BfM2aQbf.js → quadrantDiagram-AYHSOK5B-QfBPm7Y1.js} +1 -1
  157. package/ui-dist/assets/{requirementDiagram-UZGBJVZJ-rXVZupag.js → requirementDiagram-UZGBJVZJ-DnfQQuwi.js} +1 -1
  158. package/ui-dist/assets/{sankeyDiagram-TZEHDZUN-BNgaPVo6.js → sankeyDiagram-TZEHDZUN-DvLkjzIW.js} +1 -1
  159. package/ui-dist/assets/{sequenceDiagram-WL72ISMW--KnZ0qRV.js → sequenceDiagram-WL72ISMW-5Lq2rWBc.js} +1 -1
  160. package/ui-dist/assets/{stateDiagram-FKZM4ZOC-DlGdC88b.js → stateDiagram-FKZM4ZOC-DcYPHDyi.js} +1 -1
  161. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-nv50YYrl.js +1 -0
  162. package/ui-dist/assets/{timeline-definition-IT6M3QCI-CCwriy0-.js → timeline-definition-IT6M3QCI-lrN4JmWa.js} +1 -1
  163. package/ui-dist/assets/{treemap-GDKQZRPO-C-79yojr.js → treemap-GDKQZRPO-DFJKIBTQ.js} +1 -1
  164. package/ui-dist/assets/{xychartDiagram-PRI3JC2R-Dj0jcMBZ.js → xychartDiagram-PRI3JC2R-CT47vtdm.js} +1 -1
  165. package/ui-dist/index.html +4 -2
  166. package/ui-dist/worktree-favicon-16x16.png +0 -0
  167. package/ui-dist/worktree-favicon-32x32.png +0 -0
  168. package/ui-dist/worktree-favicon.ico +0 -0
  169. package/ui-dist/worktree-favicon.svg +9 -0
  170. package/skills/release/SKILL.md +0 -261
  171. package/skills/release-changelog/SKILL.md +0 -178
  172. package/ui-dist/assets/channel-DdXqC9Qy.js +0 -1
  173. package/ui-dist/assets/classDiagram-2ON5EDUG-ZV36NLFv.js +0 -1
  174. package/ui-dist/assets/classDiagram-v2-WZHVMYZB-ZV36NLFv.js +0 -1
  175. package/ui-dist/assets/clone-CvxIjPQa.js +0 -1
  176. package/ui-dist/assets/index-BYw6Loly.js +0 -900
  177. package/ui-dist/assets/index-nfAtmpEH.css +0 -1
  178. package/ui-dist/assets/stateDiagram-v2-4FDKWEC3-CuVnpOfP.js +0 -1
@@ -0,0 +1,852 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import net from "node:net";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import path from "node:path";
6
+ import { setTimeout as delay } from "node:timers/promises";
7
+ import { workspaceRuntimeServices } from "@paperclipai/db";
8
+ import { and, desc, eq, inArray } from "drizzle-orm";
9
+ import { asNumber, asString, parseObject, renderTemplate } from "../adapters/utils.js";
10
+ import { resolveHomeAwarePath } from "../home-paths.js";
11
+ const runtimeServicesById = new Map();
12
+ const runtimeServicesByReuseKey = new Map();
13
+ const runtimeServiceLeasesByRun = new Map();
14
+ function stableStringify(value) {
15
+ if (Array.isArray(value)) {
16
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
17
+ }
18
+ if (value && typeof value === "object") {
19
+ const rec = value;
20
+ return `{${Object.keys(rec).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(rec[key])}`).join(",")}}`;
21
+ }
22
+ return JSON.stringify(value);
23
+ }
24
+ function stableRuntimeServiceId(input) {
25
+ if (input.reportId)
26
+ return input.reportId;
27
+ const digest = createHash("sha256")
28
+ .update(stableStringify({
29
+ adapterType: input.adapterType,
30
+ runId: input.runId,
31
+ scopeType: input.scopeType,
32
+ scopeId: input.scopeId,
33
+ serviceName: input.serviceName,
34
+ providerRef: input.providerRef,
35
+ reuseKey: input.reuseKey,
36
+ }))
37
+ .digest("hex")
38
+ .slice(0, 32);
39
+ return `${input.adapterType}-${digest}`;
40
+ }
41
+ function toRuntimeServiceRef(record, overrides) {
42
+ return {
43
+ id: record.id,
44
+ companyId: record.companyId,
45
+ projectId: record.projectId,
46
+ projectWorkspaceId: record.projectWorkspaceId,
47
+ issueId: record.issueId,
48
+ serviceName: record.serviceName,
49
+ status: record.status,
50
+ lifecycle: record.lifecycle,
51
+ scopeType: record.scopeType,
52
+ scopeId: record.scopeId,
53
+ reuseKey: record.reuseKey,
54
+ command: record.command,
55
+ cwd: record.cwd,
56
+ port: record.port,
57
+ url: record.url,
58
+ provider: record.provider,
59
+ providerRef: record.providerRef,
60
+ ownerAgentId: record.ownerAgentId,
61
+ startedByRunId: record.startedByRunId,
62
+ lastUsedAt: record.lastUsedAt,
63
+ startedAt: record.startedAt,
64
+ stoppedAt: record.stoppedAt,
65
+ stopPolicy: record.stopPolicy,
66
+ healthStatus: record.healthStatus,
67
+ reused: record.reused,
68
+ ...overrides,
69
+ };
70
+ }
71
+ function sanitizeSlugPart(value, fallback) {
72
+ const raw = (value ?? "").trim().toLowerCase();
73
+ const normalized = raw
74
+ .replace(/[^a-z0-9/_-]+/g, "-")
75
+ .replace(/-+/g, "-")
76
+ .replace(/^[-/]+|[-/]+$/g, "");
77
+ return normalized.length > 0 ? normalized : fallback;
78
+ }
79
+ function renderWorkspaceTemplate(template, input) {
80
+ const issueIdentifier = input.issue?.identifier ?? input.issue?.id ?? "issue";
81
+ const slug = sanitizeSlugPart(input.issue?.title, sanitizeSlugPart(issueIdentifier, "issue"));
82
+ return renderTemplate(template, {
83
+ issue: {
84
+ id: input.issue?.id ?? "",
85
+ identifier: input.issue?.identifier ?? "",
86
+ title: input.issue?.title ?? "",
87
+ },
88
+ agent: {
89
+ id: input.agent.id,
90
+ name: input.agent.name,
91
+ },
92
+ project: {
93
+ id: input.projectId ?? "",
94
+ },
95
+ workspace: {
96
+ repoRef: input.repoRef ?? "",
97
+ },
98
+ slug,
99
+ });
100
+ }
101
+ function sanitizeBranchName(value) {
102
+ return value
103
+ .trim()
104
+ .replace(/[^A-Za-z0-9._/-]+/g, "-")
105
+ .replace(/-+/g, "-")
106
+ .replace(/^[-/.]+|[-/.]+$/g, "")
107
+ .slice(0, 120) || "paperclip-work";
108
+ }
109
+ function isAbsolutePath(value) {
110
+ return path.isAbsolute(value) || value.startsWith("~");
111
+ }
112
+ function resolveConfiguredPath(value, baseDir) {
113
+ if (isAbsolutePath(value)) {
114
+ return resolveHomeAwarePath(value);
115
+ }
116
+ return path.resolve(baseDir, value);
117
+ }
118
+ async function runGit(args, cwd) {
119
+ const proc = await new Promise((resolve, reject) => {
120
+ const child = spawn("git", args, {
121
+ cwd,
122
+ stdio: ["ignore", "pipe", "pipe"],
123
+ env: process.env,
124
+ });
125
+ let stdout = "";
126
+ let stderr = "";
127
+ child.stdout?.on("data", (chunk) => {
128
+ stdout += String(chunk);
129
+ });
130
+ child.stderr?.on("data", (chunk) => {
131
+ stderr += String(chunk);
132
+ });
133
+ child.on("error", reject);
134
+ child.on("close", (code) => resolve({ stdout, stderr, code }));
135
+ });
136
+ if (proc.code !== 0) {
137
+ throw new Error(proc.stderr.trim() || proc.stdout.trim() || `git ${args.join(" ")} failed`);
138
+ }
139
+ return proc.stdout.trim();
140
+ }
141
+ async function directoryExists(value) {
142
+ return fs.stat(value).then((stats) => stats.isDirectory()).catch(() => false);
143
+ }
144
+ function buildWorkspaceCommandEnv(input) {
145
+ const env = { ...process.env };
146
+ env.PAPERCLIP_WORKSPACE_CWD = input.worktreePath;
147
+ env.PAPERCLIP_WORKSPACE_PATH = input.worktreePath;
148
+ env.PAPERCLIP_WORKSPACE_WORKTREE_PATH = input.worktreePath;
149
+ env.PAPERCLIP_WORKSPACE_BRANCH = input.branchName;
150
+ env.PAPERCLIP_WORKSPACE_BASE_CWD = input.base.baseCwd;
151
+ env.PAPERCLIP_WORKSPACE_REPO_ROOT = input.repoRoot;
152
+ env.PAPERCLIP_WORKSPACE_SOURCE = input.base.source;
153
+ env.PAPERCLIP_WORKSPACE_REPO_REF = input.base.repoRef ?? "";
154
+ env.PAPERCLIP_WORKSPACE_REPO_URL = input.base.repoUrl ?? "";
155
+ env.PAPERCLIP_WORKSPACE_CREATED = input.created ? "true" : "false";
156
+ env.PAPERCLIP_PROJECT_ID = input.base.projectId ?? "";
157
+ env.PAPERCLIP_PROJECT_WORKSPACE_ID = input.base.workspaceId ?? "";
158
+ env.PAPERCLIP_AGENT_ID = input.agent.id;
159
+ env.PAPERCLIP_AGENT_NAME = input.agent.name;
160
+ env.PAPERCLIP_COMPANY_ID = input.agent.companyId;
161
+ env.PAPERCLIP_ISSUE_ID = input.issue?.id ?? "";
162
+ env.PAPERCLIP_ISSUE_IDENTIFIER = input.issue?.identifier ?? "";
163
+ env.PAPERCLIP_ISSUE_TITLE = input.issue?.title ?? "";
164
+ return env;
165
+ }
166
+ async function runWorkspaceCommand(input) {
167
+ const shell = process.env.SHELL?.trim() || "/bin/sh";
168
+ const proc = await new Promise((resolve, reject) => {
169
+ const child = spawn(shell, ["-c", input.command], {
170
+ cwd: input.cwd,
171
+ env: input.env,
172
+ stdio: ["ignore", "pipe", "pipe"],
173
+ });
174
+ let stdout = "";
175
+ let stderr = "";
176
+ child.stdout?.on("data", (chunk) => {
177
+ stdout += String(chunk);
178
+ });
179
+ child.stderr?.on("data", (chunk) => {
180
+ stderr += String(chunk);
181
+ });
182
+ child.on("error", reject);
183
+ child.on("close", (code) => resolve({ stdout, stderr, code }));
184
+ });
185
+ if (proc.code === 0)
186
+ return;
187
+ const details = [proc.stderr.trim(), proc.stdout.trim()].filter(Boolean).join("\n");
188
+ throw new Error(details.length > 0
189
+ ? `${input.label} failed: ${details}`
190
+ : `${input.label} failed with exit code ${proc.code ?? -1}`);
191
+ }
192
+ async function provisionExecutionWorktree(input) {
193
+ const provisionCommand = asString(input.strategy.provisionCommand, "").trim();
194
+ if (!provisionCommand)
195
+ return;
196
+ await runWorkspaceCommand({
197
+ command: provisionCommand,
198
+ cwd: input.worktreePath,
199
+ env: buildWorkspaceCommandEnv({
200
+ base: input.base,
201
+ repoRoot: input.repoRoot,
202
+ worktreePath: input.worktreePath,
203
+ branchName: input.branchName,
204
+ issue: input.issue,
205
+ agent: input.agent,
206
+ created: input.created,
207
+ }),
208
+ label: `Execution workspace provision command "${provisionCommand}"`,
209
+ });
210
+ }
211
+ export async function realizeExecutionWorkspace(input) {
212
+ const rawStrategy = parseObject(input.config.workspaceStrategy);
213
+ const strategyType = asString(rawStrategy.type, "project_primary");
214
+ if (strategyType !== "git_worktree") {
215
+ return {
216
+ ...input.base,
217
+ strategy: "project_primary",
218
+ cwd: input.base.baseCwd,
219
+ branchName: null,
220
+ worktreePath: null,
221
+ warnings: [],
222
+ created: false,
223
+ };
224
+ }
225
+ const repoRoot = await runGit(["rev-parse", "--show-toplevel"], input.base.baseCwd);
226
+ const branchTemplate = asString(rawStrategy.branchTemplate, "{{issue.identifier}}-{{slug}}");
227
+ const renderedBranch = renderWorkspaceTemplate(branchTemplate, {
228
+ issue: input.issue,
229
+ agent: input.agent,
230
+ projectId: input.base.projectId,
231
+ repoRef: input.base.repoRef,
232
+ });
233
+ const branchName = sanitizeBranchName(renderedBranch);
234
+ const configuredParentDir = asString(rawStrategy.worktreeParentDir, "");
235
+ const worktreeParentDir = configuredParentDir
236
+ ? resolveConfiguredPath(configuredParentDir, repoRoot)
237
+ : path.join(repoRoot, ".paperclip", "worktrees");
238
+ const worktreePath = path.join(worktreeParentDir, branchName);
239
+ const baseRef = asString(rawStrategy.baseRef, input.base.repoRef ?? "HEAD");
240
+ await fs.mkdir(worktreeParentDir, { recursive: true });
241
+ const existingWorktree = await directoryExists(worktreePath);
242
+ if (existingWorktree) {
243
+ const existingGitDir = await runGit(["rev-parse", "--git-dir"], worktreePath).catch(() => null);
244
+ if (existingGitDir) {
245
+ await provisionExecutionWorktree({
246
+ strategy: rawStrategy,
247
+ base: input.base,
248
+ repoRoot,
249
+ worktreePath,
250
+ branchName,
251
+ issue: input.issue,
252
+ agent: input.agent,
253
+ created: false,
254
+ });
255
+ return {
256
+ ...input.base,
257
+ strategy: "git_worktree",
258
+ cwd: worktreePath,
259
+ branchName,
260
+ worktreePath,
261
+ warnings: [],
262
+ created: false,
263
+ };
264
+ }
265
+ throw new Error(`Configured worktree path "${worktreePath}" already exists and is not a git worktree.`);
266
+ }
267
+ await runGit(["worktree", "add", "-B", branchName, worktreePath, baseRef], repoRoot);
268
+ await provisionExecutionWorktree({
269
+ strategy: rawStrategy,
270
+ base: input.base,
271
+ repoRoot,
272
+ worktreePath,
273
+ branchName,
274
+ issue: input.issue,
275
+ agent: input.agent,
276
+ created: true,
277
+ });
278
+ return {
279
+ ...input.base,
280
+ strategy: "git_worktree",
281
+ cwd: worktreePath,
282
+ branchName,
283
+ worktreePath,
284
+ warnings: [],
285
+ created: true,
286
+ };
287
+ }
288
+ async function allocatePort() {
289
+ return await new Promise((resolve, reject) => {
290
+ const server = net.createServer();
291
+ server.listen(0, "127.0.0.1", () => {
292
+ const address = server.address();
293
+ server.close((err) => {
294
+ if (err) {
295
+ reject(err);
296
+ return;
297
+ }
298
+ if (!address || typeof address === "string") {
299
+ reject(new Error("Failed to allocate port"));
300
+ return;
301
+ }
302
+ resolve(address.port);
303
+ });
304
+ });
305
+ server.on("error", reject);
306
+ });
307
+ }
308
+ function buildTemplateData(input) {
309
+ return {
310
+ workspace: {
311
+ cwd: input.workspace.cwd,
312
+ branchName: input.workspace.branchName ?? "",
313
+ worktreePath: input.workspace.worktreePath ?? "",
314
+ repoUrl: input.workspace.repoUrl ?? "",
315
+ repoRef: input.workspace.repoRef ?? "",
316
+ env: input.adapterEnv,
317
+ },
318
+ issue: {
319
+ id: input.issue?.id ?? "",
320
+ identifier: input.issue?.identifier ?? "",
321
+ title: input.issue?.title ?? "",
322
+ },
323
+ agent: {
324
+ id: input.agent.id,
325
+ name: input.agent.name,
326
+ },
327
+ port: input.port ?? "",
328
+ };
329
+ }
330
+ function resolveServiceScopeId(input) {
331
+ const scopeTypeRaw = asString(input.service.reuseScope, input.service.lifecycle === "shared" ? "project_workspace" : "run");
332
+ const scopeType = scopeTypeRaw === "project_workspace" ||
333
+ scopeTypeRaw === "execution_workspace" ||
334
+ scopeTypeRaw === "agent"
335
+ ? scopeTypeRaw
336
+ : "run";
337
+ if (scopeType === "project_workspace")
338
+ return { scopeType, scopeId: input.workspace.workspaceId ?? input.workspace.projectId };
339
+ if (scopeType === "execution_workspace")
340
+ return { scopeType, scopeId: input.workspace.cwd };
341
+ if (scopeType === "agent")
342
+ return { scopeType, scopeId: input.agent.id };
343
+ return { scopeType: "run", scopeId: input.runId };
344
+ }
345
+ async function waitForReadiness(input) {
346
+ const readiness = parseObject(input.service.readiness);
347
+ const readinessType = asString(readiness.type, "");
348
+ if (readinessType !== "http" || !input.url)
349
+ return;
350
+ const timeoutSec = Math.max(1, asNumber(readiness.timeoutSec, 30));
351
+ const intervalMs = Math.max(100, asNumber(readiness.intervalMs, 500));
352
+ const deadline = Date.now() + timeoutSec * 1000;
353
+ let lastError = "service did not become ready";
354
+ while (Date.now() < deadline) {
355
+ try {
356
+ const response = await fetch(input.url);
357
+ if (response.ok)
358
+ return;
359
+ lastError = `received HTTP ${response.status}`;
360
+ }
361
+ catch (err) {
362
+ lastError = err instanceof Error ? err.message : String(err);
363
+ }
364
+ await delay(intervalMs);
365
+ }
366
+ throw new Error(`Readiness check failed for ${input.url}: ${lastError}`);
367
+ }
368
+ function toPersistedWorkspaceRuntimeService(record) {
369
+ return {
370
+ id: record.id,
371
+ companyId: record.companyId,
372
+ projectId: record.projectId,
373
+ projectWorkspaceId: record.projectWorkspaceId,
374
+ issueId: record.issueId,
375
+ scopeType: record.scopeType,
376
+ scopeId: record.scopeId,
377
+ serviceName: record.serviceName,
378
+ status: record.status,
379
+ lifecycle: record.lifecycle,
380
+ reuseKey: record.reuseKey,
381
+ command: record.command,
382
+ cwd: record.cwd,
383
+ port: record.port,
384
+ url: record.url,
385
+ provider: record.provider,
386
+ providerRef: record.providerRef,
387
+ ownerAgentId: record.ownerAgentId,
388
+ startedByRunId: record.startedByRunId,
389
+ lastUsedAt: new Date(record.lastUsedAt),
390
+ startedAt: new Date(record.startedAt),
391
+ stoppedAt: record.stoppedAt ? new Date(record.stoppedAt) : null,
392
+ stopPolicy: record.stopPolicy,
393
+ healthStatus: record.healthStatus,
394
+ updatedAt: new Date(),
395
+ };
396
+ }
397
+ async function persistRuntimeServiceRecord(db, record) {
398
+ if (!db)
399
+ return;
400
+ const values = toPersistedWorkspaceRuntimeService(record);
401
+ await db
402
+ .insert(workspaceRuntimeServices)
403
+ .values(values)
404
+ .onConflictDoUpdate({
405
+ target: workspaceRuntimeServices.id,
406
+ set: {
407
+ projectId: values.projectId,
408
+ projectWorkspaceId: values.projectWorkspaceId,
409
+ issueId: values.issueId,
410
+ scopeType: values.scopeType,
411
+ scopeId: values.scopeId,
412
+ serviceName: values.serviceName,
413
+ status: values.status,
414
+ lifecycle: values.lifecycle,
415
+ reuseKey: values.reuseKey,
416
+ command: values.command,
417
+ cwd: values.cwd,
418
+ port: values.port,
419
+ url: values.url,
420
+ provider: values.provider,
421
+ providerRef: values.providerRef,
422
+ ownerAgentId: values.ownerAgentId,
423
+ startedByRunId: values.startedByRunId,
424
+ lastUsedAt: values.lastUsedAt,
425
+ startedAt: values.startedAt,
426
+ stoppedAt: values.stoppedAt,
427
+ stopPolicy: values.stopPolicy,
428
+ healthStatus: values.healthStatus,
429
+ updatedAt: values.updatedAt,
430
+ },
431
+ });
432
+ }
433
+ function clearIdleTimer(record) {
434
+ if (!record.idleTimer)
435
+ return;
436
+ clearTimeout(record.idleTimer);
437
+ record.idleTimer = null;
438
+ }
439
+ export function normalizeAdapterManagedRuntimeServices(input) {
440
+ const nowIso = (input.now ?? new Date()).toISOString();
441
+ return input.reports.map((report) => {
442
+ const scopeType = report.scopeType ?? "run";
443
+ const scopeId = report.scopeId ??
444
+ (scopeType === "project_workspace"
445
+ ? input.workspace.workspaceId
446
+ : scopeType === "execution_workspace"
447
+ ? input.workspace.cwd
448
+ : scopeType === "agent"
449
+ ? input.agent.id
450
+ : input.runId) ??
451
+ null;
452
+ const serviceName = asString(report.serviceName, "").trim() || "service";
453
+ const status = report.status ?? "running";
454
+ const lifecycle = report.lifecycle ?? "ephemeral";
455
+ const healthStatus = report.healthStatus ??
456
+ (status === "running" ? "healthy" : status === "failed" ? "unhealthy" : "unknown");
457
+ return {
458
+ id: stableRuntimeServiceId({
459
+ adapterType: input.adapterType,
460
+ runId: input.runId,
461
+ scopeType,
462
+ scopeId,
463
+ serviceName,
464
+ reportId: report.id ?? null,
465
+ providerRef: report.providerRef ?? null,
466
+ reuseKey: report.reuseKey ?? null,
467
+ }),
468
+ companyId: input.agent.companyId,
469
+ projectId: report.projectId ?? input.workspace.projectId,
470
+ projectWorkspaceId: report.projectWorkspaceId ?? input.workspace.workspaceId,
471
+ issueId: report.issueId ?? input.issue?.id ?? null,
472
+ serviceName,
473
+ status,
474
+ lifecycle,
475
+ scopeType,
476
+ scopeId,
477
+ reuseKey: report.reuseKey ?? null,
478
+ command: report.command ?? null,
479
+ cwd: report.cwd ?? null,
480
+ port: report.port ?? null,
481
+ url: report.url ?? null,
482
+ provider: "adapter_managed",
483
+ providerRef: report.providerRef ?? null,
484
+ ownerAgentId: report.ownerAgentId ?? input.agent.id,
485
+ startedByRunId: input.runId,
486
+ lastUsedAt: nowIso,
487
+ startedAt: nowIso,
488
+ stoppedAt: status === "running" || status === "starting" ? null : nowIso,
489
+ stopPolicy: report.stopPolicy ?? null,
490
+ healthStatus,
491
+ reused: false,
492
+ };
493
+ });
494
+ }
495
+ async function startLocalRuntimeService(input) {
496
+ const serviceName = asString(input.service.name, "service");
497
+ const lifecycle = asString(input.service.lifecycle, "shared") === "ephemeral" ? "ephemeral" : "shared";
498
+ const command = asString(input.service.command, "");
499
+ if (!command)
500
+ throw new Error(`Runtime service "${serviceName}" is missing command`);
501
+ const serviceCwdTemplate = asString(input.service.cwd, ".");
502
+ const portConfig = parseObject(input.service.port);
503
+ const port = asString(portConfig.type, "") === "auto" ? await allocatePort() : null;
504
+ const envConfig = parseObject(input.service.env);
505
+ const templateData = buildTemplateData({
506
+ workspace: input.workspace,
507
+ agent: input.agent,
508
+ issue: input.issue,
509
+ adapterEnv: input.adapterEnv,
510
+ port,
511
+ });
512
+ const serviceCwd = resolveConfiguredPath(renderTemplate(serviceCwdTemplate, templateData), input.workspace.cwd);
513
+ const env = { ...process.env, ...input.adapterEnv };
514
+ for (const [key, value] of Object.entries(envConfig)) {
515
+ if (typeof value === "string") {
516
+ env[key] = renderTemplate(value, templateData);
517
+ }
518
+ }
519
+ if (port) {
520
+ const portEnvKey = asString(portConfig.envKey, "PORT");
521
+ env[portEnvKey] = String(port);
522
+ }
523
+ const shell = process.env.SHELL?.trim() || "/bin/sh";
524
+ const child = spawn(shell, ["-lc", command], {
525
+ cwd: serviceCwd,
526
+ env,
527
+ detached: false,
528
+ stdio: ["ignore", "pipe", "pipe"],
529
+ });
530
+ let stderrExcerpt = "";
531
+ let stdoutExcerpt = "";
532
+ child.stdout?.on("data", async (chunk) => {
533
+ const text = String(chunk);
534
+ stdoutExcerpt = (stdoutExcerpt + text).slice(-4096);
535
+ if (input.onLog)
536
+ await input.onLog("stdout", `[service:${serviceName}] ${text}`);
537
+ });
538
+ child.stderr?.on("data", async (chunk) => {
539
+ const text = String(chunk);
540
+ stderrExcerpt = (stderrExcerpt + text).slice(-4096);
541
+ if (input.onLog)
542
+ await input.onLog("stderr", `[service:${serviceName}] ${text}`);
543
+ });
544
+ const expose = parseObject(input.service.expose);
545
+ const readiness = parseObject(input.service.readiness);
546
+ const urlTemplate = asString(expose.urlTemplate, "") ||
547
+ asString(readiness.urlTemplate, "");
548
+ const url = urlTemplate ? renderTemplate(urlTemplate, templateData) : null;
549
+ try {
550
+ await waitForReadiness({ service: input.service, url });
551
+ }
552
+ catch (err) {
553
+ child.kill("SIGTERM");
554
+ throw new Error(`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`);
555
+ }
556
+ const envFingerprint = createHash("sha256").update(stableStringify(envConfig)).digest("hex");
557
+ return {
558
+ id: randomUUID(),
559
+ companyId: input.agent.companyId,
560
+ projectId: input.workspace.projectId,
561
+ projectWorkspaceId: input.workspace.workspaceId,
562
+ issueId: input.issue?.id ?? null,
563
+ serviceName,
564
+ status: "running",
565
+ lifecycle,
566
+ scopeType: input.scopeType,
567
+ scopeId: input.scopeId,
568
+ reuseKey: input.reuseKey,
569
+ command,
570
+ cwd: serviceCwd,
571
+ port,
572
+ url,
573
+ provider: "local_process",
574
+ providerRef: child.pid ? String(child.pid) : null,
575
+ ownerAgentId: input.agent.id,
576
+ startedByRunId: input.runId,
577
+ lastUsedAt: new Date().toISOString(),
578
+ startedAt: new Date().toISOString(),
579
+ stoppedAt: null,
580
+ stopPolicy: parseObject(input.service.stopPolicy),
581
+ healthStatus: "healthy",
582
+ reused: false,
583
+ db: input.db,
584
+ child,
585
+ leaseRunIds: new Set([input.runId]),
586
+ idleTimer: null,
587
+ envFingerprint,
588
+ };
589
+ }
590
+ function scheduleIdleStop(record) {
591
+ clearIdleTimer(record);
592
+ const stopType = asString(record.stopPolicy?.type, "manual");
593
+ if (stopType !== "idle_timeout")
594
+ return;
595
+ const idleSeconds = Math.max(1, asNumber(record.stopPolicy?.idleSeconds, 1800));
596
+ record.idleTimer = setTimeout(() => {
597
+ stopRuntimeService(record.id).catch(() => undefined);
598
+ }, idleSeconds * 1000);
599
+ }
600
+ async function stopRuntimeService(serviceId) {
601
+ const record = runtimeServicesById.get(serviceId);
602
+ if (!record)
603
+ return;
604
+ clearIdleTimer(record);
605
+ record.status = "stopped";
606
+ record.lastUsedAt = new Date().toISOString();
607
+ record.stoppedAt = new Date().toISOString();
608
+ if (record.child && !record.child.killed) {
609
+ record.child.kill("SIGTERM");
610
+ }
611
+ runtimeServicesById.delete(serviceId);
612
+ if (record.reuseKey) {
613
+ runtimeServicesByReuseKey.delete(record.reuseKey);
614
+ }
615
+ await persistRuntimeServiceRecord(record.db, record);
616
+ }
617
+ function registerRuntimeService(db, record) {
618
+ record.db = db;
619
+ runtimeServicesById.set(record.id, record);
620
+ if (record.reuseKey) {
621
+ runtimeServicesByReuseKey.set(record.reuseKey, record.id);
622
+ }
623
+ record.child?.on("exit", (code, signal) => {
624
+ const current = runtimeServicesById.get(record.id);
625
+ if (!current)
626
+ return;
627
+ clearIdleTimer(current);
628
+ current.status = code === 0 || signal === "SIGTERM" ? "stopped" : "failed";
629
+ current.healthStatus = current.status === "failed" ? "unhealthy" : "unknown";
630
+ current.lastUsedAt = new Date().toISOString();
631
+ current.stoppedAt = new Date().toISOString();
632
+ runtimeServicesById.delete(current.id);
633
+ if (current.reuseKey && runtimeServicesByReuseKey.get(current.reuseKey) === current.id) {
634
+ runtimeServicesByReuseKey.delete(current.reuseKey);
635
+ }
636
+ void persistRuntimeServiceRecord(db, current);
637
+ });
638
+ }
639
+ export async function ensureRuntimeServicesForRun(input) {
640
+ const runtime = parseObject(input.config.workspaceRuntime);
641
+ const rawServices = Array.isArray(runtime.services)
642
+ ? runtime.services.filter((entry) => typeof entry === "object" && entry !== null)
643
+ : [];
644
+ const acquiredServiceIds = [];
645
+ const refs = [];
646
+ runtimeServiceLeasesByRun.set(input.runId, acquiredServiceIds);
647
+ try {
648
+ for (const service of rawServices) {
649
+ const lifecycle = asString(service.lifecycle, "shared") === "ephemeral" ? "ephemeral" : "shared";
650
+ const { scopeType, scopeId } = resolveServiceScopeId({
651
+ service,
652
+ workspace: input.workspace,
653
+ issue: input.issue,
654
+ runId: input.runId,
655
+ agent: input.agent,
656
+ });
657
+ const envConfig = parseObject(service.env);
658
+ const envFingerprint = createHash("sha256").update(stableStringify(envConfig)).digest("hex");
659
+ const serviceName = asString(service.name, "service");
660
+ const reuseKey = lifecycle === "shared"
661
+ ? [scopeType, scopeId ?? "", serviceName, envFingerprint].join(":")
662
+ : null;
663
+ if (reuseKey) {
664
+ const existingId = runtimeServicesByReuseKey.get(reuseKey);
665
+ const existing = existingId ? runtimeServicesById.get(existingId) : null;
666
+ if (existing && existing.status === "running") {
667
+ existing.leaseRunIds.add(input.runId);
668
+ existing.lastUsedAt = new Date().toISOString();
669
+ existing.stoppedAt = null;
670
+ clearIdleTimer(existing);
671
+ await persistRuntimeServiceRecord(input.db, existing);
672
+ acquiredServiceIds.push(existing.id);
673
+ refs.push(toRuntimeServiceRef(existing, { reused: true }));
674
+ continue;
675
+ }
676
+ }
677
+ const record = await startLocalRuntimeService({
678
+ db: input.db,
679
+ runId: input.runId,
680
+ agent: input.agent,
681
+ issue: input.issue,
682
+ workspace: input.workspace,
683
+ adapterEnv: input.adapterEnv,
684
+ service,
685
+ onLog: input.onLog,
686
+ reuseKey,
687
+ scopeType,
688
+ scopeId,
689
+ });
690
+ registerRuntimeService(input.db, record);
691
+ await persistRuntimeServiceRecord(input.db, record);
692
+ acquiredServiceIds.push(record.id);
693
+ refs.push(toRuntimeServiceRef(record));
694
+ }
695
+ }
696
+ catch (err) {
697
+ await releaseRuntimeServicesForRun(input.runId);
698
+ throw err;
699
+ }
700
+ return refs;
701
+ }
702
+ export async function releaseRuntimeServicesForRun(runId) {
703
+ const acquired = runtimeServiceLeasesByRun.get(runId) ?? [];
704
+ runtimeServiceLeasesByRun.delete(runId);
705
+ for (const serviceId of acquired) {
706
+ const record = runtimeServicesById.get(serviceId);
707
+ if (!record)
708
+ continue;
709
+ record.leaseRunIds.delete(runId);
710
+ record.lastUsedAt = new Date().toISOString();
711
+ const stopType = asString(record.stopPolicy?.type, record.lifecycle === "ephemeral" ? "on_run_finish" : "manual");
712
+ await persistRuntimeServiceRecord(record.db, record);
713
+ if (record.leaseRunIds.size === 0) {
714
+ if (record.lifecycle === "ephemeral" || stopType === "on_run_finish") {
715
+ await stopRuntimeService(serviceId);
716
+ continue;
717
+ }
718
+ scheduleIdleStop(record);
719
+ }
720
+ }
721
+ }
722
+ export async function listWorkspaceRuntimeServicesForProjectWorkspaces(db, companyId, projectWorkspaceIds) {
723
+ if (projectWorkspaceIds.length === 0)
724
+ return new Map();
725
+ const rows = await db
726
+ .select()
727
+ .from(workspaceRuntimeServices)
728
+ .where(and(eq(workspaceRuntimeServices.companyId, companyId), inArray(workspaceRuntimeServices.projectWorkspaceId, projectWorkspaceIds)))
729
+ .orderBy(desc(workspaceRuntimeServices.updatedAt), desc(workspaceRuntimeServices.createdAt));
730
+ const grouped = new Map();
731
+ for (const row of rows) {
732
+ if (!row.projectWorkspaceId)
733
+ continue;
734
+ const existing = grouped.get(row.projectWorkspaceId);
735
+ if (existing)
736
+ existing.push(row);
737
+ else
738
+ grouped.set(row.projectWorkspaceId, [row]);
739
+ }
740
+ return grouped;
741
+ }
742
+ export async function reconcilePersistedRuntimeServicesOnStartup(db) {
743
+ const staleRows = await db
744
+ .select({ id: workspaceRuntimeServices.id })
745
+ .from(workspaceRuntimeServices)
746
+ .where(and(eq(workspaceRuntimeServices.provider, "local_process"), inArray(workspaceRuntimeServices.status, ["starting", "running"])));
747
+ if (staleRows.length === 0)
748
+ return { reconciled: 0 };
749
+ const now = new Date();
750
+ await db
751
+ .update(workspaceRuntimeServices)
752
+ .set({
753
+ status: "stopped",
754
+ healthStatus: "unknown",
755
+ stoppedAt: now,
756
+ lastUsedAt: now,
757
+ updatedAt: now,
758
+ })
759
+ .where(and(eq(workspaceRuntimeServices.provider, "local_process"), inArray(workspaceRuntimeServices.status, ["starting", "running"])));
760
+ return { reconciled: staleRows.length };
761
+ }
762
+ export async function persistAdapterManagedRuntimeServices(input) {
763
+ const refs = normalizeAdapterManagedRuntimeServices(input);
764
+ if (refs.length === 0)
765
+ return refs;
766
+ const existingRows = await input.db
767
+ .select()
768
+ .from(workspaceRuntimeServices)
769
+ .where(inArray(workspaceRuntimeServices.id, refs.map((ref) => ref.id)));
770
+ const existingById = new Map(existingRows.map((row) => [row.id, row]));
771
+ for (const ref of refs) {
772
+ const existing = existingById.get(ref.id);
773
+ const startedAt = existing?.startedAt ?? new Date(ref.startedAt);
774
+ const createdAt = existing?.createdAt ?? new Date();
775
+ await input.db
776
+ .insert(workspaceRuntimeServices)
777
+ .values({
778
+ id: ref.id,
779
+ companyId: ref.companyId,
780
+ projectId: ref.projectId,
781
+ projectWorkspaceId: ref.projectWorkspaceId,
782
+ issueId: ref.issueId,
783
+ scopeType: ref.scopeType,
784
+ scopeId: ref.scopeId,
785
+ serviceName: ref.serviceName,
786
+ status: ref.status,
787
+ lifecycle: ref.lifecycle,
788
+ reuseKey: ref.reuseKey,
789
+ command: ref.command,
790
+ cwd: ref.cwd,
791
+ port: ref.port,
792
+ url: ref.url,
793
+ provider: ref.provider,
794
+ providerRef: ref.providerRef,
795
+ ownerAgentId: ref.ownerAgentId,
796
+ startedByRunId: ref.startedByRunId,
797
+ lastUsedAt: new Date(ref.lastUsedAt),
798
+ startedAt,
799
+ stoppedAt: ref.stoppedAt ? new Date(ref.stoppedAt) : null,
800
+ stopPolicy: ref.stopPolicy,
801
+ healthStatus: ref.healthStatus,
802
+ createdAt,
803
+ updatedAt: new Date(),
804
+ })
805
+ .onConflictDoUpdate({
806
+ target: workspaceRuntimeServices.id,
807
+ set: {
808
+ projectId: ref.projectId,
809
+ projectWorkspaceId: ref.projectWorkspaceId,
810
+ issueId: ref.issueId,
811
+ scopeType: ref.scopeType,
812
+ scopeId: ref.scopeId,
813
+ serviceName: ref.serviceName,
814
+ status: ref.status,
815
+ lifecycle: ref.lifecycle,
816
+ reuseKey: ref.reuseKey,
817
+ command: ref.command,
818
+ cwd: ref.cwd,
819
+ port: ref.port,
820
+ url: ref.url,
821
+ provider: ref.provider,
822
+ providerRef: ref.providerRef,
823
+ ownerAgentId: ref.ownerAgentId,
824
+ startedByRunId: ref.startedByRunId,
825
+ lastUsedAt: new Date(ref.lastUsedAt),
826
+ startedAt,
827
+ stoppedAt: ref.stoppedAt ? new Date(ref.stoppedAt) : null,
828
+ stopPolicy: ref.stopPolicy,
829
+ healthStatus: ref.healthStatus,
830
+ updatedAt: new Date(),
831
+ },
832
+ });
833
+ }
834
+ return refs;
835
+ }
836
+ export function buildWorkspaceReadyComment(input) {
837
+ const lines = ["## Workspace Ready", ""];
838
+ lines.push(`- Strategy: \`${input.workspace.strategy}\``);
839
+ if (input.workspace.branchName)
840
+ lines.push(`- Branch: \`${input.workspace.branchName}\``);
841
+ lines.push(`- CWD: \`${input.workspace.cwd}\``);
842
+ if (input.workspace.worktreePath && input.workspace.worktreePath !== input.workspace.cwd) {
843
+ lines.push(`- Worktree: \`${input.workspace.worktreePath}\``);
844
+ }
845
+ for (const service of input.runtimeServices) {
846
+ const detail = service.url ? `${service.serviceName}: ${service.url}` : `${service.serviceName}: running`;
847
+ const suffix = service.reused ? " (reused)" : "";
848
+ lines.push(`- Service: ${detail}${suffix}`);
849
+ }
850
+ return lines.join("\n");
851
+ }
852
+ //# sourceMappingURL=workspace-runtime.js.map