terramend 0.2.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 (406) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +145 -0
  3. package/dist/agents/claude.d.ts +73 -0
  4. package/dist/agents/claudePretoolGate.d.ts +99 -0
  5. package/dist/agents/gateServer.d.ts +7 -0
  6. package/dist/agents/index.d.ts +6 -0
  7. package/dist/agents/nativeFsDenies.d.ts +28 -0
  8. package/dist/agents/opencode.d.ts +231 -0
  9. package/dist/agents/opencodePlugin.d.ts +85 -0
  10. package/dist/agents/opencodeShared.d.ts +40 -0
  11. package/dist/agents/postRun.d.ts +132 -0
  12. package/dist/agents/reviewer.d.ts +38 -0
  13. package/dist/agents/sessionLabeler.d.ts +97 -0
  14. package/dist/agents/shared.d.ts +189 -0
  15. package/dist/agents/subagentModels.d.ts +19 -0
  16. package/dist/agents/subagentToolGates.d.ts +55 -0
  17. package/dist/cli.mjs +197426 -0
  18. package/dist/external.d.ts +227 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +196783 -0
  21. package/dist/internal/index.d.ts +18 -0
  22. package/dist/internal.js +1714 -0
  23. package/dist/lifecycle.d.ts +2 -0
  24. package/dist/main.d.ts +8 -0
  25. package/dist/mcp/arkConfig.d.ts +1 -0
  26. package/dist/mcp/checkSuite.d.ts +25 -0
  27. package/dist/mcp/checkout.d.ts +77 -0
  28. package/dist/mcp/comment.d.ts +119 -0
  29. package/dist/mcp/commitInfo.d.ts +9 -0
  30. package/dist/mcp/crosswalk.d.ts +105 -0
  31. package/dist/mcp/dependencies.d.ts +8 -0
  32. package/dist/mcp/geminiSanitizer.d.ts +28 -0
  33. package/dist/mcp/git.d.ts +46 -0
  34. package/dist/mcp/guardrails.d.ts +104 -0
  35. package/dist/mcp/issue.d.ts +18 -0
  36. package/dist/mcp/issueComments.d.ts +9 -0
  37. package/dist/mcp/issueEvents.d.ts +9 -0
  38. package/dist/mcp/issueInfo.d.ts +9 -0
  39. package/dist/mcp/labels.d.ts +12 -0
  40. package/dist/mcp/localContext.d.ts +19 -0
  41. package/dist/mcp/moduleExtraction.d.ts +71 -0
  42. package/dist/mcp/moduleTests.d.ts +104 -0
  43. package/dist/mcp/modules.d.ts +179 -0
  44. package/dist/mcp/output.d.ts +12 -0
  45. package/dist/mcp/pathSafety.d.ts +14 -0
  46. package/dist/mcp/policy.d.ts +48 -0
  47. package/dist/mcp/pr.d.ts +49 -0
  48. package/dist/mcp/prInfo.d.ts +9 -0
  49. package/dist/mcp/providerSchema.d.ts +50 -0
  50. package/dist/mcp/review.d.ts +199 -0
  51. package/dist/mcp/reviewComments.d.ts +178 -0
  52. package/dist/mcp/roots.d.ts +58 -0
  53. package/dist/mcp/scope.d.ts +15 -0
  54. package/dist/mcp/selectMode.d.ts +18 -0
  55. package/dist/mcp/server.d.ts +48 -0
  56. package/dist/mcp/shared.d.ts +47 -0
  57. package/dist/mcp/shell.d.ts +37 -0
  58. package/dist/mcp/staleFix.d.ts +51 -0
  59. package/dist/mcp/terraform/cost.d.ts +55 -0
  60. package/dist/mcp/terraform/currency.d.ts +94 -0
  61. package/dist/mcp/terraform/decisions.d.ts +178 -0
  62. package/dist/mcp/terraform/findings.d.ts +75 -0
  63. package/dist/mcp/terraform/plan.d.ts +157 -0
  64. package/dist/mcp/terraform/scanners.d.ts +131 -0
  65. package/dist/mcp/terraform/tools.d.ts +63 -0
  66. package/dist/mcp/terraform/types.d.ts +172 -0
  67. package/dist/mcp/terraform.d.ts +22 -0
  68. package/dist/mcp/terratest.d.ts +83 -0
  69. package/dist/mcp/upload.d.ts +6 -0
  70. package/dist/models.d.ts +171 -0
  71. package/dist/modes.d.ts +26 -0
  72. package/dist/prep/index.d.ts +7 -0
  73. package/dist/prep/installNodeDependencies.d.ts +2 -0
  74. package/dist/prep/installPythonDependencies.d.ts +2 -0
  75. package/dist/prep/types.d.ts +31 -0
  76. package/dist/reviewQuality.d.ts +64 -0
  77. package/dist/skills/terraform-best-practices/SKILL.md +369 -0
  78. package/dist/toolState.d.ts +135 -0
  79. package/dist/utils/activity.d.ts +40 -0
  80. package/dist/utils/agent.d.ts +20 -0
  81. package/dist/utils/agentHangReport.d.ts +38 -0
  82. package/dist/utils/apiFetch.d.ts +19 -0
  83. package/dist/utils/apiKeys.d.ts +41 -0
  84. package/dist/utils/apiUrl.d.ts +20 -0
  85. package/dist/utils/assets.d.ts +8 -0
  86. package/dist/utils/billingErrors.d.ts +85 -0
  87. package/dist/utils/body.d.ts +34 -0
  88. package/dist/utils/buildTerramendFooter.d.ts +25 -0
  89. package/dist/utils/byokFallback.d.ts +85 -0
  90. package/dist/utils/claudeSubscription.d.ts +30 -0
  91. package/dist/utils/cli.d.ts +10 -0
  92. package/dist/utils/codexHome.d.ts +29 -0
  93. package/dist/utils/codexOAuth.d.ts +60 -0
  94. package/dist/utils/diffCoverage.d.ts +63 -0
  95. package/dist/utils/errorReport.d.ts +17 -0
  96. package/dist/utils/exitHandler.d.ts +8 -0
  97. package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
  98. package/dist/utils/gitAuth.d.ts +84 -0
  99. package/dist/utils/gitAuthServer.d.ts +24 -0
  100. package/dist/utils/github.d.ts +78 -0
  101. package/dist/utils/globals.d.ts +3 -0
  102. package/dist/utils/install.d.ts +60 -0
  103. package/dist/utils/instructions.d.ts +48 -0
  104. package/dist/utils/leapingComment.d.ts +11 -0
  105. package/dist/utils/learnings.d.ts +62 -0
  106. package/dist/utils/learningsTruncate.d.ts +25 -0
  107. package/dist/utils/lifecycle.d.ts +57 -0
  108. package/dist/utils/log.d.ts +111 -0
  109. package/dist/utils/normalizeEnv.d.ts +30 -0
  110. package/dist/utils/openCodeModels.d.ts +11 -0
  111. package/dist/utils/overrides.d.ts +40 -0
  112. package/dist/utils/packageManager.d.ts +49 -0
  113. package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
  114. package/dist/utils/payload.d.ts +105 -0
  115. package/dist/utils/prSummary.d.ts +61 -0
  116. package/dist/utils/progressComment.d.ts +146 -0
  117. package/dist/utils/providerErrors.d.ts +31 -0
  118. package/dist/utils/rangeDiff.d.ts +51 -0
  119. package/dist/utils/remediationCommand.d.ts +55 -0
  120. package/dist/utils/retry.d.ts +13 -0
  121. package/dist/utils/reviewCleanup.d.ts +14 -0
  122. package/dist/utils/run.d.ts +9 -0
  123. package/dist/utils/runContext.d.ts +60 -0
  124. package/dist/utils/runContextData.d.ts +23 -0
  125. package/dist/utils/runErrorRenderer.d.ts +64 -0
  126. package/dist/utils/runLifecycle.d.ts +86 -0
  127. package/dist/utils/runStartupLog.d.ts +15 -0
  128. package/dist/utils/secrets.d.ts +22 -0
  129. package/dist/utils/setup.d.ts +90 -0
  130. package/dist/utils/shell.d.ts +32 -0
  131. package/dist/utils/skills.d.ts +10 -0
  132. package/dist/utils/subprocess.d.ts +80 -0
  133. package/dist/utils/terraformMcp.d.ts +42 -0
  134. package/dist/utils/time.d.ts +15 -0
  135. package/dist/utils/timer.d.ts +23 -0
  136. package/dist/utils/todoTracking.d.ts +16 -0
  137. package/dist/utils/token.d.ts +39 -0
  138. package/dist/utils/version.d.ts +2 -0
  139. package/dist/utils/versioning.d.ts +7 -0
  140. package/dist/utils/vertex.d.ts +16 -0
  141. package/dist/utils/workflow.d.ts +13 -0
  142. package/package.json +119 -0
  143. package/src/agents/claude.test.ts +1016 -0
  144. package/src/agents/claude.ts +1246 -0
  145. package/src/agents/claudePretoolGate.test.ts +28 -0
  146. package/src/agents/claudePretoolGate.ts +173 -0
  147. package/src/agents/gateServer.test.ts +204 -0
  148. package/src/agents/gateServer.ts +124 -0
  149. package/src/agents/index.ts +10 -0
  150. package/src/agents/nativeFsDenies.ts +82 -0
  151. package/src/agents/opencode.test.ts +1440 -0
  152. package/src/agents/opencode.ts +1312 -0
  153. package/src/agents/opencodePlugin.ts +222 -0
  154. package/src/agents/opencodeShared.test.ts +34 -0
  155. package/src/agents/opencodeShared.ts +121 -0
  156. package/src/agents/postRun.test.ts +549 -0
  157. package/src/agents/postRun.ts +535 -0
  158. package/src/agents/reviewer.ts +104 -0
  159. package/src/agents/sessionLabeler.test.ts +247 -0
  160. package/src/agents/sessionLabeler.ts +178 -0
  161. package/src/agents/shared.test.ts +76 -0
  162. package/src/agents/shared.ts +292 -0
  163. package/src/agents/subagentModels.test.ts +113 -0
  164. package/src/agents/subagentModels.ts +40 -0
  165. package/src/agents/subagentRegistration.test.ts +41 -0
  166. package/src/agents/subagentToolGates.ts +114 -0
  167. package/src/cli.test.ts +129 -0
  168. package/src/cli.ts +105 -0
  169. package/src/commands/gha.test.ts +192 -0
  170. package/src/commands/gha.ts +188 -0
  171. package/src/commands/mcp.ts +122 -0
  172. package/src/config.ts +1 -0
  173. package/src/entry.ts +7 -0
  174. package/src/entryPost.stdlibOnly.test.ts +109 -0
  175. package/src/entryPost.ts +99 -0
  176. package/src/external.test.ts +16 -0
  177. package/src/external.ts +302 -0
  178. package/src/index.ts +11 -0
  179. package/src/internal/index.ts +71 -0
  180. package/src/lifecycle.ts +2 -0
  181. package/src/main.test.ts +873 -0
  182. package/src/main.ts +712 -0
  183. package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
  184. package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
  185. package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
  186. package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
  187. package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
  188. package/src/mcp/arkConfig.ts +7 -0
  189. package/src/mcp/checkSuite.test.ts +245 -0
  190. package/src/mcp/checkSuite.ts +255 -0
  191. package/src/mcp/checkout.test.ts +752 -0
  192. package/src/mcp/checkout.ts +886 -0
  193. package/src/mcp/comment.test.ts +772 -0
  194. package/src/mcp/comment.ts +582 -0
  195. package/src/mcp/commitInfo.test.ts +127 -0
  196. package/src/mcp/commitInfo.ts +61 -0
  197. package/src/mcp/crosswalk.test.ts +106 -0
  198. package/src/mcp/crosswalk.ts +339 -0
  199. package/src/mcp/dependencies.test.ts +309 -0
  200. package/src/mcp/dependencies.ts +189 -0
  201. package/src/mcp/geminiSanitizer.test.ts +287 -0
  202. package/src/mcp/geminiSanitizer.ts +207 -0
  203. package/src/mcp/git.test.ts +1083 -0
  204. package/src/mcp/git.ts +890 -0
  205. package/src/mcp/guardrails.test.ts +705 -0
  206. package/src/mcp/guardrails.ts +465 -0
  207. package/src/mcp/issue.test.ts +113 -0
  208. package/src/mcp/issue.ts +73 -0
  209. package/src/mcp/issueComments.test.ts +69 -0
  210. package/src/mcp/issueComments.ts +48 -0
  211. package/src/mcp/issueEvents.test.ts +134 -0
  212. package/src/mcp/issueEvents.ts +100 -0
  213. package/src/mcp/issueInfo.test.ts +104 -0
  214. package/src/mcp/issueInfo.ts +72 -0
  215. package/src/mcp/labels.test.ts +52 -0
  216. package/src/mcp/labels.ts +34 -0
  217. package/src/mcp/localContext.ts +28 -0
  218. package/src/mcp/localServer.test.ts +75 -0
  219. package/src/mcp/localServer.ts +131 -0
  220. package/src/mcp/moduleExtraction.test.ts +261 -0
  221. package/src/mcp/moduleExtraction.ts +313 -0
  222. package/src/mcp/moduleTests.test.ts +269 -0
  223. package/src/mcp/moduleTests.ts +421 -0
  224. package/src/mcp/modules.test.ts +640 -0
  225. package/src/mcp/modules.ts +696 -0
  226. package/src/mcp/output.test.ts +96 -0
  227. package/src/mcp/output.ts +70 -0
  228. package/src/mcp/pathSafety.test.ts +44 -0
  229. package/src/mcp/pathSafety.ts +28 -0
  230. package/src/mcp/policy.test.ts +282 -0
  231. package/src/mcp/policy.ts +199 -0
  232. package/src/mcp/pr.test.ts +387 -0
  233. package/src/mcp/pr.ts +194 -0
  234. package/src/mcp/prInfo.test.ts +96 -0
  235. package/src/mcp/prInfo.ts +91 -0
  236. package/src/mcp/providerSchema.test.ts +85 -0
  237. package/src/mcp/providerSchema.ts +175 -0
  238. package/src/mcp/review.test.ts +936 -0
  239. package/src/mcp/review.ts +923 -0
  240. package/src/mcp/reviewComments.test.ts +549 -0
  241. package/src/mcp/reviewComments.ts +896 -0
  242. package/src/mcp/roots.test.ts +175 -0
  243. package/src/mcp/roots.ts +217 -0
  244. package/src/mcp/scope.test.ts +59 -0
  245. package/src/mcp/scope.ts +65 -0
  246. package/src/mcp/security.test.ts +720 -0
  247. package/src/mcp/selectMode.test.ts +210 -0
  248. package/src/mcp/selectMode.ts +181 -0
  249. package/src/mcp/server.test.ts +292 -0
  250. package/src/mcp/server.ts +403 -0
  251. package/src/mcp/shared.ts +100 -0
  252. package/src/mcp/shell.test.ts +520 -0
  253. package/src/mcp/shell.ts +505 -0
  254. package/src/mcp/staleFix.test.ts +237 -0
  255. package/src/mcp/staleFix.ts +277 -0
  256. package/src/mcp/terraform/cost.ts +163 -0
  257. package/src/mcp/terraform/currency.test.ts +338 -0
  258. package/src/mcp/terraform/currency.ts +336 -0
  259. package/src/mcp/terraform/decisions.ts +527 -0
  260. package/src/mcp/terraform/findings.ts +333 -0
  261. package/src/mcp/terraform/plan.ts +348 -0
  262. package/src/mcp/terraform/scanners.ts +809 -0
  263. package/src/mcp/terraform/tools.test.ts +1071 -0
  264. package/src/mcp/terraform/tools.ts +908 -0
  265. package/src/mcp/terraform/types.ts +305 -0
  266. package/src/mcp/terraform.test.ts +1957 -0
  267. package/src/mcp/terraform.ts +23 -0
  268. package/src/mcp/terratest.test.ts +105 -0
  269. package/src/mcp/terratest.ts +196 -0
  270. package/src/mcp/toolFiltering.test.ts +85 -0
  271. package/src/mcp/upload.test.ts +180 -0
  272. package/src/mcp/upload.ts +112 -0
  273. package/src/models.test.ts +300 -0
  274. package/src/models.ts +708 -0
  275. package/src/modes.test.ts +107 -0
  276. package/src/modes.ts +880 -0
  277. package/src/prep/index.ts +43 -0
  278. package/src/prep/installNodeDependencies.test.ts +298 -0
  279. package/src/prep/installNodeDependencies.ts +196 -0
  280. package/src/prep/installPythonDependencies.test.ts +268 -0
  281. package/src/prep/installPythonDependencies.ts +199 -0
  282. package/src/prep/types.ts +38 -0
  283. package/src/reviewQuality.test.ts +63 -0
  284. package/src/reviewQuality.ts +134 -0
  285. package/src/runCli.test.ts +214 -0
  286. package/src/runCli.ts +282 -0
  287. package/src/skills/terraform-best-practices/SKILL.md +369 -0
  288. package/src/toolState.test.ts +45 -0
  289. package/src/toolState.ts +252 -0
  290. package/src/utils/activity.test.ts +188 -0
  291. package/src/utils/activity.ts +210 -0
  292. package/src/utils/agent.test.ts +251 -0
  293. package/src/utils/agent.ts +139 -0
  294. package/src/utils/agentHangReport.test.ts +203 -0
  295. package/src/utils/agentHangReport.ts +170 -0
  296. package/src/utils/apiFetch.test.ts +115 -0
  297. package/src/utils/apiFetch.ts +62 -0
  298. package/src/utils/apiKeys.test.ts +344 -0
  299. package/src/utils/apiKeys.ts +206 -0
  300. package/src/utils/apiUrl.test.ts +30 -0
  301. package/src/utils/apiUrl.ts +59 -0
  302. package/src/utils/assets.test.ts +153 -0
  303. package/src/utils/assets.ts +107 -0
  304. package/src/utils/billingErrors.test.ts +121 -0
  305. package/src/utils/billingErrors.ts +189 -0
  306. package/src/utils/body.test.ts +217 -0
  307. package/src/utils/body.ts +168 -0
  308. package/src/utils/buildTerramendFooter.test.ts +38 -0
  309. package/src/utils/buildTerramendFooter.ts +82 -0
  310. package/src/utils/byokFallback.test.ts +205 -0
  311. package/src/utils/byokFallback.ts +128 -0
  312. package/src/utils/claudeSubscription.test.ts +179 -0
  313. package/src/utils/claudeSubscription.ts +93 -0
  314. package/src/utils/cli.ts +31 -0
  315. package/src/utils/codexHome.test.ts +190 -0
  316. package/src/utils/codexHome.ts +191 -0
  317. package/src/utils/codexOAuth.ts +147 -0
  318. package/src/utils/codexRefreshDetect.test.ts +85 -0
  319. package/src/utils/codexRefreshDetect.ts +35 -0
  320. package/src/utils/diffCoverage.test.ts +468 -0
  321. package/src/utils/diffCoverage.ts +404 -0
  322. package/src/utils/errorReport.test.ts +135 -0
  323. package/src/utils/errorReport.ts +83 -0
  324. package/src/utils/exitHandler.ts +35 -0
  325. package/src/utils/fixDoubleEscapedString.ts +9 -0
  326. package/src/utils/ghaCore.ts +13 -0
  327. package/src/utils/gitAuth.test.ts +322 -0
  328. package/src/utils/gitAuth.ts +263 -0
  329. package/src/utils/gitAuthServer.test.ts +260 -0
  330. package/src/utils/gitAuthServer.ts +182 -0
  331. package/src/utils/github.test.ts +615 -0
  332. package/src/utils/github.ts +538 -0
  333. package/src/utils/globals.ts +9 -0
  334. package/src/utils/humanEditCapture.test.ts +100 -0
  335. package/src/utils/humanEditCapture.ts +193 -0
  336. package/src/utils/install.test.ts +768 -0
  337. package/src/utils/install.ts +492 -0
  338. package/src/utils/instructions.test.ts +240 -0
  339. package/src/utils/instructions.ts +543 -0
  340. package/src/utils/leapingComment.test.ts +51 -0
  341. package/src/utils/leapingComment.ts +18 -0
  342. package/src/utils/learnings.test.ts +87 -0
  343. package/src/utils/learnings.ts +138 -0
  344. package/src/utils/learningsTocRender.test.ts +116 -0
  345. package/src/utils/learningsTruncate.test.ts +39 -0
  346. package/src/utils/learningsTruncate.ts +42 -0
  347. package/src/utils/lifecycle.test.ts +195 -0
  348. package/src/utils/lifecycle.ts +198 -0
  349. package/src/utils/log.test.ts +402 -0
  350. package/src/utils/log.ts +432 -0
  351. package/src/utils/normalizeEnv.test.ts +91 -0
  352. package/src/utils/normalizeEnv.ts +106 -0
  353. package/src/utils/openCodeModels.ts +82 -0
  354. package/src/utils/overrides.test.ts +89 -0
  355. package/src/utils/overrides.ts +98 -0
  356. package/src/utils/packageManager.test.ts +321 -0
  357. package/src/utils/packageManager.ts +257 -0
  358. package/src/utils/patchWorkflowRunFields.test.ts +92 -0
  359. package/src/utils/patchWorkflowRunFields.ts +150 -0
  360. package/src/utils/payload.test.ts +497 -0
  361. package/src/utils/payload.ts +371 -0
  362. package/src/utils/postApiFetch.ts +51 -0
  363. package/src/utils/prSummary.test.ts +224 -0
  364. package/src/utils/prSummary.ts +147 -0
  365. package/src/utils/progressComment.ts +261 -0
  366. package/src/utils/providerErrors.test.ts +315 -0
  367. package/src/utils/providerErrors.ts +172 -0
  368. package/src/utils/rangeDiff.test.ts +236 -0
  369. package/src/utils/rangeDiff.ts +182 -0
  370. package/src/utils/remediationCommand.test.ts +163 -0
  371. package/src/utils/remediationCommand.ts +119 -0
  372. package/src/utils/retry.test.ts +153 -0
  373. package/src/utils/retry.ts +58 -0
  374. package/src/utils/reviewCleanup.ts +106 -0
  375. package/src/utils/run.ts +99 -0
  376. package/src/utils/runContext.ts +145 -0
  377. package/src/utils/runContextData.ts +58 -0
  378. package/src/utils/runErrorRenderer.test.ts +95 -0
  379. package/src/utils/runErrorRenderer.ts +259 -0
  380. package/src/utils/runFixture.ts +76 -0
  381. package/src/utils/runLifecycle.ts +237 -0
  382. package/src/utils/runStartupLog.ts +60 -0
  383. package/src/utils/secrets.test.ts +103 -0
  384. package/src/utils/secrets.ts +177 -0
  385. package/src/utils/setup.test.ts +509 -0
  386. package/src/utils/setup.ts +352 -0
  387. package/src/utils/shell.ts +103 -0
  388. package/src/utils/skills.test.ts +46 -0
  389. package/src/utils/skills.ts +67 -0
  390. package/src/utils/subprocess.test.ts +170 -0
  391. package/src/utils/subprocess.ts +438 -0
  392. package/src/utils/terraformMcp.test.ts +63 -0
  393. package/src/utils/terraformMcp.ts +83 -0
  394. package/src/utils/time.test.ts +105 -0
  395. package/src/utils/time.ts +59 -0
  396. package/src/utils/timer.test.ts +91 -0
  397. package/src/utils/timer.ts +72 -0
  398. package/src/utils/todoTracking.test.ts +223 -0
  399. package/src/utils/todoTracking.ts +167 -0
  400. package/src/utils/token.test.ts +239 -0
  401. package/src/utils/token.ts +186 -0
  402. package/src/utils/version.ts +10 -0
  403. package/src/utils/versioning.test.ts +34 -0
  404. package/src/utils/versioning.ts +44 -0
  405. package/src/utils/vertex.ts +85 -0
  406. package/src/utils/workflow.ts +25 -0
@@ -0,0 +1,247 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ deriveLabelFromTaskInput,
4
+ formatWithLabel,
5
+ ORCHESTRATOR_LABEL,
6
+ SessionLabeler,
7
+ } from "#app/agents/sessionLabeler";
8
+
9
+ describe("deriveLabelFromTaskInput", () => {
10
+ test("prefers explicit lens marker in prompt over description", () => {
11
+ expect(
12
+ deriveLabelFromTaskInput({
13
+ prompt: "lens: security\nReview the diff for...",
14
+ description: "general review",
15
+ }),
16
+ ).toBe("lens:security");
17
+ });
18
+
19
+ test("supports lens=<name> alternative syntax", () => {
20
+ expect(
21
+ deriveLabelFromTaskInput({
22
+ prompt: "lens=user-journey\nWalk through the happy path...",
23
+ }),
24
+ ).toBe("lens:user-journey");
25
+ });
26
+
27
+ test("falls back to description when no lens marker present", () => {
28
+ expect(
29
+ deriveLabelFromTaskInput({
30
+ prompt: "Review this diff for any bugs",
31
+ description: "Auth lens",
32
+ }),
33
+ ).toBe("lens:auth-lens");
34
+ });
35
+
36
+ test("falls back to subagent_type when description and lens marker absent", () => {
37
+ expect(
38
+ deriveLabelFromTaskInput({
39
+ prompt: "Some generic prompt",
40
+ subagent_type: "review",
41
+ }),
42
+ ).toBe("review");
43
+ });
44
+
45
+ test("returns generic subagent when nothing identifiable", () => {
46
+ expect(deriveLabelFromTaskInput({})).toBe("subagent");
47
+ });
48
+
49
+ test("slug normalizes whitespace and special chars", () => {
50
+ expect(
51
+ deriveLabelFromTaskInput({
52
+ description: "Schema migration & operational readiness!",
53
+ }),
54
+ ).toBe("lens:schema-migration-operational-readiness");
55
+ });
56
+
57
+ test("slug truncates labels longer than 40 chars to keep prefix readable", () => {
58
+ expect(
59
+ deriveLabelFromTaskInput({
60
+ description: "this is a very long lens description that exceeds the slug limit",
61
+ }),
62
+ ).toBe("lens:this-is-a-very-long-lens-description-tha");
63
+ });
64
+
65
+ test("ignores lens marker mid-line — must be at line start", () => {
66
+ expect(
67
+ deriveLabelFromTaskInput({
68
+ prompt: "Please review the lens: security claim made above",
69
+ description: "billing",
70
+ }),
71
+ ).toBe("lens:billing");
72
+ });
73
+ });
74
+
75
+ describe("SessionLabeler", () => {
76
+ test("first session seen is the orchestrator", () => {
77
+ const labeler = new SessionLabeler();
78
+ expect(labeler.labelFor("ses-A")).toBe(ORCHESTRATOR_LABEL);
79
+ // bound — same session returns same label on second call
80
+ expect(labeler.labelFor("ses-A")).toBe(ORCHESTRATOR_LABEL);
81
+ expect(labeler.size()).toBe(1);
82
+ });
83
+
84
+ test("FIFO matches dispatched labels to new sessions in dispatch order", () => {
85
+ const labeler = new SessionLabeler();
86
+ // orchestrator session
87
+ labeler.labelFor("parent");
88
+
89
+ // orchestrator dispatches 3 tasks in one assistant turn
90
+ labeler.recordTaskDispatch({ description: "security" });
91
+ labeler.recordTaskDispatch({ description: "correctness" });
92
+ labeler.recordTaskDispatch({ description: "user journey" });
93
+
94
+ expect(labeler.pendingDispatchCount()).toBe(3);
95
+
96
+ // children appear (potentially interleaved)
97
+ expect(labeler.labelFor("child-1")).toBe("lens:security");
98
+ expect(labeler.labelFor("child-2")).toBe("lens:correctness");
99
+ expect(labeler.labelFor("child-3")).toBe("lens:user-journey");
100
+
101
+ expect(labeler.pendingDispatchCount()).toBe(0);
102
+ expect(labeler.size()).toBe(4);
103
+ });
104
+
105
+ test("interleaved events from parent and children resolve to stable labels", () => {
106
+ const labeler = new SessionLabeler();
107
+ labeler.labelFor("parent");
108
+ labeler.recordTaskDispatch({ description: "security" });
109
+ labeler.recordTaskDispatch({ description: "correctness" });
110
+
111
+ // child-1 emits an event first (its label binds)
112
+ expect(labeler.labelFor("child-1")).toBe("lens:security");
113
+ // parent emits some events in between
114
+ expect(labeler.labelFor("parent")).toBe(ORCHESTRATOR_LABEL);
115
+ // child-2 finally appears
116
+ expect(labeler.labelFor("child-2")).toBe("lens:correctness");
117
+ // child-1 emits more events — still the same label
118
+ expect(labeler.labelFor("child-1")).toBe("lens:security");
119
+ });
120
+
121
+ test("falls back to subagent#N when child appears without a queued dispatch", () => {
122
+ const labeler = new SessionLabeler();
123
+ labeler.labelFor("parent");
124
+ // no recordTaskDispatch — but a child appears anyway (defensive path)
125
+ expect(labeler.labelFor("ghost")).toBe("subagent#1");
126
+ expect(labeler.labelFor("ghost-2")).toBe("subagent#2");
127
+ });
128
+
129
+ test("undefined/null/empty sessionID resolves to orchestrator label without binding", () => {
130
+ const labeler = new SessionLabeler();
131
+ expect(labeler.labelFor(undefined)).toBe(ORCHESTRATOR_LABEL);
132
+ expect(labeler.labelFor(null)).toBe(ORCHESTRATOR_LABEL);
133
+ expect(labeler.labelFor("")).toBe(ORCHESTRATOR_LABEL);
134
+ // size stays zero — those calls didn't bind anything
135
+ expect(labeler.size()).toBe(0);
136
+ });
137
+
138
+ test("entries returns insertion-ordered (sessionID, label) pairs", () => {
139
+ const labeler = new SessionLabeler();
140
+ labeler.labelFor("parent");
141
+ labeler.recordTaskDispatch({ description: "security" });
142
+ labeler.labelFor("child-1");
143
+ expect(labeler.entries()).toEqual([
144
+ ["parent", ORCHESTRATOR_LABEL],
145
+ ["child-1", "lens:security"],
146
+ ]);
147
+ });
148
+
149
+ test("Claude path: parent_tool_use_id resolves directly without consuming FIFO", () => {
150
+ // Claude runs subagents inside the orchestrator's session — they share
151
+ // session_id — and stamps subagent messages with parent_tool_use_id.
152
+ // recording dispatch with the Agent tool_use id binds it directly so
153
+ // future events resolve regardless of session_id.
154
+ const labeler = new SessionLabeler();
155
+ expect(labeler.labelFor("shared-session", null)).toBe(ORCHESTRATOR_LABEL);
156
+
157
+ labeler.recordTaskDispatch({ description: "correctness" }, "toolu_01");
158
+ labeler.recordTaskDispatch({ description: "security" }, "toolu_02");
159
+
160
+ // subagent events come through with shared session_id but distinct
161
+ // parent_tool_use_id — direct mapping wins
162
+ expect(labeler.labelFor("shared-session", "toolu_01")).toBe("lens:correctness");
163
+ expect(labeler.labelFor("shared-session", "toolu_02")).toBe("lens:security");
164
+
165
+ // orchestrator events on the same session still resolve correctly
166
+ expect(labeler.labelFor("shared-session", null)).toBe(ORCHESTRATOR_LABEL);
167
+
168
+ // pendingLabels is unused on the Claude path — FIFO never consumed
169
+ expect(labeler.pendingDispatchCount()).toBe(2);
170
+ expect(labeler.size()).toBe(1);
171
+ });
172
+
173
+ test("Claude path: unknown parent_tool_use_id falls through to sessionID/FIFO logic", () => {
174
+ // defensive: if a subagent event arrives with a parent_tool_use_id we
175
+ // never recorded (e.g. orchestrator dispatched off-stream, or a tool we
176
+ // didn't track), the labeler shouldn't crash — it should fall through
177
+ // to the sessionID-keyed path.
178
+ const labeler = new SessionLabeler();
179
+ labeler.labelFor("shared", null);
180
+ expect(labeler.labelFor("shared", "unknown-tool-id")).toBe(ORCHESTRATOR_LABEL);
181
+ });
182
+
183
+ test("realistic four-lens parallel fan-out — interleaved tool_use stream", () => {
184
+ // simulates the event order we'd see when the orchestrator dispatches
185
+ // 4 lens subagents in a single assistant turn and they all start emitting
186
+ // tool_use events more or less concurrently.
187
+ const labeler = new SessionLabeler();
188
+
189
+ // 1. orchestrator's `init` event
190
+ expect(labeler.labelFor("p")).toBe(ORCHESTRATOR_LABEL);
191
+
192
+ // 2. orchestrator emits 4 task tool_use events back-to-back
193
+ labeler.recordTaskDispatch({ description: "correctness & invariants" });
194
+ labeler.recordTaskDispatch({ description: "security" });
195
+ labeler.recordTaskDispatch({ description: "user journey" });
196
+ labeler.recordTaskDispatch({ description: "schema migration" });
197
+
198
+ // 3. children emit in arbitrary interleaved order
199
+ const observed: Array<[string, string]> = [];
200
+ for (const session of ["c1", "c2", "p", "c3", "c1", "c4", "c2", "p"]) {
201
+ observed.push([session, labeler.labelFor(session)]);
202
+ }
203
+
204
+ expect(observed).toEqual([
205
+ ["c1", "lens:correctness-invariants"],
206
+ ["c2", "lens:security"],
207
+ ["p", ORCHESTRATOR_LABEL],
208
+ ["c3", "lens:user-journey"],
209
+ ["c1", "lens:correctness-invariants"],
210
+ ["c4", "lens:schema-migration"],
211
+ ["c2", "lens:security"],
212
+ ["p", ORCHESTRATOR_LABEL],
213
+ ]);
214
+
215
+ expect(labeler.size()).toBe(5);
216
+ expect(labeler.pendingDispatchCount()).toBe(0);
217
+ });
218
+ });
219
+
220
+ describe("formatWithLabel", () => {
221
+ test("prefixes a single-line message with magenta-wrapped label", () => {
222
+ const out = formatWithLabel("orchestrator", "hello world");
223
+ expect(out).toContain("[orchestrator]");
224
+ expect(out).toContain("hello world");
225
+ // ANSI magenta + reset markers around the bracketed label (escapes
226
+ // built via fromCharCode to satisfy biome's no-control-character-in-regex)
227
+ const ESC = String.fromCharCode(27);
228
+ expect(out).toMatch(new RegExp(`${ESC}\\[35m\\[orchestrator\\]${ESC}\\[0m hello world$`));
229
+ });
230
+
231
+ test("prefixes every line of a multi-line message", () => {
232
+ const out = formatWithLabel("lens:security", "line one\nline two\nline three");
233
+ const lines = out.split("\n");
234
+ expect(lines).toHaveLength(3);
235
+ for (const line of lines) {
236
+ expect(line).toContain("[lens:security]");
237
+ }
238
+ expect(lines[0]).toContain("line one");
239
+ expect(lines[1]).toContain("line two");
240
+ expect(lines[2]).toContain("line three");
241
+ });
242
+
243
+ test("handles empty input without throwing", () => {
244
+ const out = formatWithLabel("orchestrator", "");
245
+ expect(out).toContain("[orchestrator]");
246
+ });
247
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Track per-session labels so log lines from parallel subagents can be
3
+ * differentiated. The orchestrator dispatches lens subagents
4
+ * via the Task tool; each subagent runs in its own opencode/claude Session
5
+ * with its own `sessionID` (or `session_id`) tag on the NDJSON event stream.
6
+ *
7
+ * Without per-session prefixing, parallel subagent tool_use / tool_result /
8
+ * text events appear as a single interleaved stream tagged with `[Terramend]`,
9
+ * making it impossible for a human reading the logs to attribute work to a
10
+ * specific lens.
11
+ *
12
+ * The labeler is deliberately runtime-agnostic — both opencode.ts and
13
+ * claude.ts feed it the same shape. The contract is FIFO: when the orchestrator
14
+ * dispatches N task tool_use blocks in a single assistant turn (the parallel
15
+ * fan-out the multi-lens prompt requires), the i-th new sessionID is assumed
16
+ * to belong to the i-th task dispatch. This is correct as long as parallel
17
+ * dispatches are emitted in source-order and the runtimes respect that order
18
+ * when assigning child sessions; we do not depend on it for correctness of
19
+ * the read-only contract — only for log readability.
20
+ */
21
+
22
+ export interface TaskDispatchInput {
23
+ description?: string | undefined;
24
+ subagent_type?: string | undefined;
25
+ prompt?: string | undefined;
26
+ }
27
+
28
+ export const ORCHESTRATOR_LABEL = "orchestrator";
29
+
30
+ const LENS_PROMPT_PATTERN = /^\s*(?:lens|Lens|LENS)\s*[:=]\s*([A-Za-z][\w &/.-]{0,60})/m;
31
+
32
+ function slug(value: string): string {
33
+ return value
34
+ .trim()
35
+ .toLowerCase()
36
+ .replace(/[^\w-]+/g, "-")
37
+ .replace(/^-+|-+$/g, "")
38
+ .slice(0, 40);
39
+ }
40
+
41
+ /**
42
+ * Extract a human-readable label from a Task tool's input. Tries (in order):
43
+ * 1. explicit `lens: <name>` marker on a line in the prompt — preferred,
44
+ * lets the orchestrator name the lens deterministically
45
+ * 2. the Task tool's `description` field — short, written by orchestrator
46
+ * per call, usually enough
47
+ * 3. the `subagent_type` — falls back to the named
48
+ * subagent identity when description is missing
49
+ * 4. generic "subagent" — last resort
50
+ */
51
+ export function deriveLabelFromTaskInput(input: TaskDispatchInput): string {
52
+ if (typeof input.prompt === "string") {
53
+ const match = input.prompt.match(LENS_PROMPT_PATTERN);
54
+ if (match?.[1]) {
55
+ const slugged = slug(match[1]);
56
+ if (slugged) return `lens:${slugged}`;
57
+ }
58
+ }
59
+ if (input.description) {
60
+ const slugged = slug(input.description);
61
+ if (slugged) return `lens:${slugged}`;
62
+ }
63
+ if (input.subagent_type) {
64
+ return input.subagent_type;
65
+ }
66
+ return "subagent";
67
+ }
68
+
69
+ /**
70
+ * Stateful tracker mapping subagent activity back to human-readable labels.
71
+ *
72
+ * Two attribution channels are supported because the runtimes differ:
73
+ *
74
+ * - **OpenCode** spawns each subagent as its own opencode `Session` with
75
+ * a distinct `sessionID`. The harness records each Task dispatch into a
76
+ * pending FIFO queue; the next previously-unseen sessionID consumes the
77
+ * head of the queue and binds it to that label.
78
+ *
79
+ * - **Claude Code** runs subagents inside the orchestrator's session — they
80
+ * all share `session_id` — and instead stamps every subagent message with
81
+ * `parent_tool_use_id` pointing at the Agent tool_use id that spawned them.
82
+ * The harness binds each Agent tool_use id to its dispatched label up
83
+ * front, then `labelFor` looks the label up directly when an event arrives
84
+ * carrying that `parent_tool_use_id`.
85
+ *
86
+ * `labelFor(sessionID, parentToolUseId?)` accepts both: when
87
+ * `parentToolUseId` is set and known it short-circuits to the direct mapping;
88
+ * otherwise it falls through to the FIFO/sessionID path.
89
+ */
90
+ export class SessionLabeler {
91
+ private readonly labels = new Map<string, string>();
92
+ private readonly labelsByToolUseId = new Map<string, string>();
93
+ private readonly pendingLabels: string[] = [];
94
+ private fallbackCounter = 0;
95
+
96
+ /**
97
+ * Record a Task/Agent tool dispatch.
98
+ *
99
+ * @param input Task tool input — used to derive the lens label.
100
+ * @param toolUseId Optional Agent tool_use id. When provided, future events
101
+ * carrying `parent_tool_use_id === toolUseId` resolve
102
+ * directly to this label without consuming the FIFO queue
103
+ * (Claude path). Always also pushed to the FIFO queue so
104
+ * the OpenCode path still works when toolUseId is absent.
105
+ */
106
+ recordTaskDispatch(input: TaskDispatchInput, toolUseId?: string | null): string {
107
+ const label = deriveLabelFromTaskInput(input);
108
+ this.pendingLabels.push(label);
109
+ if (toolUseId) this.labelsByToolUseId.set(toolUseId, label);
110
+ return label;
111
+ }
112
+
113
+ /**
114
+ * Return a label for the given event.
115
+ *
116
+ * @param sessionID Session id from the event (OpenCode: per-session;
117
+ * Claude: shared across orchestrator + subagents).
118
+ * @param parentToolUseId Claude's `parent_tool_use_id` — non-null on
119
+ * subagent messages. When set and known, takes
120
+ * priority over the FIFO/sessionID path.
121
+ */
122
+ labelFor(sessionID: string | undefined | null, parentToolUseId?: string | null): string {
123
+ // Claude path: subagent messages carry parent_tool_use_id pointing at
124
+ // the Agent tool_use that spawned them. resolve directly without
125
+ // touching the sessionID-keyed map (which is bound to the orchestrator
126
+ // for the shared session_id and would otherwise misattribute).
127
+ if (parentToolUseId) {
128
+ const direct = this.labelsByToolUseId.get(parentToolUseId);
129
+ if (direct) return direct;
130
+ }
131
+
132
+ if (!sessionID) return ORCHESTRATOR_LABEL;
133
+ const existing = this.labels.get(sessionID);
134
+ if (existing) return existing;
135
+
136
+ let label: string;
137
+ if (this.labels.size === 0) {
138
+ label = ORCHESTRATOR_LABEL;
139
+ } else if (this.pendingLabels.length > 0) {
140
+ label = this.pendingLabels.shift() as string;
141
+ } else {
142
+ this.fallbackCounter += 1;
143
+ label = `subagent#${this.fallbackCounter}`;
144
+ }
145
+ this.labels.set(sessionID, label);
146
+ return label;
147
+ }
148
+
149
+ /** number of distinct sessions seen so far (for diagnostics) */
150
+ size(): number {
151
+ return this.labels.size;
152
+ }
153
+
154
+ /** all (sessionID, label) pairs, oldest first */
155
+ entries(): Array<[string, string]> {
156
+ return Array.from(this.labels.entries());
157
+ }
158
+
159
+ /** how many pending labels are queued waiting to bind to a new session */
160
+ pendingDispatchCount(): number {
161
+ return this.pendingLabels.length;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Format a log message with a session label prefix in magenta. Mirrors the
167
+ * style of utils/log.ts:prefixLines() so per-session prefixes look the same
168
+ * as the dormant withLogPrefix-based ones.
169
+ */
170
+ export function formatWithLabel(label: string, message: string): string {
171
+ const MAGENTA = "\x1b[35m";
172
+ const RESET = "\x1b[0m";
173
+ const colored = `${MAGENTA}[${label}]${RESET} `;
174
+ return message
175
+ .split("\n")
176
+ .map((line) => `${colored}${line}`)
177
+ .join("\n");
178
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { type AgentUsage, mergeAgentUsage } from "#app/agents/shared";
3
+
4
+ const entry = (overrides: Partial<AgentUsage>): AgentUsage => ({
5
+ agent: "terramend",
6
+ inputTokens: 0,
7
+ outputTokens: 0,
8
+ ...overrides,
9
+ });
10
+
11
+ describe("mergeAgentUsage", () => {
12
+ it("returns undefined when both sides are undefined", () => {
13
+ expect(mergeAgentUsage(undefined, undefined)).toBeUndefined();
14
+ });
15
+
16
+ it("returns a copy of b when a is undefined", () => {
17
+ const b = entry({ inputTokens: 10 });
18
+ expect(mergeAgentUsage(undefined, b)).toEqual(b);
19
+ });
20
+
21
+ it("returns a copy of a when b is undefined", () => {
22
+ const a = entry({ inputTokens: 10 });
23
+ expect(mergeAgentUsage(a, undefined)).toEqual(a);
24
+ });
25
+
26
+ it("sums inputTokens and outputTokens unconditionally", () => {
27
+ const merged = mergeAgentUsage(
28
+ entry({ inputTokens: 10, outputTokens: 5 }),
29
+ entry({ inputTokens: 20, outputTokens: 7 }),
30
+ );
31
+ expect(merged?.inputTokens).toBe(30);
32
+ expect(merged?.outputTokens).toBe(12);
33
+ });
34
+
35
+ it("keeps cache/cost fields undefined when both sides lack them", () => {
36
+ // this matters so downstream aggregateUsage doesn't persist spurious 0s into the DB
37
+ const merged = mergeAgentUsage(entry({ inputTokens: 10 }), entry({ inputTokens: 20 }));
38
+ expect(merged?.cacheReadTokens).toBeUndefined();
39
+ expect(merged?.cacheWriteTokens).toBeUndefined();
40
+ expect(merged?.costUsd).toBeUndefined();
41
+ });
42
+
43
+ it("sums cache and cost fields when either side reports them", () => {
44
+ const merged = mergeAgentUsage(
45
+ entry({ inputTokens: 10, cacheReadTokens: 100, costUsd: 0.01 }),
46
+ entry({ inputTokens: 20, cacheWriteTokens: 50, costUsd: 0.02 }),
47
+ );
48
+ expect(merged?.cacheReadTokens).toBe(100);
49
+ expect(merged?.cacheWriteTokens).toBe(50);
50
+ expect(merged?.costUsd).toBeCloseTo(0.03, 10);
51
+ });
52
+
53
+ it("preserves the agent id of the left operand", () => {
54
+ // the aggregator is called inside a single agent's run() — the agent label
55
+ // is a fixed property of the harness, not something that can flip mid-run
56
+ const merged = mergeAgentUsage(
57
+ entry({ agent: "claude", inputTokens: 10 }),
58
+ entry({ agent: "something-else", inputTokens: 20 }),
59
+ );
60
+ expect(merged?.agent).toBe("claude");
61
+ });
62
+
63
+ it("returns a fresh object rather than the input reference", () => {
64
+ // callers treat AgentUsage as immutable; returning the input itself would
65
+ // leak that invariant. mutating the returned value must not affect inputs.
66
+ const a = entry({ inputTokens: 10 });
67
+ const mergedWithUndef = mergeAgentUsage(a, undefined);
68
+ expect(mergedWithUndef).not.toBe(a);
69
+ expect(mergedWithUndef).toEqual(a);
70
+
71
+ const b = entry({ inputTokens: 20 });
72
+ const mergedFromUndef = mergeAgentUsage(undefined, b);
73
+ expect(mergedFromUndef).not.toBe(b);
74
+ expect(mergedFromUndef).toEqual(b);
75
+ });
76
+ });