@arnilo/prism-tools 0.0.0-stage → 0.13.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 (331) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +29 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +584 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,828 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
3
+ import type { JsonObject } from "@arnilo/prism";
4
+ import { sha256Hex } from "./artifacts.js";
5
+ import type { CodingLifecycleEvent } from "./lifecycle-events.js";
6
+ import {
7
+ DEFAULT_MAX_CHECK_SUMMARY_BYTES,
8
+ DEFAULT_MAX_CODING_ARTIFACT_BYTES,
9
+ DEFAULT_MAX_CODING_ARTIFACTS,
10
+ DEFAULT_MAX_CODING_CHECKPOINT_BYTES,
11
+ DEFAULT_MAX_PLAN_BYTES,
12
+ DEFAULT_MAX_TODO_TEXT_BYTES,
13
+ DEFAULT_MAX_TODOS,
14
+ HARD_MAX_CHECK_SUMMARY_BYTES,
15
+ HARD_MAX_CODING_ARTIFACT_BYTES,
16
+ HARD_MAX_CODING_ARTIFACTS,
17
+ HARD_MAX_CODING_CHECKPOINT_BYTES,
18
+ HARD_MAX_PLAN_BYTES,
19
+ HARD_MAX_TODO_TEXT_BYTES,
20
+ HARD_MAX_TODOS,
21
+ validateCodingLimit,
22
+ } from "./limits.js";
23
+
24
+ export const CODING_CHECKPOINT_SCHEMA_VERSION = 1 as const;
25
+ /** Workflow shared-state key that holds coding checkpoint metadata. */
26
+ export const CODING_STATE_KEY = "coding";
27
+
28
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
29
+ const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
30
+ const BRANCH = /^[^\s]{1,255}$/;
31
+ // Linear todo-line scanner replaces `/^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$/` (CodeQL js/polynomial-redos, alert 4)
32
+ function parseTodoLine(line: string): { done: boolean; raw: string } | undefined {
33
+ let i = 0;
34
+ while (i < line.length && /\s/.test(line[i])) i += 1;
35
+ if (line[i] !== "-" && line[i] !== "*") return undefined;
36
+ i += 1;
37
+ while (i < line.length && /\s/.test(line[i])) i += 1;
38
+ if (line[i] !== "[") return undefined;
39
+ i += 1;
40
+ const doneChar = line[i];
41
+ if (doneChar !== "x" && doneChar !== "X" && doneChar !== " ") return undefined;
42
+ i += 1;
43
+ if (line[i] !== "]") return undefined;
44
+ i += 1;
45
+ while (i < line.length && /\s/.test(line[i])) i += 1;
46
+ let end = line.length;
47
+ while (end > i && /\s/.test(line[end - 1])) end -= 1;
48
+ if (end <= i) return undefined;
49
+ return { done: doneChar.toLowerCase() === "x", raw: line.slice(i, end) };
50
+ }
51
+
52
+ const FORBIDDEN_METADATA_KEYS = new Set([
53
+ "credentials",
54
+ "credential",
55
+ "secret",
56
+ "secrets",
57
+ "token",
58
+ "tokens",
59
+ "password",
60
+ "cookie",
61
+ "cookies",
62
+ "storageState",
63
+ "storage_state",
64
+ "authorization",
65
+ "env",
66
+ "processEnv",
67
+ "commandOutput",
68
+ "rawOutput",
69
+ "stdout",
70
+ "stderr",
71
+ ]);
72
+
73
+ export type CodingArtifactKind = "plan" | "workspace" | "patch" | "bundle" | "diff" | "other";
74
+
75
+ export type CodingTaskStatus =
76
+ | "planned"
77
+ | "editing"
78
+ | "checking"
79
+ | "awaiting_approval"
80
+ | "ready_for_handoff"
81
+ | "completed"
82
+ | "failed"
83
+ | "cancelled";
84
+
85
+ export interface CodingArtifactRef {
86
+ readonly kind: CodingArtifactKind;
87
+ readonly uri: string;
88
+ readonly sha256: string;
89
+ readonly bytes: number;
90
+ }
91
+
92
+ export interface CodingTodoItem {
93
+ readonly id: string;
94
+ readonly text: string;
95
+ readonly done: boolean;
96
+ }
97
+
98
+ export interface CodingCheckSummary {
99
+ readonly name: string;
100
+ readonly exitCode: number;
101
+ readonly summary: string;
102
+ }
103
+
104
+ export interface CodingFingerprints {
105
+ /** Workflow definition revision string. */
106
+ readonly workflowRevision: string;
107
+ /** Optional definition hash captured after `defineWorkflow`. */
108
+ readonly definitionHash?: string;
109
+ /** Host-pinned sandbox/image digest when a disposable sandbox is used. */
110
+ readonly imageDigest?: string;
111
+ /** Host-computed hash of the selected tool surface. */
112
+ readonly toolFingerprint: string;
113
+ /** Host-computed hash of the selected execution/approval policy. */
114
+ readonly policyFingerprint: string;
115
+ }
116
+
117
+ export interface CodingHandoffSummary {
118
+ readonly base: string;
119
+ readonly head: string;
120
+ readonly changedPathCount: number;
121
+ readonly checkCount: number;
122
+ readonly artifact?: CodingArtifactRef;
123
+ }
124
+
125
+ export interface CodingCheckpointMetadata {
126
+ readonly schemaVersion: typeof CODING_CHECKPOINT_SCHEMA_VERSION;
127
+ readonly taskId: string;
128
+ readonly workspaceRoot: string;
129
+ readonly baseBranch: string;
130
+ readonly branch: string;
131
+ readonly worktreePath?: string;
132
+ /** Workspace-relative plan path (for example `plans/task-1.md`). */
133
+ readonly planPath: string;
134
+ readonly plan: CodingArtifactRef;
135
+ readonly workspaceExport?: CodingArtifactRef;
136
+ readonly artifacts: readonly CodingArtifactRef[];
137
+ readonly checks: readonly CodingCheckSummary[];
138
+ readonly handoff?: CodingHandoffSummary;
139
+ readonly status: CodingTaskStatus;
140
+ readonly fingerprints: CodingFingerprints;
141
+ readonly todos: readonly CodingTodoItem[];
142
+ readonly updatedAt: string;
143
+ }
144
+
145
+ export interface CodingCheckpointLimitOptions {
146
+ readonly maxPlanBytes?: number;
147
+ readonly maxTodos?: number;
148
+ readonly maxTodoTextBytes?: number;
149
+ readonly maxArtifacts?: number;
150
+ readonly maxArtifactBytes?: number;
151
+ readonly maxCheckSummaryBytes?: number;
152
+ readonly maxCheckpointBytes?: number;
153
+ }
154
+
155
+ export interface ResolvedCodingCheckpointLimits {
156
+ readonly maxPlanBytes: number;
157
+ readonly maxTodos: number;
158
+ readonly maxTodoTextBytes: number;
159
+ readonly maxArtifacts: number;
160
+ readonly maxArtifactBytes: number;
161
+ readonly maxCheckSummaryBytes: number;
162
+ readonly maxCheckpointBytes: number;
163
+ }
164
+
165
+ export class CodingCheckpointError extends Error {
166
+ readonly code = "ERR_PRISM_CODING_CHECKPOINT";
167
+ constructor(message: string) {
168
+ super(message);
169
+ this.name = "CodingCheckpointError";
170
+ }
171
+ }
172
+
173
+ export function resolveCodingCheckpointLimits(options?: CodingCheckpointLimitOptions): ResolvedCodingCheckpointLimits {
174
+ return {
175
+ maxPlanBytes: validateCodingLimit("maxPlanBytes", options?.maxPlanBytes ?? DEFAULT_MAX_PLAN_BYTES, HARD_MAX_PLAN_BYTES),
176
+ maxTodos: validateCodingLimit("maxTodos", options?.maxTodos ?? DEFAULT_MAX_TODOS, HARD_MAX_TODOS),
177
+ maxTodoTextBytes: validateCodingLimit(
178
+ "maxTodoTextBytes",
179
+ options?.maxTodoTextBytes ?? DEFAULT_MAX_TODO_TEXT_BYTES,
180
+ HARD_MAX_TODO_TEXT_BYTES,
181
+ ),
182
+ maxArtifacts: validateCodingLimit("maxArtifacts", options?.maxArtifacts ?? DEFAULT_MAX_CODING_ARTIFACTS, HARD_MAX_CODING_ARTIFACTS),
183
+ maxArtifactBytes: validateCodingLimit(
184
+ "maxArtifactBytes",
185
+ options?.maxArtifactBytes ?? DEFAULT_MAX_CODING_ARTIFACT_BYTES,
186
+ HARD_MAX_CODING_ARTIFACT_BYTES,
187
+ ),
188
+ maxCheckSummaryBytes: validateCodingLimit(
189
+ "maxCheckSummaryBytes",
190
+ options?.maxCheckSummaryBytes ?? DEFAULT_MAX_CHECK_SUMMARY_BYTES,
191
+ HARD_MAX_CHECK_SUMMARY_BYTES,
192
+ ),
193
+ maxCheckpointBytes: validateCodingLimit(
194
+ "maxCheckpointBytes",
195
+ options?.maxCheckpointBytes ?? DEFAULT_MAX_CODING_CHECKPOINT_BYTES,
196
+ HARD_MAX_CODING_CHECKPOINT_BYTES,
197
+ ),
198
+ };
199
+ }
200
+
201
+ /** Deterministic SHA-256 fingerprint over a JSON-stable encoding. */
202
+ export function fingerprintJson(value: unknown): string {
203
+ return sha256Hex(Buffer.from(stableStringify(value), "utf8"));
204
+ }
205
+
206
+ export function createCodingArtifactRef(input: {
207
+ readonly kind: CodingArtifactKind;
208
+ readonly uri: string;
209
+ readonly bytes: Buffer;
210
+ readonly maxBytes?: number;
211
+ }): CodingArtifactRef {
212
+ const maxBytes = input.maxBytes ?? DEFAULT_MAX_CODING_ARTIFACT_BYTES;
213
+ if (input.bytes.length > maxBytes) {
214
+ throw new CodingCheckpointError(`Artifact exceeds ${maxBytes} byte limit`);
215
+ }
216
+ if (!isNonEmptyString(input.uri) || input.uri.length > 2_048) {
217
+ throw new CodingCheckpointError("Artifact URI must be a non-empty string at most 2048 characters");
218
+ }
219
+ return {
220
+ kind: input.kind,
221
+ uri: input.uri,
222
+ sha256: sha256Hex(input.bytes),
223
+ bytes: input.bytes.length,
224
+ };
225
+ }
226
+
227
+ export function verifyCodingArtifactBytes(ref: CodingArtifactRef, bytes: Buffer, limits?: CodingCheckpointLimitOptions): void {
228
+ const resolved = resolveCodingCheckpointLimits(limits);
229
+ if (ref.bytes > resolved.maxArtifactBytes || bytes.length > resolved.maxArtifactBytes) {
230
+ throw new CodingCheckpointError(`Artifact exceeds ${resolved.maxArtifactBytes} byte limit`);
231
+ }
232
+ if (bytes.length !== ref.bytes) {
233
+ throw new CodingCheckpointError(`Artifact byte count mismatch: expected ${ref.bytes}, got ${bytes.length}`);
234
+ }
235
+ const digest = sha256Hex(bytes);
236
+ if (digest !== ref.sha256) {
237
+ throw new CodingCheckpointError("Artifact SHA-256 mismatch");
238
+ }
239
+ }
240
+
241
+ export function createCodingPlanMarkdown(input: {
242
+ readonly title: string;
243
+ readonly taskId: string;
244
+ readonly status?: CodingTaskStatus;
245
+ readonly todos: readonly { readonly id?: string; readonly text: string; readonly done?: boolean }[];
246
+ readonly notes?: string;
247
+ readonly limits?: CodingCheckpointLimitOptions;
248
+ }): string {
249
+ const limits = resolveCodingCheckpointLimits(input.limits);
250
+ if (input.todos.length > limits.maxTodos) {
251
+ throw new CodingCheckpointError(`Plan exceeds ${limits.maxTodos} todo limit`);
252
+ }
253
+ const lines = [
254
+ `# ${input.title.trim() || "Coding task"}`,
255
+ "",
256
+ `- Task ID: \`${input.taskId}\``,
257
+ `- Status: \`${input.status ?? "planned"}\``,
258
+ "",
259
+ "## Todos",
260
+ "",
261
+ ];
262
+ for (const todo of input.todos) {
263
+ const text = todo.text.trim();
264
+ assertTodoText(text, limits.maxTodoTextBytes);
265
+ const mark = todo.done ? "x" : " ";
266
+ const idPrefix = todo.id ? `[${todo.id}] ` : "";
267
+ lines.push(`- [${mark}] ${idPrefix}${text}`);
268
+ }
269
+ if (input.notes?.trim()) {
270
+ lines.push("", "## Notes", "", input.notes.trim(), "");
271
+ } else {
272
+ lines.push("");
273
+ }
274
+ const markdown = lines.join("\n");
275
+ assertByteLimit("plan", markdown, limits.maxPlanBytes);
276
+ return markdown;
277
+ }
278
+
279
+ export function parseCodingPlanTodos(markdown: string, limits?: CodingCheckpointLimitOptions): CodingTodoItem[] {
280
+ const resolved = resolveCodingCheckpointLimits(limits);
281
+ assertByteLimit("plan", markdown, resolved.maxPlanBytes);
282
+ const todos: CodingTodoItem[] = [];
283
+ for (const line of markdown.split(/\r?\n/)) {
284
+ const match = parseTodoLine(line);
285
+ if (!match) continue;
286
+ const done = match.done;
287
+ const raw = match.raw;
288
+ assertTodoText(raw, resolved.maxTodoTextBytes);
289
+ // Linear id-prefix parse replaces `/^\[([A-Za-z0-9._-]{1,64})\]\s+(.+)$/` (CodeQL js/polynomial-redos, alert 5)
290
+ let id: string | undefined;
291
+ let text = raw;
292
+ if (raw.startsWith("[")) {
293
+ const close = raw.indexOf("]", 1);
294
+ if (close !== -1) {
295
+ const candidate = raw.slice(1, close);
296
+ const after = raw.slice(close + 1);
297
+ if (
298
+ candidate.length >= 1 &&
299
+ candidate.length <= 64 &&
300
+ /^[A-Za-z0-9._-]+$/u.test(candidate) &&
301
+ /^\s/u.test(after) &&
302
+ after.trim().length > 0
303
+ ) {
304
+ id = candidate;
305
+ text = after.trim();
306
+ }
307
+ }
308
+ }
309
+ assertTodoText(text, resolved.maxTodoTextBytes);
310
+ todos.push({ id: id ?? `todo-${todos.length + 1}`, text, done });
311
+ if (todos.length > resolved.maxTodos) {
312
+ throw new CodingCheckpointError(`Plan exceeds ${resolved.maxTodos} todo limit`);
313
+ }
314
+ }
315
+ return todos;
316
+ }
317
+
318
+ export async function writeCodingPlanFile(input: {
319
+ readonly workspaceRoot: string;
320
+ readonly planPath: string;
321
+ readonly markdown: string;
322
+ readonly limits?: CodingCheckpointLimitOptions;
323
+ /** F5: advisory plan event (telemetry — never throws into the write path). */
324
+ readonly onEvent?: (event: CodingLifecycleEvent) => void;
325
+ }): Promise<CodingArtifactRef> {
326
+ const limits = resolveCodingCheckpointLimits(input.limits);
327
+ assertByteLimit("plan", input.markdown, limits.maxPlanBytes);
328
+ const absolute = resolveUnderWorkspace(input.workspaceRoot, input.planPath);
329
+ await mkdir(dirname(absolute), { recursive: true });
330
+ const bytes = Buffer.from(input.markdown, "utf8");
331
+ await writeFile(absolute, bytes, { mode: 0o600 });
332
+ if (input.onEvent) {
333
+ try {
334
+ input.onEvent({
335
+ type: "plan_changed",
336
+ planPath: input.planPath,
337
+ todos: parseCodingPlanTodos(input.markdown, limits),
338
+ });
339
+ } catch {
340
+ // Telemetry must never fail a successful plan write.
341
+ }
342
+ }
343
+ return createCodingArtifactRef({
344
+ kind: "plan",
345
+ uri: `file://${absolute}`,
346
+ bytes,
347
+ maxBytes: limits.maxPlanBytes,
348
+ });
349
+ }
350
+
351
+ export async function readCodingPlanFile(input: {
352
+ readonly workspaceRoot: string;
353
+ readonly planPath: string;
354
+ readonly expected?: CodingArtifactRef;
355
+ readonly limits?: CodingCheckpointLimitOptions;
356
+ }): Promise<{ markdown: string; artifact: CodingArtifactRef; todos: CodingTodoItem[] }> {
357
+ const limits = resolveCodingCheckpointLimits(input.limits);
358
+ const absolute = resolveUnderWorkspace(input.workspaceRoot, input.planPath);
359
+ const bytes = await readFile(absolute);
360
+ if (bytes.length > limits.maxPlanBytes) {
361
+ throw new CodingCheckpointError(`Plan exceeds ${limits.maxPlanBytes} byte limit`);
362
+ }
363
+ const artifact = createCodingArtifactRef({
364
+ kind: "plan",
365
+ uri: `file://${absolute}`,
366
+ bytes,
367
+ maxBytes: limits.maxPlanBytes,
368
+ });
369
+ if (input.expected) {
370
+ verifyCodingArtifactBytes(input.expected, bytes, limits);
371
+ }
372
+ const markdown = bytes.toString("utf8");
373
+ return {
374
+ markdown,
375
+ artifact,
376
+ todos: parseCodingPlanTodos(markdown, limits),
377
+ };
378
+ }
379
+
380
+ export function buildCodingCheckpointMetadata(input: {
381
+ readonly taskId: string;
382
+ readonly workspaceRoot: string;
383
+ readonly baseBranch: string;
384
+ readonly branch: string;
385
+ readonly planPath: string;
386
+ readonly plan: CodingArtifactRef;
387
+ readonly fingerprints: CodingFingerprints;
388
+ readonly status?: CodingTaskStatus;
389
+ readonly todos?: readonly CodingTodoItem[];
390
+ readonly worktreePath?: string;
391
+ readonly workspaceExport?: CodingArtifactRef;
392
+ readonly artifacts?: readonly CodingArtifactRef[];
393
+ readonly checks?: readonly CodingCheckSummary[];
394
+ readonly handoff?: CodingHandoffSummary;
395
+ readonly updatedAt?: string;
396
+ readonly limits?: CodingCheckpointLimitOptions;
397
+ }): CodingCheckpointMetadata {
398
+ // Worktree identity (paths, heads, session bindings) lives in the `workspace` service
399
+ // (plan 165 Task 6, workspace/service.ts), not in this v1 metadata; Phase 16's `agentNode`
400
+ // revalidates that worktree before reuse.
401
+ const metadata: CodingCheckpointMetadata = {
402
+ schemaVersion: CODING_CHECKPOINT_SCHEMA_VERSION,
403
+ taskId: input.taskId,
404
+ workspaceRoot: input.workspaceRoot,
405
+ baseBranch: input.baseBranch,
406
+ branch: input.branch,
407
+ worktreePath: input.worktreePath,
408
+ planPath: input.planPath,
409
+ plan: input.plan,
410
+ workspaceExport: input.workspaceExport,
411
+ artifacts: input.artifacts ?? [],
412
+ checks: input.checks ?? [],
413
+ handoff: input.handoff,
414
+ status: input.status ?? "planned",
415
+ fingerprints: input.fingerprints,
416
+ todos: input.todos ?? [],
417
+ updatedAt: input.updatedAt ?? new Date().toISOString(),
418
+ };
419
+ return validateCodingCheckpointMetadata(metadata, input.limits);
420
+ }
421
+
422
+ export function validateCodingCheckpointMetadata(value: unknown, limits?: CodingCheckpointLimitOptions): CodingCheckpointMetadata {
423
+ const resolved = resolveCodingCheckpointLimits(limits);
424
+ if (!isPlainObject(value)) {
425
+ throw new CodingCheckpointError("Coding checkpoint metadata must be an object");
426
+ }
427
+ assertNoForbiddenKeys(value);
428
+
429
+ if (value.schemaVersion !== CODING_CHECKPOINT_SCHEMA_VERSION) {
430
+ throw new CodingCheckpointError(`Unsupported coding checkpoint schemaVersion: ${String(value.schemaVersion)}`);
431
+ }
432
+ const taskId = requireString(value.taskId, "taskId");
433
+ if (!TASK_ID.test(taskId)) {
434
+ throw new CodingCheckpointError("taskId has invalid format");
435
+ }
436
+ const workspaceRoot = requireAbsolutePath(value.workspaceRoot, "workspaceRoot");
437
+ const baseBranch = requireBranch(value.baseBranch, "baseBranch");
438
+ const branch = requireBranch(value.branch, "branch");
439
+ const planPath = requireRelativePath(value.planPath, "planPath");
440
+ const plan = validateArtifactRef(value.plan, resolved, { requireKind: "plan" });
441
+ const worktreePath = value.worktreePath === undefined ? undefined : requireAbsolutePath(value.worktreePath, "worktreePath");
442
+ const workspaceExport = value.workspaceExport === undefined ? undefined : validateArtifactRef(value.workspaceExport, resolved);
443
+ const artifacts = requireArray(value.artifacts, "artifacts").map((item, index) =>
444
+ validateArtifactRef(item, resolved, { label: `artifacts[${index}]` }),
445
+ );
446
+ if (artifacts.length > resolved.maxArtifacts) {
447
+ throw new CodingCheckpointError(`Coding checkpoint exceeds ${resolved.maxArtifacts} artifact references`);
448
+ }
449
+ const checks = requireArray(value.checks, "checks").map((item, index) => validateCheckSummary(item, resolved, `checks[${index}]`));
450
+ if (checks.length > HARD_MAX_CODING_ARTIFACTS) {
451
+ throw new CodingCheckpointError("Too many check summaries");
452
+ }
453
+ const todos = requireArray(value.todos, "todos").map((item, index) => validateTodo(item, resolved, `todos[${index}]`));
454
+ if (todos.length > resolved.maxTodos) {
455
+ throw new CodingCheckpointError(`Coding checkpoint exceeds ${resolved.maxTodos} todos`);
456
+ }
457
+ const fingerprints = validateFingerprints(value.fingerprints);
458
+ const status = requireStatus(value.status);
459
+ const updatedAt = requireString(value.updatedAt, "updatedAt");
460
+ if (Number.isNaN(Date.parse(updatedAt))) {
461
+ throw new CodingCheckpointError("updatedAt must be an ISO-8601 timestamp");
462
+ }
463
+ const handoff = value.handoff === undefined ? undefined : validateHandoffSummary(value.handoff, resolved);
464
+
465
+ const metadata: CodingCheckpointMetadata = {
466
+ schemaVersion: CODING_CHECKPOINT_SCHEMA_VERSION,
467
+ taskId,
468
+ workspaceRoot,
469
+ baseBranch,
470
+ branch,
471
+ worktreePath,
472
+ planPath,
473
+ plan,
474
+ workspaceExport,
475
+ artifacts,
476
+ checks,
477
+ handoff,
478
+ status,
479
+ fingerprints,
480
+ todos,
481
+ updatedAt,
482
+ };
483
+
484
+ const encoded = Buffer.byteLength(JSON.stringify(metadata), "utf8");
485
+ if (encoded > resolved.maxCheckpointBytes) {
486
+ throw new CodingCheckpointError(`Coding checkpoint metadata exceeds ${resolved.maxCheckpointBytes} byte limit`);
487
+ }
488
+ return metadata;
489
+ }
490
+
491
+ /**
492
+ * Fail closed before resume/import when ownership-equivalent fingerprints diverge
493
+ * or artifact references cannot be verified.
494
+ */
495
+ export function assertCodingResumeAllowed(input: {
496
+ readonly metadata: CodingCheckpointMetadata;
497
+ readonly expected: CodingFingerprints;
498
+ readonly expectedWorkspaceRoot?: string;
499
+ readonly expectedBaseBranch?: string;
500
+ readonly planBytes?: Buffer;
501
+ readonly workspaceExportBytes?: Buffer;
502
+ readonly limits?: CodingCheckpointLimitOptions;
503
+ }): CodingCheckpointMetadata {
504
+ const metadata = validateCodingCheckpointMetadata(input.metadata, input.limits);
505
+ assertFingerprintsMatch(metadata.fingerprints, input.expected);
506
+ if (input.expectedWorkspaceRoot !== undefined && resolve(input.expectedWorkspaceRoot) !== resolve(metadata.workspaceRoot)) {
507
+ throw new CodingCheckpointError("Workspace root mismatch on coding resume");
508
+ }
509
+ if (input.expectedBaseBranch !== undefined && input.expectedBaseBranch !== metadata.baseBranch) {
510
+ throw new CodingCheckpointError("Base branch mismatch on coding resume");
511
+ }
512
+ if (input.planBytes) {
513
+ verifyCodingArtifactBytes(metadata.plan, input.planBytes, input.limits);
514
+ }
515
+ if (input.workspaceExportBytes) {
516
+ if (!metadata.workspaceExport) {
517
+ throw new CodingCheckpointError("Workspace export bytes provided without metadata reference");
518
+ }
519
+ verifyCodingArtifactBytes(metadata.workspaceExport, input.workspaceExportBytes, input.limits);
520
+ }
521
+ return metadata;
522
+ }
523
+
524
+ /** Extract and validate `state.coding` when present. */
525
+ export function readCodingCheckpointFromState(
526
+ state: Readonly<Record<string, unknown>>,
527
+ limits?: CodingCheckpointLimitOptions,
528
+ ): CodingCheckpointMetadata | undefined {
529
+ if (!(CODING_STATE_KEY in state)) return undefined;
530
+ return validateCodingCheckpointMetadata(state[CODING_STATE_KEY], limits);
531
+ }
532
+
533
+ export function codingCheckpointStatePatch(metadata: CodingCheckpointMetadata): JsonObject {
534
+ return { [CODING_STATE_KEY]: validateCodingCheckpointMetadata(metadata) } as unknown as JsonObject;
535
+ }
536
+
537
+ function assertFingerprintsMatch(actual: CodingFingerprints, expected: CodingFingerprints): void {
538
+ if (actual.workflowRevision !== expected.workflowRevision) {
539
+ throw new CodingCheckpointError("Workflow revision fingerprint mismatch on coding resume");
540
+ }
541
+ if (actual.toolFingerprint !== expected.toolFingerprint) {
542
+ throw new CodingCheckpointError("Tool fingerprint mismatch on coding resume");
543
+ }
544
+ if (actual.policyFingerprint !== expected.policyFingerprint) {
545
+ throw new CodingCheckpointError("Policy fingerprint mismatch on coding resume");
546
+ }
547
+ if (expected.definitionHash !== undefined) {
548
+ if (actual.definitionHash !== expected.definitionHash) {
549
+ throw new CodingCheckpointError("Definition hash mismatch on coding resume");
550
+ }
551
+ }
552
+ if (expected.imageDigest !== undefined) {
553
+ if (actual.imageDigest !== expected.imageDigest) {
554
+ throw new CodingCheckpointError("Image digest mismatch on coding resume");
555
+ }
556
+ }
557
+ }
558
+
559
+ function validateFingerprints(value: unknown): CodingFingerprints {
560
+ if (!isPlainObject(value)) {
561
+ throw new CodingCheckpointError("fingerprints must be an object");
562
+ }
563
+ assertNoForbiddenKeys(value);
564
+ const workflowRevision = requireString(value.workflowRevision, "fingerprints.workflowRevision");
565
+ const toolFingerprint = requireFingerprint(value.toolFingerprint, "fingerprints.toolFingerprint");
566
+ const policyFingerprint = requireFingerprint(value.policyFingerprint, "fingerprints.policyFingerprint");
567
+ const definitionHash =
568
+ value.definitionHash === undefined ? undefined : requireFingerprint(value.definitionHash, "fingerprints.definitionHash");
569
+ const imageDigest = value.imageDigest === undefined ? undefined : requireString(value.imageDigest, "fingerprints.imageDigest");
570
+ if (imageDigest !== undefined && !/sha256:[a-f0-9]{64}/.test(imageDigest) && !SHA256_HEX.test(imageDigest)) {
571
+ // Allow either raw hex or docker digest form.
572
+ if (!imageDigest.includes("@sha256:") && !imageDigest.startsWith("sha256:")) {
573
+ throw new CodingCheckpointError("fingerprints.imageDigest must be a digest string");
574
+ }
575
+ }
576
+ return {
577
+ workflowRevision,
578
+ definitionHash,
579
+ imageDigest,
580
+ toolFingerprint,
581
+ policyFingerprint,
582
+ };
583
+ }
584
+
585
+ function validateArtifactRef(
586
+ value: unknown,
587
+ limits: ResolvedCodingCheckpointLimits,
588
+ options?: { readonly requireKind?: CodingArtifactKind; readonly label?: string },
589
+ ): CodingArtifactRef {
590
+ const label = options?.label ?? "artifact";
591
+ if (!isPlainObject(value)) {
592
+ throw new CodingCheckpointError(`${label} must be an object`);
593
+ }
594
+ assertNoForbiddenKeys(value);
595
+ const kind = requireString(value.kind, `${label}.kind`) as CodingArtifactKind;
596
+ if (!["plan", "workspace", "patch", "bundle", "diff", "other"].includes(kind)) {
597
+ throw new CodingCheckpointError(`${label}.kind is unsupported`);
598
+ }
599
+ if (options?.requireKind && kind !== options.requireKind) {
600
+ throw new CodingCheckpointError(`${label}.kind must be ${options.requireKind}`);
601
+ }
602
+ const uri = requireString(value.uri, `${label}.uri`);
603
+ if (uri.length > 2_048) {
604
+ throw new CodingCheckpointError(`${label}.uri exceeds 2048 characters`);
605
+ }
606
+ const sha256 = requireString(value.sha256, `${label}.sha256`).toLowerCase();
607
+ if (!SHA256_HEX.test(sha256)) {
608
+ throw new CodingCheckpointError(`${label}.sha256 must be a 64-char hex digest`);
609
+ }
610
+ const bytes = requireSafeInt(value.bytes, `${label}.bytes`);
611
+ if (bytes < 0 || bytes > limits.maxArtifactBytes) {
612
+ throw new CodingCheckpointError(`${label}.bytes out of range`);
613
+ }
614
+ return { kind, uri, sha256, bytes };
615
+ }
616
+
617
+ function validateCheckSummary(value: unknown, limits: ResolvedCodingCheckpointLimits, label: string): CodingCheckSummary {
618
+ if (!isPlainObject(value)) {
619
+ throw new CodingCheckpointError(`${label} must be an object`);
620
+ }
621
+ assertNoForbiddenKeys(value);
622
+ const name = requireString(value.name, `${label}.name`);
623
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name)) {
624
+ throw new CodingCheckpointError(`${label}.name has invalid format`);
625
+ }
626
+ const exitCode = requireSafeInt(value.exitCode, `${label}.exitCode`);
627
+ if (exitCode < 0 || exitCode > 255) {
628
+ throw new CodingCheckpointError(`${label}.exitCode out of range`);
629
+ }
630
+ const summary = requireString(value.summary, `${label}.summary`);
631
+ assertByteLimit(`${label}.summary`, summary, limits.maxCheckSummaryBytes);
632
+ if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(summary)) {
633
+ throw new CodingCheckpointError(`${label}.summary contains control characters`);
634
+ }
635
+ return { name, exitCode, summary };
636
+ }
637
+
638
+ function validateTodo(value: unknown, limits: ResolvedCodingCheckpointLimits, label: string): CodingTodoItem {
639
+ if (!isPlainObject(value)) {
640
+ throw new CodingCheckpointError(`${label} must be an object`);
641
+ }
642
+ assertNoForbiddenKeys(value);
643
+ const id = requireString(value.id, `${label}.id`);
644
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(id)) {
645
+ throw new CodingCheckpointError(`${label}.id has invalid format`);
646
+ }
647
+ const text = requireString(value.text, `${label}.text`);
648
+ assertTodoText(text, limits.maxTodoTextBytes);
649
+ if (typeof value.done !== "boolean") {
650
+ throw new CodingCheckpointError(`${label}.done must be a boolean`);
651
+ }
652
+ return { id, text, done: value.done };
653
+ }
654
+
655
+ function validateHandoffSummary(value: unknown, limits: ResolvedCodingCheckpointLimits): CodingHandoffSummary {
656
+ if (!isPlainObject(value)) {
657
+ throw new CodingCheckpointError("handoff must be an object");
658
+ }
659
+ assertNoForbiddenKeys(value);
660
+ const base = requireString(value.base, "handoff.base");
661
+ const head = requireString(value.head, "handoff.head");
662
+ const changedPathCount = requireSafeInt(value.changedPathCount, "handoff.changedPathCount");
663
+ const checkCount = requireSafeInt(value.checkCount, "handoff.checkCount");
664
+ if (changedPathCount < 0 || checkCount < 0) {
665
+ throw new CodingCheckpointError("handoff counts must be non-negative");
666
+ }
667
+ const artifact = value.artifact === undefined ? undefined : validateArtifactRef(value.artifact, limits, { label: "handoff.artifact" });
668
+ return { base, head, changedPathCount, checkCount, artifact };
669
+ }
670
+
671
+ function requireStatus(value: unknown): CodingTaskStatus {
672
+ const status = requireString(value, "status");
673
+ const allowed: CodingTaskStatus[] = [
674
+ "planned",
675
+ "editing",
676
+ "checking",
677
+ "awaiting_approval",
678
+ "ready_for_handoff",
679
+ "completed",
680
+ "failed",
681
+ "cancelled",
682
+ ];
683
+ if (!allowed.includes(status as CodingTaskStatus)) {
684
+ throw new CodingCheckpointError(`Unsupported coding task status: ${status}`);
685
+ }
686
+ return status as CodingTaskStatus;
687
+ }
688
+
689
+ function resolveUnderWorkspace(workspaceRoot: string, relativePath: string): string {
690
+ const root = requireAbsolutePath(workspaceRoot, "workspaceRoot");
691
+ const rel = requireRelativePath(relativePath, "planPath");
692
+ const candidate = resolve(root, rel);
693
+ const relToRoot = relative(root, candidate);
694
+ if (relToRoot.startsWith("..") || isAbsolute(relToRoot)) {
695
+ throw new CodingCheckpointError("Plan path escapes workspace root");
696
+ }
697
+ return candidate;
698
+ }
699
+
700
+ function requireAbsolutePath(value: unknown, label: string): string {
701
+ const path = requireString(value, label);
702
+ if (!isAbsolute(path)) {
703
+ throw new CodingCheckpointError(`${label} must be an absolute path`);
704
+ }
705
+ const normalized = normalize(path);
706
+ if (normalized.includes(`..${sep}`) || normalized.endsWith(`${sep}..`)) {
707
+ throw new CodingCheckpointError(`${label} must not contain parent segments`);
708
+ }
709
+ return normalized;
710
+ }
711
+
712
+ function requireRelativePath(value: unknown, label: string): string {
713
+ const path = requireString(value, label);
714
+ if (isAbsolute(path) || path.split(/[\\/]/).includes("..")) {
715
+ throw new CodingCheckpointError(`${label} must be a relative path without parent segments`);
716
+ }
717
+ if (!path || path === ".") {
718
+ throw new CodingCheckpointError(`${label} must be a non-empty relative path`);
719
+ }
720
+ return path.replace(/\\/g, "/");
721
+ }
722
+
723
+ function requireBranch(value: unknown, label: string): string {
724
+ const branch = requireString(value, label);
725
+ if (!BRANCH.test(branch) || branch.includes("..")) {
726
+ throw new CodingCheckpointError(`${label} has invalid format`);
727
+ }
728
+ return branch;
729
+ }
730
+
731
+ function requireFingerprint(value: unknown, label: string): string {
732
+ const digest = requireString(value, label).toLowerCase();
733
+ if (!SHA256_HEX.test(digest)) {
734
+ throw new CodingCheckpointError(`${label} must be a 64-char hex digest`);
735
+ }
736
+ return digest;
737
+ }
738
+
739
+ function assertTodoText(text: string, maxBytes: number): void {
740
+ if (!text) {
741
+ throw new CodingCheckpointError("Todo text must be non-empty");
742
+ }
743
+ assertByteLimit("todo text", text, maxBytes);
744
+ if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(text)) {
745
+ throw new CodingCheckpointError("Todo text contains control characters");
746
+ }
747
+ }
748
+
749
+ function assertByteLimit(label: string, text: string, maxBytes: number): void {
750
+ const bytes = Buffer.byteLength(text, "utf8");
751
+ if (bytes > maxBytes) {
752
+ throw new CodingCheckpointError(`${label} exceeds ${maxBytes} byte limit`);
753
+ }
754
+ }
755
+
756
+ function assertNoForbiddenKeys(value: Record<string, unknown>, path = ""): void {
757
+ for (const [key, child] of Object.entries(value)) {
758
+ const lower = key.toLowerCase();
759
+ if (FORBIDDEN_METADATA_KEYS.has(key) || FORBIDDEN_METADATA_KEYS.has(lower)) {
760
+ throw new CodingCheckpointError(`Forbidden coding checkpoint field: ${path}${key}`);
761
+ }
762
+ if (isPlainObject(child)) {
763
+ assertNoForbiddenKeys(child, `${path}${key}.`);
764
+ }
765
+ }
766
+ }
767
+
768
+ function requireString(value: unknown, label: string): string {
769
+ if (typeof value !== "string" || value.length === 0) {
770
+ throw new CodingCheckpointError(`${label} must be a non-empty string`);
771
+ }
772
+ return value;
773
+ }
774
+
775
+ function requireSafeInt(value: unknown, label: string): number {
776
+ if (!Number.isSafeInteger(value)) {
777
+ throw new CodingCheckpointError(`${label} must be a safe integer`);
778
+ }
779
+ return value as number;
780
+ }
781
+
782
+ function requireArray(value: unknown, label: string): unknown[] {
783
+ if (!Array.isArray(value)) {
784
+ throw new CodingCheckpointError(`${label} must be an array`);
785
+ }
786
+ return value;
787
+ }
788
+
789
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
790
+ return typeof value === "object" && value !== null && !Array.isArray(value);
791
+ }
792
+
793
+ function isNonEmptyString(value: unknown): value is string {
794
+ return typeof value === "string" && value.length > 0;
795
+ }
796
+
797
+ function stableStringify(value: unknown): string {
798
+ return JSON.stringify(sortValue(value));
799
+ }
800
+
801
+ function sortValue(value: unknown): unknown {
802
+ if (Array.isArray(value)) {
803
+ return value.map(sortValue);
804
+ }
805
+ if (isPlainObject(value)) {
806
+ const out: Record<string, unknown> = {};
807
+ for (const key of Object.keys(value).sort()) {
808
+ out[key] = sortValue(value[key]);
809
+ }
810
+ return out;
811
+ }
812
+ if (typeof value === "number" && !Number.isFinite(value)) {
813
+ throw new CodingCheckpointError("Fingerprint input must not contain non-finite numbers");
814
+ }
815
+ if (typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
816
+ throw new CodingCheckpointError("Fingerprint input contains unsupported values");
817
+ }
818
+ return value;
819
+ }
820
+
821
+ // Keep path join available for hosts constructing plan paths.
822
+ export function codingPlanPathForTask(taskId: string): string {
823
+ if (!TASK_ID.test(taskId)) {
824
+ throw new CodingCheckpointError("taskId has invalid format");
825
+ }
826
+ const safe = taskId.replace(/[^A-Za-z0-9._-]+/g, "_");
827
+ return join("plans", `${safe}.md`).replace(/\\/g, "/");
828
+ }