carthagent 0.0.0-stage → 1.0.7

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 (379) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -2
  3. package/bin/ctg.mjs +736 -0
  4. package/docs/budgets.md +72 -0
  5. package/docs/compaction.md +34 -0
  6. package/docs/context-efficiency-plan.md +24 -0
  7. package/docs/context-efficiency-review.md +27 -0
  8. package/docs/context-efficiency.md +49 -0
  9. package/docs/deepseek-jev-benchmark-plan.md +70 -0
  10. package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
  11. package/docs/deepseek-jev-benchmark-restart.md +9 -0
  12. package/docs/deepseek-jev-benchmark-results.md +77 -0
  13. package/docs/deepseek-jev-benchmark-running.md +60 -0
  14. package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
  15. package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
  16. package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
  17. package/docs/deepseek-jev-http400-diagnosis.md +45 -0
  18. package/docs/delivery-review-evidence.md +53 -0
  19. package/docs/delivery-timeout-repair-plan.md +51 -0
  20. package/docs/evaluation-readiness.md +31 -0
  21. package/docs/evaluation.md +70 -0
  22. package/docs/extending.md +354 -0
  23. package/docs/glm-pilot-execution.md +9 -0
  24. package/docs/glm-pilot-plan.md +15 -0
  25. package/docs/glm-pilot-results.md +71 -0
  26. package/docs/graph-context-optimization.md +15 -0
  27. package/docs/jev-repository-pilot-plan.md +25 -0
  28. package/docs/jev-repository-pilot-protocol.md +39 -0
  29. package/docs/jev-repository-pilot-restart.md +7 -0
  30. package/docs/jev-repository-pilot-results.md +79 -0
  31. package/docs/jev.md +32 -0
  32. package/docs/model-registry.md +25 -0
  33. package/docs/native-compatibility.md +117 -0
  34. package/docs/opencode-archive.md +9 -0
  35. package/docs/opencode-migration.md +115 -0
  36. package/docs/opencode-runtime.md +94 -0
  37. package/docs/opencode-workspace.md +31 -0
  38. package/docs/owned-runtime-workflow-registration.md +17 -0
  39. package/docs/plan-advisor-evaluation.md +38 -0
  40. package/docs/planning-startup-fix-plan.md +23 -0
  41. package/docs/planning-startup-fix-review.md +24 -0
  42. package/docs/planning.md +118 -0
  43. package/docs/production-merge-review.md +23 -0
  44. package/docs/quality.md +35 -0
  45. package/docs/repository-benchmark-design.md +111 -0
  46. package/docs/review-assurance.md +9 -0
  47. package/docs/review-experiment-results.md +59 -0
  48. package/docs/tested-planning-roadmap.md +58 -0
  49. package/docs/tested-planning.md +58 -0
  50. package/docs/tui-pickers.md +9 -0
  51. package/docs/webdriver.md +83 -0
  52. package/docs/workflow-modes.md +26 -0
  53. package/docs/workflow.d2 +17 -0
  54. package/docs/workflow.svg +149 -0
  55. package/docs/workspace-coordination.md +9 -0
  56. package/evaluate.mjs +249 -0
  57. package/extensions/codegraph.ts +36 -0
  58. package/extensions/delivery.ts +819 -0
  59. package/extensions/experiential.ts +21 -0
  60. package/extensions/subscriptions.ts +21 -0
  61. package/guidance/profiles/authenticated-web.json +24 -0
  62. package/guidance/profiles/data-visualization.json +25 -0
  63. package/guidance/profiles/database-change.json +24 -0
  64. package/guidance/profiles/external-api.json +24 -0
  65. package/guidance/profiles/transactional-data.json +25 -0
  66. package/guidance/profiles/web-application.json +26 -0
  67. package/lib/acceptance-review.mjs +4 -0
  68. package/lib/browser-bin.mjs +160 -0
  69. package/lib/browser-errors.mjs +54 -0
  70. package/lib/browser.mjs +219 -0
  71. package/lib/budget.mjs +37 -0
  72. package/lib/cdp.mjs +221 -0
  73. package/lib/cloud/client.mjs +275 -0
  74. package/lib/cloud/control-plane.mjs +350 -0
  75. package/lib/cloud/gateway.mjs +380 -0
  76. package/lib/cloud/http.mjs +135 -0
  77. package/lib/cloud/ledger.mjs +215 -0
  78. package/lib/codegraph-query.mjs +60 -0
  79. package/lib/codegraph.mjs +378 -0
  80. package/lib/context-archive.mjs +18 -0
  81. package/lib/delivery-context.mjs +132 -0
  82. package/lib/delivery-guidance.mjs +9 -0
  83. package/lib/delivery-review.mjs +94 -0
  84. package/lib/delivery.mjs +610 -0
  85. package/lib/engine.mjs +137 -0
  86. package/lib/guidance.mjs +71 -0
  87. package/lib/jev-advisor.mjs +47 -0
  88. package/lib/opencode/archive.mjs +122 -0
  89. package/lib/opencode/compatibility.mjs +19 -0
  90. package/lib/opencode/credentials.mjs +205 -0
  91. package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
  92. package/lib/opencode/evaluation.mjs +242 -0
  93. package/lib/opencode/file-lock.mjs +86 -0
  94. package/lib/opencode/headless.mjs +200 -0
  95. package/lib/opencode/review.mjs +213 -0
  96. package/lib/opencode/runtime.mjs +246 -0
  97. package/lib/opencode/service.mjs +249 -0
  98. package/lib/opencode/session-options.mjs +60 -0
  99. package/lib/opencode/workspace.mjs +133 -0
  100. package/lib/openrouter-jev.mjs +37 -0
  101. package/lib/outcome-contract.mjs +21 -0
  102. package/lib/plan-advisor.mjs +38 -0
  103. package/lib/planning-access.mjs +7 -0
  104. package/lib/planning-advice.mjs +28 -0
  105. package/lib/planning-discovery.mjs +14 -0
  106. package/lib/planning-guide.mjs +34 -0
  107. package/lib/planning-view.mjs +57 -0
  108. package/lib/planning.mjs +155 -0
  109. package/lib/probe-diagnostics.mjs +13 -0
  110. package/lib/providers/experiential.mjs +292 -0
  111. package/lib/providers/model-catalog.json +700 -0
  112. package/lib/providers/model-catalog.mjs +7 -0
  113. package/lib/providers/names.mjs +13 -0
  114. package/lib/providers/subscription-models.mjs +483 -0
  115. package/lib/reports.mjs +80 -0
  116. package/lib/review-state.mjs +49 -0
  117. package/lib/review.mjs +114 -0
  118. package/lib/scenarios.mjs +99 -0
  119. package/lib/ship-auth.mjs +44 -0
  120. package/lib/ship-jev.mjs +59 -0
  121. package/lib/ship-telemetry.mjs +183 -0
  122. package/lib/tui/ansi.mjs +255 -0
  123. package/lib/tui/app.mjs +2445 -0
  124. package/lib/tui/auth.mjs +249 -0
  125. package/lib/tui/carthage.mjs +109 -0
  126. package/lib/tui/clipboard.mjs +122 -0
  127. package/lib/tui/d2.mjs +431 -0
  128. package/lib/tui/feed.mjs +467 -0
  129. package/lib/tui/framing.mjs +80 -0
  130. package/lib/tui/glyphs.mjs +172 -0
  131. package/lib/tui/hash-worker.mjs +11 -0
  132. package/lib/tui/mock-engine.mjs +222 -0
  133. package/lib/tui/models.mjs +73 -0
  134. package/lib/tui/picker.mjs +18 -0
  135. package/lib/tui/theme.mjs +663 -0
  136. package/lib/tui/title.mjs +7 -0
  137. package/lib/update-channel.mjs +62 -0
  138. package/lib/update.mjs +230 -0
  139. package/lib/webdriver.mjs +82 -0
  140. package/lib/workflow.mjs +88 -0
  141. package/lib/workspace.mjs +39 -0
  142. package/metadata.json +8 -0
  143. package/package.json +94 -4
  144. package/plugins/carthagent/index.ts +3 -0
  145. package/plugins/carthagent/package.json +28 -0
  146. package/plugins/carthagent/src/archive-rpc.ts +46 -0
  147. package/plugins/carthagent/src/bootstrap.ts +38 -0
  148. package/plugins/carthagent/src/index.ts +62 -0
  149. package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
  150. package/plugins/carthagent/src/rpc.ts +76 -0
  151. package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
  152. package/plugins/carthagent/src/server/budgets.ts +21 -0
  153. package/plugins/carthagent/src/server/codegraph.ts +46 -0
  154. package/plugins/carthagent/src/server/delivery.ts +488 -0
  155. package/plugins/carthagent/src/server/device-auth.ts +91 -0
  156. package/plugins/carthagent/src/server/native-delivery.ts +26 -0
  157. package/plugins/carthagent/src/server/orchestration.ts +56 -0
  158. package/plugins/carthagent/src/server/policy.ts +24 -0
  159. package/plugins/carthagent/src/server/providers.ts +424 -0
  160. package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
  161. package/plugins/carthagent/src/server/session-controls.ts +82 -0
  162. package/plugins/carthagent/src/server/telemetry.ts +92 -0
  163. package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
  164. package/plugins/carthagent/src/session-rpc.ts +31 -0
  165. package/plugins/carthagent/src/testing.ts +85 -0
  166. package/plugins/carthagent/src/workspace-rpc.ts +133 -0
  167. package/plugins/carthagent/test-plugin/index.ts +4 -0
  168. package/plugins/carthagent/test-plugin/package.json +1 -0
  169. package/plugins/carthagent/tsconfig.json +19 -0
  170. package/plugins/carthagent/tui.ts +1 -0
  171. package/prompts/delivery.md +7 -0
  172. package/public/index.html +983 -0
  173. package/scenarios/cli/grade_cli.py +69 -0
  174. package/scenarios/cli/scenario.json +21 -0
  175. package/scenarios/game/baseline/minecraft.py +193 -0
  176. package/scenarios/game/context.md +1 -0
  177. package/scenarios/game/grade_minecraft_clone.py +603 -0
  178. package/scenarios/game/requirements.txt +2 -0
  179. package/scenarios/game/scenario.json +33 -0
  180. package/server.mjs +341 -0
  181. package/skills/game-development/SKILL.md +36 -0
  182. package/skills/game-development/assets/ursina_starter.py +98 -0
  183. package/skills/game-development/scripts/verify_ursina.py +80 -0
  184. package/skills/software-delivery/SKILL.md +50 -0
  185. package/skills/typescript-delivery/SKILL.md +69 -0
  186. package/tests/acceptance-review.test.mjs +13 -0
  187. package/tests/auth.test.mjs +234 -0
  188. package/tests/bounded-summary.test.mjs +68 -0
  189. package/tests/browser-bin.test.mjs +91 -0
  190. package/tests/browser.test.mjs +96 -0
  191. package/tests/budget-engine.test.mjs +110 -0
  192. package/tests/budget.test.mjs +23 -0
  193. package/tests/cli.test.mjs +245 -0
  194. package/tests/clipboard.test.mjs +79 -0
  195. package/tests/cloud-client.test.mjs +129 -0
  196. package/tests/cloud-control-plane.test.mjs +144 -0
  197. package/tests/cloud-gateway.test.mjs +147 -0
  198. package/tests/cloud-http.test.mjs +102 -0
  199. package/tests/cloud-ledger.test.mjs +75 -0
  200. package/tests/codegraph-query.test.mjs +66 -0
  201. package/tests/codegraph.test.mjs +142 -0
  202. package/tests/delivery-context.test.mjs +66 -0
  203. package/tests/delivery-review.test.mjs +139 -0
  204. package/tests/delivery.test.mjs +343 -0
  205. package/tests/engine-api.mjs +24 -0
  206. package/tests/experiential-extension.test.mjs +37 -0
  207. package/tests/experiential.test.mjs +300 -0
  208. package/tests/extension.test.mjs +921 -0
  209. package/tests/fixtures/archive-plugin/index.ts +10 -0
  210. package/tests/guidance.test.mjs +78 -0
  211. package/tests/model-registry.test.mjs +68 -0
  212. package/tests/models.test.mjs +70 -0
  213. package/tests/openai-idempotency.test.mjs +99 -0
  214. package/tests/opencode-archive-rpc.test.mjs +82 -0
  215. package/tests/opencode-delivery.test.mjs +231 -0
  216. package/tests/opencode-providers.test.mjs +414 -0
  217. package/tests/opencode-runtime.test.mjs +455 -0
  218. package/tests/opencode-session-controls.test.mjs +115 -0
  219. package/tests/opencode-session-options.test.mjs +26 -0
  220. package/tests/opencode-workspace.test.mjs +339 -0
  221. package/tests/outcome-contract.test.mjs +17 -0
  222. package/tests/picker.test.mjs +34 -0
  223. package/tests/plan-advisor.test.mjs +88 -0
  224. package/tests/plan-review-loop.test.mjs +53 -0
  225. package/tests/planning-advice.test.mjs +27 -0
  226. package/tests/planning-contract.test.mjs +65 -0
  227. package/tests/planning-startup.test.mjs +38 -0
  228. package/tests/planning-view.test.mjs +33 -0
  229. package/tests/planning.test.mjs +223 -0
  230. package/tests/probe-diagnostics.test.mjs +21 -0
  231. package/tests/pull-request-rpc.test.mjs +366 -0
  232. package/tests/review-state.test.mjs +32 -0
  233. package/tests/review.test.mjs +127 -0
  234. package/tests/server.test.mjs +252 -0
  235. package/tests/ship-jev.test.mjs +77 -0
  236. package/tests/ship-telemetry.test.mjs +333 -0
  237. package/tests/standard-workflow.test.mjs +28 -0
  238. package/tests/subscription-models.test.mjs +416 -0
  239. package/tests/subscriptions-extension.test.mjs +27 -0
  240. package/tests/terminal-title.test.mjs +13 -0
  241. package/tests/test_grade_minecraft_clone.py +199 -0
  242. package/tests/test_starter.py +78 -0
  243. package/tests/theme.test.mjs +175 -0
  244. package/tests/tui-copy-paste.test.mjs +54 -0
  245. package/tests/tui.test.mjs +808 -0
  246. package/tests/update-channels.test.mjs +129 -0
  247. package/tests/update.test.mjs +175 -0
  248. package/tests/webdriver.test.mjs +31 -0
  249. package/tests/workflow.test.mjs +47 -0
  250. package/tests/workspace-rpc.test.mjs +236 -0
  251. package/tests/workspace.test.mjs +24 -0
  252. package/tools/benchmark/advisor.test.mjs +7 -0
  253. package/tools/benchmark/advisor.ts +27 -0
  254. package/tools/benchmark/campaign.mjs +45 -0
  255. package/tools/benchmark/diagnose-request.mjs +40 -0
  256. package/tools/benchmark/eval-common.mjs +32 -0
  257. package/tools/benchmark/eval-ledger.mjs +22 -0
  258. package/tools/benchmark/eval-shop.mjs +26 -0
  259. package/tools/benchmark/eval-voxel.mjs +20 -0
  260. package/tools/benchmark/evaluate.mjs +24 -0
  261. package/tools/benchmark/evaluators.test.mjs +13 -0
  262. package/tools/benchmark/fixtures/shop/server.mjs +18 -0
  263. package/tools/benchmark/fixtures/shop/ui.html +8 -0
  264. package/tools/benchmark/freeze.mjs +30 -0
  265. package/tools/benchmark/gateway.mjs +91 -0
  266. package/tools/benchmark/gateway.test.mjs +38 -0
  267. package/tools/benchmark/generator.mjs +11 -0
  268. package/tools/benchmark/generator.test.mjs +26 -0
  269. package/tools/benchmark/guard.test.mjs +133 -0
  270. package/tools/benchmark/guard.ts +136 -0
  271. package/tools/benchmark/native-plugin/index.ts +24 -0
  272. package/tools/benchmark/planning-startup.test.mjs +47 -0
  273. package/tools/benchmark/preflight-live.mjs +31 -0
  274. package/tools/benchmark/preflight.mjs +22 -0
  275. package/tools/benchmark/replay-diagnostic.mjs +23 -0
  276. package/tools/benchmark/runner.mjs +141 -0
  277. package/tools/benchmark/runner.test.mjs +93 -0
  278. package/tools/benchmark/runtime.mjs +23 -0
  279. package/tools/benchmark/selftest.mjs +47 -0
  280. package/tools/benchmark/smoke-jev.mjs +5 -0
  281. package/tools/benchmark/tasks.mjs +20 -0
  282. package/tools/benchmark/transport.mjs +65 -0
  283. package/tools/benchmark/transport.test.mjs +44 -0
  284. package/tools/benchmark/upstream-error.mjs +18 -0
  285. package/tools/benchmark/upstream-error.test.mjs +10 -0
  286. package/tools/browser-check.mjs +109 -0
  287. package/tools/check-syntax.mjs +27 -0
  288. package/tools/evaluate-plan-advisor.mjs +20 -0
  289. package/tools/native-fixture.mjs +194 -0
  290. package/tools/native-pty-smoke.py +108 -0
  291. package/tools/native-report-plugin/index.ts +50 -0
  292. package/tools/native-report-plugin/package.json +7 -0
  293. package/tools/patch-compaction.mjs +24 -0
  294. package/tools/patch-model-catalog.mjs +12 -0
  295. package/tools/patch-print-shutdown.mjs +39 -0
  296. package/tools/repo-pilot/evaluator.cjs +32 -0
  297. package/tools/repo-pilot/reference.mjs +43 -0
  298. package/tools/repo-pilot/runner.mjs +85 -0
  299. package/tools/repo-pilot/sandbox-entry.mjs +12 -0
  300. package/tools/repo-pilot/tasks.mjs +20 -0
  301. package/tools/repo-pilot/type-evaluator.cjs +6 -0
  302. package/tools/test-native-controls.mjs +84 -0
  303. package/tools/test-native-delivery-host.mjs +307 -0
  304. package/tools/test-native-evaluation.mjs +46 -0
  305. package/tools/test-native-host.mjs +385 -0
  306. package/tools/test-native-outcomes.mjs +73 -0
  307. package/tools/test-native-package.mjs +70 -0
  308. package/tools/test-native-pty.mjs +20 -0
  309. package/tools/test-native-report.mjs +88 -0
  310. package/tools/test-native-swe-host.mjs +132 -0
  311. package/tools/test-native-swe.mjs +122 -0
  312. package/tools/test-picker-pty.py +45 -0
  313. package/tools/test-python.mjs +29 -0
  314. package/tools/test-webdriver.mjs +54 -0
  315. package/tools/verify-release-version.mjs +37 -0
  316. package/tools/webdriver-check.mjs +52 -0
  317. package/vendor/agent/bounded-summary.mjs +88 -0
  318. package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
  319. package/vendor/agent/chunks/anthropic.js +75 -0
  320. package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
  321. package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
  322. package/vendor/agent/chunks/chord-context.js +96 -0
  323. package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
  324. package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
  325. package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
  326. package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
  327. package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
  328. package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
  329. package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
  330. package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
  331. package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
  332. package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
  333. package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
  334. package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
  335. package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
  336. package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
  337. package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
  338. package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
  339. package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
  340. package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
  341. package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
  342. package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
  343. package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
  344. package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
  345. package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
  346. package/vendor/agent/chunks/github-copilot.js +2 -0
  347. package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
  348. package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
  349. package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
  350. package/vendor/agent/chunks/image-resize-worker.js +2 -0
  351. package/vendor/agent/chunks/kimi-coding.js +2 -0
  352. package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
  353. package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
  354. package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
  355. package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
  356. package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
  357. package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
  358. package/vendor/agent/chunks/openai-codex.js +75 -0
  359. package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
  360. package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
  361. package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
  362. package/vendor/agent/chunks/openrouter.js +75 -0
  363. package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
  364. package/vendor/agent/chunks/radius.js +75 -0
  365. package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
  366. package/vendor/agent/chunks/xai.js +2 -0
  367. package/vendor/agent/cli.js +3 -0
  368. package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
  369. package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
  370. package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
  371. package/vendor/agent/index.js +2 -0
  372. package/vendor/agent/modes/interactive/theme/dark.json +91 -0
  373. package/vendor/agent/modes/interactive/theme/light.json +90 -0
  374. package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
  375. package/vendor/agent/package.json +14 -0
  376. package/vendor/agent/rpc-entry.js +3 -0
  377. package/vendor/agent/theme/dark.json +91 -0
  378. package/vendor/agent/theme/light.json +90 -0
  379. package/vendor/agent/theme/theme-schema.json +356 -0
@@ -0,0 +1,343 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync, readFileSync, utimesSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { checkDigest, fingerprint, validatePlan, planD2, pendingChecks, restoreState, runCommand, shouldContinue, localPath, createSerialQueue, validateRevision, turnBudgetExceeded, maxEvidenceFiles, maxEvidenceBytes, bindRequiredChecks, verificationMode, looksInformational, computePhase } from '../lib/delivery.mjs';
7
+ import { createReport, latestReport, saveReport, reconcileReport } from '../lib/reports.mjs';
8
+
9
+ function fixture(t) {
10
+ const cwd = mkdtempSync(join(tmpdir(), 'carthagent delivery spaces '));
11
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
12
+ writeFileSync(join(cwd, 'app.py'), 'print(1)');
13
+ return cwd;
14
+ }
15
+ function plan() { return { goal: 'Working app', assumptions: [], artifacts: ['app.py'], steps: ['Launch', 'Implement and test'], acceptance: [{ requirement: 'Runs', checks: ['smoke'] }], checks: [{ id: 'smoke', kind: 'runtime', argv: [process.execPath, '-e', 'console.log("ok")'], timeoutSeconds: 2 }] }; }
16
+
17
+ test('contract validates mapping, IDs, deadlines and meaningful checks', t => {
18
+ const cwd = fixture(t);
19
+ assert.deepEqual(validatePlan(plan(), cwd), plan());
20
+ for (const mutate of [p => p.acceptance[0].checks.push('missing'), p => p.checks.push(p.checks[0]), p => p.checks[0].timeoutSeconds = 0, p => p.checks[0].kind = 'static', p => p.artifacts = ['../outside']]) {
21
+ const p = plan(); mutate(p); assert.throws(() => validatePlan(p, cwd));
22
+ }
23
+ });
24
+ test('D2 escapes arbitrary labels and records repair loop', () => {
25
+ const p = plan(); p.steps[0] = 'Quoted "value"\nand newline';
26
+ const d2 = planD2(p);
27
+ assert.ok(d2.includes('\\"value\\"\\nand newline')); assert.ok(d2.includes('repair -> verify'));
28
+ });
29
+ test('fingerprints detect edits, additions and deletion; ignore generated evidence', t => {
30
+ const cwd = fixture(t), initial = fingerprint(cwd, ['.']);
31
+ mkdirSync(join(cwd, 'artifacts')); writeFileSync(join(cwd, 'artifacts', 'frame.png'), 'frame');
32
+ assert.equal(fingerprint(cwd, ['.']), initial);
33
+ writeFileSync(join(cwd, 'test.py'), 'assert True');
34
+ assert.notEqual(fingerprint(cwd, ['.']), initial);
35
+ rmSync(join(cwd, 'test.py')); assert.equal(fingerprint(cwd, ['.']), initial);
36
+ writeFileSync(join(cwd, 'app.py'), 'print(2)'); assert.notEqual(fingerprint(cwd, ['.']), initial);
37
+ });
38
+ test('evidence cannot pass on missing, failed or stale checks', () => {
39
+ const state = { runId: 'run-a', revision: 1, plan: plan(), evidence: {} };
40
+ assert.deepEqual(pendingChecks(state, 'new'), ['smoke']);
41
+ state.evidence.smoke = { passed: true, fingerprint: 'old' };
42
+ assert.deepEqual(pendingChecks(state, 'new'), ['smoke']);
43
+ state.evidence.smoke.fingerprint = 'new';
44
+ assert.deepEqual(pendingChecks(state, 'new'), ['smoke'], 'legacy evidence must rerun');
45
+ state.evidence.smoke.checkDigest = checkDigest(state.plan.checks[0]);
46
+ assert.deepEqual(pendingChecks(state, 'new'), ['smoke']);
47
+ Object.assign(state.evidence.smoke, { runId: state.runId, revision: state.revision });
48
+ assert.deepEqual(pendingChecks(state, 'new'), []);
49
+ assert.deepEqual(pendingChecks({ ...state, runId: 'run-b' }, 'new'), ['smoke']);
50
+ assert.deepEqual(pendingChecks({ ...state, revision: 2 }, 'new'), ['smoke']);
51
+ const restored = restoreState([{ type: 'custom', customType: 'delivery-state-v1', data: state }]);
52
+ assert.deepEqual(pendingChecks(restored, 'new'), []);
53
+ restored.revision++;
54
+ assert.deepEqual(pendingChecks(restored, 'new'), ['smoke']);
55
+ assert.deepEqual(pendingChecks(state, 'new'), [], 'restoration must not mutate original identity');
56
+ const original = structuredClone(state.plan.checks[0]);
57
+ for (const change of [{ argv: ['node', '-e', 'throw 1'] }, { timeoutSeconds: 99 }, { kind: 'static' }]) {
58
+ state.plan.checks[0] = { ...original, ...change };
59
+ assert.deepEqual(pendingChecks(state, 'new'), ['smoke']);
60
+ }
61
+ state.plan.checks[0] = original;
62
+ state.evidence.smoke.passed = false; assert.deepEqual(pendingChecks(state, 'new'), ['smoke']);
63
+ });
64
+ test('branch restoration only uses provided active-branch entries and clones state', () => {
65
+ const entry = { type: 'custom', customType: 'delivery-state-v1', data: { status: 'implementing' } };
66
+ assert.equal(restoreState([]), null);
67
+ const restored = restoreState([entry, { type: 'message' }]);
68
+ restored.status = 'verified'; assert.equal(entry.data.status, 'implementing');
69
+ });
70
+ test('follow-ups are bounded and respect abort, errors, blocking and user queues', () => {
71
+ const args = { state: null, touched: true, nudges: 0, stopReason: 'stop', pendingMessages: false, fresh: false };
72
+ assert.equal(shouldContinue(args), true);
73
+ for (const change of [{ nudges: 2 }, { stopReason: 'aborted' }, { stopReason: 'error' }, { pendingMessages: true }, { touched: false }, { state: { status: 'blocked' } }, { state: { status: 'verified' }, fresh: true }]) assert.equal(shouldContinue({ ...args, ...change }), false);
74
+ assert.equal(shouldContinue({ ...args, state: { status: 'verified' }, fresh: false }), true);
75
+ });
76
+ test('argv execution captures output without shell expansion, including spaces', async t => {
77
+ const cwd = fixture(t), logPath = join(cwd, '.harness', 'ok.log');
78
+ const result = await runCommand([process.execPath, '-e', 'console.log(process.argv[1])', 'literal $HOME ; echo bad'], { cwd, logPath });
79
+ assert.equal(result.code, 0); assert.match(result.output, /literal \$HOME ; echo bad/);
80
+ assert.match(readFileSync(logPath, 'utf8'), /literal/);
81
+ });
82
+ test('nonzero exit and missing executable cannot be successful', async t => {
83
+ const cwd = fixture(t);
84
+ const fail = await runCommand([process.execPath, '-e', 'process.exit(7)'], { cwd, logPath: join(cwd, 'fail.log') });
85
+ assert.equal(fail.code, 7);
86
+ const missing = await runCommand(['definitely-no-such-delivery-command'], { cwd, logPath: join(cwd, 'missing.log') });
87
+ assert.notEqual(missing.code, 0); assert.match(missing.output, /ENOENT/);
88
+ });
89
+ test('timeout terminates long-lived check; cancellation before launch is cheap', async t => {
90
+ const cwd = fixture(t);
91
+ const result = await runCommand([process.execPath, '-e', 'setInterval(()=>{},1000)'], { cwd, timeoutSeconds: .15, logPath: join(cwd, 'timeout.log') });
92
+ assert.equal(result.timedOut, true); assert.notEqual(result.code, 0);
93
+ const signal = AbortSignal.abort();
94
+ const cancelled = await runCommand(['never-launched'], { cwd, signal, logPath: join(cwd, 'cancel.log') });
95
+ assert.equal(cancelled.cancelled, true);
96
+ });
97
+ test('generated-only scopes cannot create empty fingerprints as evidence', t => {
98
+ const cwd = fixture(t);
99
+ for (const root of ['artifacts/', './artifacts', '.harness', '.venv', 'nested/node_modules']) {
100
+ const p = plan(); p.artifacts = [root];
101
+ assert.throws(() => validatePlan(p, cwd), /Artifact roots/);
102
+ }
103
+ });
104
+ test('replanning cannot silently drop requirements', () => {
105
+ const previous = { plan: plan(), status: 'verifying' };
106
+ const next = plan(); next.acceptance = [{ requirement: 'A weaker requirement', checks: ['smoke'] }];
107
+ assert.throws(() => validateRevision(previous, next), /remove acceptance/);
108
+ assert.doesNotThrow(() => validateRevision(previous, plan()));
109
+ });
110
+ test('parallel delivery operations serialize and recover after failure', async () => {
111
+ const enqueue = createSerialQueue(), order = [];
112
+ const a = enqueue(async () => { order.push('a-start'); await new Promise(r => setTimeout(r, 15)); order.push('a-end'); });
113
+ const b = enqueue(async () => { order.push('b'); throw Error('expected'); });
114
+ const c = enqueue(async () => { order.push('c'); return 7; });
115
+ await a; await assert.rejects(b, /expected/); assert.equal(await c, 7);
116
+ assert.deepEqual(order, ['a-start', 'a-end', 'b', 'c']);
117
+ });
118
+ test('local path boundary is not fooled by sibling prefixes', t => {
119
+ const cwd = fixture(t); assert.throws(() => localPath(cwd, cwd + '-sibling/file'));
120
+ assert.equal(localPath(cwd, '@app.py'), join(cwd, 'app.py'));
121
+ });
122
+ test('provider errors do not consume the productive turn budget', () => {
123
+ // Observed live: 91 turn starts included 13 zero-token connection-error turns that
124
+ // consumed the old budget. Only productive turns may trip the limit; wall-clock still bounds.
125
+ assert.equal(turnBudgetExceeded({ turns: 91, providerErrors: 13, maxTurns: 90 }), false);
126
+ assert.equal(turnBudgetExceeded({ turns: 91, providerErrors: 0, maxTurns: 90 }), true);
127
+ // The 91st start with one wasted error turn is exactly 90 productive turns: allowed.
128
+ assert.equal(turnBudgetExceeded({ turns: 91, providerErrors: 1, maxTurns: 90 }), false);
129
+ assert.equal(turnBudgetExceeded({ turns: 92, providerErrors: 1, maxTurns: 90 }), true);
130
+ assert.equal(turnBudgetExceeded({ turns: 10, providerErrors: 13, maxTurns: 90 }), false);
131
+ assert.equal(turnBudgetExceeded({ turns: 0, providerErrors: 0, maxTurns: 0 }), false);
132
+ assert.equal(turnBudgetExceeded({ turns: 1, providerErrors: 0, maxTurns: 0 }), true);
133
+ for (const bad of [{ turns: 1.5, providerErrors: 0, maxTurns: 1 }, { turns: 1, providerErrors: -1, maxTurns: 1 }, { turns: 1, providerErrors: 0, maxTurns: -1 }])
134
+ assert.throws(() => turnBudgetExceeded(bad));
135
+ });
136
+ test('runCommand works without a log path', async t => {
137
+ const cwd = fixture(t);
138
+ const result = await runCommand([process.execPath, '-e', 'console.log("ok")'], { cwd });
139
+ assert.equal(result.code, 0);
140
+ assert.match(result.output, /ok/);
141
+ assert.equal(result.logPath, undefined);
142
+ });
143
+ test('delivery reports are persisted and the newest report is discovered', t => {
144
+ const cwd = fixture(t);
145
+ const first = createReport(cwd, { status: 'implementing', plan: plan(), evidence: {} }, 'session-a');
146
+ const second = createReport(cwd, { status: 'verified', plan: plan(), evidence: {} }, 'session-b');
147
+ const state = { ...second.state, review: 'reviewed' };
148
+ saveReport(second.path, state);
149
+ const newer = new Date(Date.now() + 1000);
150
+ utimesSync(second.path, newer, newer);
151
+ const latest = latestReport(cwd);
152
+ assert.equal(latest.path, second.path);
153
+ assert.equal(latest.state.review, 'reviewed');
154
+ assert.match(readFileSync(join(first.dir, 'plan.d2'), 'utf8'), /Working app/);
155
+ });
156
+
157
+ test('report writes reject stale snapshots and upgrade legacy reports', t => {
158
+ const cwd = fixture(t);
159
+ const report = createReport(cwd, { status: 'implementing', plan: plan(), evidence: {} });
160
+ const stale = structuredClone(report.state);
161
+ report.state.review = 'newer café';
162
+ saveReport(report.path, report.state);
163
+ assert.throws(() => saveReport(report.path, stale), /write conflict/);
164
+ assert.equal(JSON.parse(readFileSync(report.path)).review, 'newer café');
165
+ saveReport(report.path, report.state); // conflict released the lock
166
+ const legacyPath = join(cwd, 'legacy.json');
167
+ writeFileSync(legacyPath, JSON.stringify({ status: 'idle' }));
168
+ const legacy = { status: 'idle' };
169
+ saveReport(legacyPath, legacy);
170
+ assert.equal(legacy.storageVersion, 1);
171
+ });
172
+
173
+ test('same-run reconciliation adopts newer disk state and rejects corrupt identity or versions', t => {
174
+ const cwd = fixture(t);
175
+ const report = createReport(cwd, { status: 'implementing', evidence: {} });
176
+ const restored = structuredClone(report.state);
177
+ report.state.status = 'blocked';
178
+ saveReport(report.path, report.state);
179
+ assert.equal(reconcileReport(report.path, restored).status, 'blocked');
180
+ assert.equal(restored.status, 'implementing');
181
+ assert.equal(reconcileReport(join(cwd, 'missing.json'), restored), restored);
182
+ assert.throws(() => reconcileReport(report.path, { ...restored, runId: 'other' }), /identity mismatch/);
183
+ assert.throws(() => reconcileReport(report.path, { ...restored, storageVersion: 999 }), /regressed/);
184
+ assert.throws(() => reconcileReport(report.path, { ...restored, storageVersion: -1 }), /Invalid report/);
185
+ writeFileSync(report.path, '{broken');
186
+ assert.throws(() => reconcileReport(report.path, restored), SyntaxError);
187
+ });
188
+
189
+ test('simultaneous subprocess report writers cannot overwrite each other', async t => {
190
+ const cwd = fixture(t);
191
+ const report = createReport(cwd, { status: 'implementing', evidence: {} });
192
+ const moduleUrl = new URL('../lib/reports.mjs', import.meta.url).href;
193
+ const snapshot = JSON.stringify(report.state);
194
+ const results = await Promise.all(['one', 'two'].map(review => runCommand([
195
+ process.execPath, '--input-type=module', '-e',
196
+ `import {saveReport} from ${JSON.stringify(moduleUrl)}; const state = ${snapshot}; state.review = ${JSON.stringify(review)}; saveReport(${JSON.stringify(report.path)}, state);`
197
+ ], { cwd, timeoutSeconds: 10 })));
198
+ assert.equal(results.filter(result => result.code === 0).length, 1);
199
+ assert.match(results.find(result => result.code !== 0).output, /write conflict|Report is locked/);
200
+ const saved = JSON.parse(readFileSync(report.path));
201
+ assert.equal(saved.storageVersion, 2);
202
+ saveReport(report.path, saved);
203
+ assert.equal(saved.storageVersion, 3);
204
+ });
205
+
206
+ test('fingerprint respects maxEvidenceFiles and CARTHAGENT_MAX_FILES override', t => {
207
+ const cwd = fixture(t);
208
+ const oldEnv = process.env.CARTHAGENT_MAX_FILES;
209
+ t.after(() => {
210
+ if (oldEnv === undefined) delete process.env.CARTHAGENT_MAX_FILES;
211
+ else process.env.CARTHAGENT_MAX_FILES = oldEnv;
212
+ });
213
+
214
+ assert.equal(maxEvidenceFiles(), 25000);
215
+ assert.equal(maxEvidenceBytes(), 256 * 1024 * 1024);
216
+
217
+ process.env.CARTHAGENT_MAX_FILES = '3';
218
+ assert.equal(maxEvidenceFiles(), 3);
219
+
220
+ // 1 file (app.py) passes
221
+ assert.doesNotThrow(() => fingerprint(cwd, ['.']));
222
+
223
+ // Add 3 more files (total 4 > 3)
224
+ writeFileSync(join(cwd, 'a.txt'), 'a');
225
+ writeFileSync(join(cwd, 'b.txt'), 'b');
226
+ writeFileSync(join(cwd, 'c.txt'), 'c');
227
+ assert.throws(() => fingerprint(cwd, ['.']), /Evidence scope exceeds 3 files/);
228
+ });
229
+
230
+ test('fingerprint omits .pi and .carthagent directory trees', t => {
231
+ const cwd = fixture(t);
232
+ const hashBefore = fingerprint(cwd, ['.']);
233
+ mkdirSync(join(cwd, '.pi'), { recursive: true });
234
+ writeFileSync(join(cwd, '.pi', 'data.json'), '{"ignore": true}');
235
+ mkdirSync(join(cwd, '.carthagent'), { recursive: true });
236
+ writeFileSync(join(cwd, '.carthagent', 'data.json'), '{"ignore": true}');
237
+ const hashAfter = fingerprint(cwd, ['.']);
238
+ assert.equal(hashBefore, hashAfter);
239
+ });
240
+
241
+ test('verification classification: none/advisory rules keep required strict', t => {
242
+ const cwd = fixture(t);
243
+ // Default is required: non-empty acceptance + checks, at least one non-static.
244
+ assert.deepEqual(validatePlan(plan(), cwd), plan());
245
+ assert.equal(verificationMode(plan()), 'required');
246
+ assert.equal(verificationMode({ verification: 'bogus' }), 'required');
247
+ assert.throws(() => validatePlan({ ...plan(), verification: 'informational' }, cwd), /verification must be one of/);
248
+
249
+ // none = pure informational answer: checks/acceptance must be absent.
250
+ const none = { ...plan(), verification: 'none', checks: [], acceptance: [] };
251
+ assert.deepEqual(validatePlan(none, cwd), none);
252
+ assert.throws(() => validatePlan({ ...plan(), verification: 'none' }, cwd), /none/);
253
+
254
+ // required stays strict even if the model empties the arrays.
255
+ assert.throws(() => validatePlan({ ...plan(), checks: [], acceptance: [] }, cwd), /required/);
256
+
257
+ // advisory may be empty, or carry checks whose failures are non-blocking.
258
+ const advisoryEmpty = { ...plan(), verification: 'advisory', checks: [], acceptance: [] };
259
+ assert.deepEqual(validatePlan(advisoryEmpty, cwd), advisoryEmpty);
260
+ const advisoryChecks = { ...plan(), verification: 'advisory' };
261
+ assert.deepEqual(validatePlan(advisoryChecks, cwd), advisoryChecks);
262
+ assert.throws(() => validatePlan({ ...advisoryChecks, acceptance: [] }, cwd), /together/);
263
+ const staticOnly = { ...plan(), verification: 'advisory', checks: [{ ...plan().checks[0], kind: 'static' }] };
264
+ assert.deepEqual(validatePlan(staticOnly, cwd), staticOnly);
265
+ });
266
+
267
+ test('required validators force strict verification and cannot be downgraded away', () => {
268
+ const informative = { goal: 'Q', artifacts: ['.'], steps: ['Answer'], verification: 'none', acceptance: [], checks: [] };
269
+ const bound = bindRequiredChecks(informative, [{ id: 'required_oracle', kind: 'test', argv: [process.execPath, '-e', 'process.exit(0)'], timeoutSeconds: 5 }]);
270
+ assert.equal(bound.verification, 'required');
271
+ assert.equal(bound.checks.length, 1);
272
+ assert.equal(bound.acceptance.length, 1);
273
+ assert.equal(bound.acceptance[0].checks[0], 'required_oracle');
274
+ });
275
+
276
+ test('pre-verify reclassification is allowed only before evidence exists', () => {
277
+ const previous = { plan: plan(), status: 'implementing', evidence: {} };
278
+ const informational = { goal: 'Answered a question', artifacts: ['.'], steps: ['Answer'], verification: 'none', acceptance: [], checks: [] };
279
+ // Re-check before the verify loop: no evidence, so reclassification is explicit.
280
+ assert.doesNotThrow(() => validateRevision(previous, informational));
281
+ // Once a check has run, a downgrade would escape its failing evidence.
282
+ assert.throws(
283
+ () => validateRevision({ ...previous, evidence: { smoke: { passed: false } } }, informational),
284
+ /Cannot downgrade verification after checks have run/,
285
+ );
286
+ // Upgrading informative -> required still runs acceptance protection.
287
+ assert.doesNotThrow(() => validateRevision({ plan: informational, status: 'implementing', evidence: {} }, plan()));
288
+ const weakened = plan();
289
+ weakened.acceptance = [{ requirement: 'A weaker requirement', checks: ['smoke'] }];
290
+ assert.throws(() => validateRevision({ plan: plan(), status: 'implementing', evidence: {} }, weakened), /remove acceptance/);
291
+ });
292
+
293
+ test('looksInformational recognizes read-only asks but not build tasks', () => {
294
+ for (const text of ['what does index.ts do?', 'explain the retry logic', 'how does auth work', 'review this diff', 'list the pending checks']) {
295
+ assert.equal(looksInformational(text), true, text);
296
+ }
297
+ for (const text of ['build a task cli', 'fix the failing test', 'what cache layer should I add — implement it', '/compact', '!ls', '']) {
298
+ assert.equal(looksInformational(text), false, text);
299
+ }
300
+ });
301
+
302
+ test('informational plans are terminal and skip repair nudges', () => {
303
+ const args = { touched: true, nudges: 0, stopReason: 'stop', pendingMessages: false, fresh: false };
304
+ assert.equal(shouldContinue({ ...args, state: { status: 'implementing', plan: { verification: 'none' } } }), false);
305
+ assert.equal(shouldContinue({ ...args, state: { status: 'implementing', plan: { verification: 'advisory' } } }), false);
306
+ assert.equal(shouldContinue({ ...args, state: { status: 'implementing', plan: plan() } }), true);
307
+ });
308
+
309
+ test('computePhase treats persisted verifying with fresh evidence as ready for review', () => {
310
+ const p = plan();
311
+ const state = {
312
+ runId: 'run-a', revision: 1, plan: p, status: 'verifying',
313
+ evidence: { smoke: { runId: 'run-a', revision: 1, passed: true, fingerprint: 'h', checkDigest: checkDigest(p.checks[0]) } },
314
+ };
315
+ assert.deepEqual(pendingChecks(state, 'h'), []);
316
+ assert.equal(computePhase(state, 'h').phase, 'review');
317
+ assert.equal(computePhase(state, 'h', true).phase, 'verify');
318
+ });
319
+
320
+ test('computePhase reaches review for check-free informational plans', () => {
321
+ const informative = { goal: 'Q', artifacts: ['.'], steps: ['Answer'], acceptance: [], checks: [], verification: 'none' };
322
+ const state = { plan: informative, evidence: {}, status: 'implementing', revision: 1 };
323
+ assert.equal(computePhase(state, 'h').phase, 'review');
324
+ assert.equal(computePhase({ ...state, stepStatus: { step0: 'done' } }, 'h').phase, 'review');
325
+ // Required plans still wait for evidence before review.
326
+ assert.equal(computePhase({ plan: plan(), evidence: {}, status: 'implementing' }, 'h').phase, 'plan');
327
+ });
328
+
329
+ test('guidance, prompt template and README document the classification', () => {
330
+ const prompt = readFileSync(new URL('../prompts/delivery.md', import.meta.url), 'utf8');
331
+ const extension = readFileSync(new URL('../extensions/delivery.ts', import.meta.url), 'utf8');
332
+ const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8');
333
+ assert.match(prompt, /Classify the ask/);
334
+ assert.match(prompt, /verification:"none"/);
335
+ assert.match(prompt, /verification:"advisory"/);
336
+ assert.match(prompt, /re-check the classification/);
337
+ assert.match(extension, /Classify the ask before planning/);
338
+ assert.match(extension, /verification:"none"/);
339
+ assert.match(extension, /looksInformational\(event\.prompt\)/);
340
+ assert.match(readme, /Informational vs delivery asks/);
341
+ assert.match(readme, /advisory/);
342
+ });
343
+
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Test stand-in for the engine's extension API module.
3
+ * The real engine (vendor/agent) aliases '@earendil-works/pi-coding-agent' to
4
+ * its bundled exports when loading extensions; pulling the whole bundle into a
5
+ * unit test is unnecessary — only these pure helpers are imported.
6
+ */
7
+ export const CONFIG_DIR_NAME = '.carthagent';
8
+
9
+ /** Keep the tail of `content` within maxLines/maxBytes, like the engine does. */
10
+ export function truncateTail(content, { maxLines = 2000, maxBytes = 51200 } = {}) {
11
+ const lines = String(content).split('\n');
12
+ if (lines.length <= maxLines && Buffer.byteLength(content, 'utf-8') <= maxBytes) {
13
+ return { content, truncated: false };
14
+ }
15
+ const out = [];
16
+ let bytes = 0;
17
+ for (let i = lines.length - 1; i >= 0 && out.length < maxLines; i--) {
18
+ const size = Buffer.byteLength(lines[i], 'utf-8') + (out.length ? 1 : 0);
19
+ if (bytes + size > maxBytes) break;
20
+ out.unshift(lines[i]);
21
+ bytes += size;
22
+ }
23
+ return { content: out.join('\n'), truncated: true };
24
+ }
@@ -0,0 +1,37 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRequire } from 'node:module';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const requireEngine = createRequire(import.meta.url);
7
+ const { createJiti } = requireEngine('jiti');
8
+ const jiti = createJiti(import.meta.url, {
9
+ alias: { '@earendil-works/pi-coding-agent': fileURLToPath(new URL('../vendor/agent/index.js', import.meta.url)) },
10
+ });
11
+ const factory = await jiti.import(fileURLToPath(new URL('../extensions/experiential.ts', import.meta.url)), { default: true });
12
+
13
+ test('Cloud extension gives limit-specific recovery without hiding BYOK', () => {
14
+ let handler;
15
+ const registered = [];
16
+ factory({
17
+ registerProvider(id) { registered.push(id); },
18
+ on(name, callback) { if (name === 'message_end') handler = callback; },
19
+ });
20
+ assert.deepEqual(registered, ['experiential-labs']);
21
+ assert.equal(typeof handler, 'function');
22
+
23
+ const notices = [];
24
+ const ctx = { model: { provider: 'experiential-labs' }, ui: { notify: (...args) => notices.push(args) } };
25
+ const event = code => ({ message: { role: 'assistant', provider: 'experiential-labs', stopReason: 'error', errorMessage: `429: {"error":{"code":"${code}"}}` } });
26
+ handler(event('daily_limit_reached'), ctx);
27
+ handler(event('monthly_allowance_exhausted'), ctx);
28
+ handler(event('subscription_inactive'), ctx);
29
+ handler(event('unrelated_error'), ctx);
30
+
31
+ assert.equal(notices.length, 3);
32
+ assert.match(notices[0][0], /00:00 UTC/);
33
+ assert.match(notices[1][0], /billing portal/);
34
+ assert.match(notices[2][0], /subscription is inactive/);
35
+ assert.ok(notices.every(([, tone]) => tone === 'warning'));
36
+ assert.ok(notices.every(([message]) => /\/login for BYOK/.test(message)));
37
+ });