@arnilo/prism-tools 0.0.0-stage → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/CHANGELOG.md +410 -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 +28 -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 +545 -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,1074 @@
1
+ /**
2
+ * Structured Git operations over a typed runner.
3
+ *
4
+ * All invocations use file+argument arrays with `--` pathspec separation,
5
+ * `git check-ref-format` for refs, and safe config that disables hooks,
6
+ * external diff/textconv, pagers, and credential prompts by default.
7
+ */
8
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { sha256Hex } from "./artifacts.js";
12
+ import {
13
+ type BoundGitRunner,
14
+ type CreateGitRunnerOptions,
15
+ createBoundGitRunner,
16
+ GitError,
17
+ type GitExecResult,
18
+ gitRequireOk,
19
+ gitText,
20
+ } from "./git-exec.js";
21
+ import { type GitStatusResult, parsePorcelainV2 } from "./git-status.js";
22
+ import {
23
+ DEFAULT_MAX_GIT_CHANGED_FILES,
24
+ DEFAULT_MAX_GIT_DIFF_LINES,
25
+ DEFAULT_MAX_GIT_MESSAGE_BYTES,
26
+ DEFAULT_MAX_GIT_OUTPUT_BYTES,
27
+ DEFAULT_MAX_GIT_PATCH_BYTES,
28
+ DEFAULT_MAX_GIT_PATHS,
29
+ DEFAULT_MAX_GIT_REF_BYTES,
30
+ DEFAULT_MAX_GIT_WORKTREES,
31
+ DEFAULT_MAX_PR_COMMITS,
32
+ DEFAULT_MAX_PR_HANDOFF_BYTES,
33
+ HARD_MAX_GIT_CHANGED_FILES,
34
+ HARD_MAX_GIT_DIFF_LINES,
35
+ HARD_MAX_GIT_MESSAGE_BYTES,
36
+ HARD_MAX_GIT_OUTPUT_BYTES,
37
+ HARD_MAX_GIT_PATCH_BYTES,
38
+ HARD_MAX_GIT_PATHS,
39
+ HARD_MAX_GIT_REF_BYTES,
40
+ HARD_MAX_GIT_WORKTREES,
41
+ HARD_MAX_PR_COMMITS,
42
+ HARD_MAX_PR_HANDOFF_BYTES,
43
+ validateCodingLimit,
44
+ } from "./limits.js";
45
+ import { resolveToCwd } from "./path-utils.js";
46
+
47
+ export interface GitLimitOptions {
48
+ readonly maxPaths?: number;
49
+ readonly maxRefBytes?: number;
50
+ readonly maxMessageBytes?: number;
51
+ readonly maxOutputBytes?: number;
52
+ readonly maxDiffLines?: number;
53
+ readonly maxChangedFiles?: number;
54
+ readonly maxPatchBytes?: number;
55
+ readonly maxWorktrees?: number;
56
+ readonly maxPrCommits?: number;
57
+ readonly maxPrHandoffBytes?: number;
58
+ }
59
+
60
+ export interface ResolvedGitLimits {
61
+ readonly maxPaths: number;
62
+ readonly maxRefBytes: number;
63
+ readonly maxMessageBytes: number;
64
+ readonly maxOutputBytes: number;
65
+ readonly maxDiffLines: number;
66
+ readonly maxChangedFiles: number;
67
+ readonly maxPatchBytes: number;
68
+ readonly maxWorktrees: number;
69
+ readonly maxPrCommits: number;
70
+ readonly maxPrHandoffBytes: number;
71
+ }
72
+
73
+ export function resolveGitLimits(options?: GitLimitOptions): ResolvedGitLimits {
74
+ return {
75
+ maxPaths: validateCodingLimit("maxPaths", options?.maxPaths ?? DEFAULT_MAX_GIT_PATHS, HARD_MAX_GIT_PATHS),
76
+ maxRefBytes: validateCodingLimit("maxRefBytes", options?.maxRefBytes ?? DEFAULT_MAX_GIT_REF_BYTES, HARD_MAX_GIT_REF_BYTES),
77
+ maxMessageBytes: validateCodingLimit(
78
+ "maxMessageBytes",
79
+ options?.maxMessageBytes ?? DEFAULT_MAX_GIT_MESSAGE_BYTES,
80
+ HARD_MAX_GIT_MESSAGE_BYTES,
81
+ ),
82
+ maxOutputBytes: validateCodingLimit(
83
+ "maxOutputBytes",
84
+ options?.maxOutputBytes ?? DEFAULT_MAX_GIT_OUTPUT_BYTES,
85
+ HARD_MAX_GIT_OUTPUT_BYTES,
86
+ ),
87
+ maxDiffLines: validateCodingLimit("maxDiffLines", options?.maxDiffLines ?? DEFAULT_MAX_GIT_DIFF_LINES, HARD_MAX_GIT_DIFF_LINES),
88
+ maxChangedFiles: validateCodingLimit(
89
+ "maxChangedFiles",
90
+ options?.maxChangedFiles ?? DEFAULT_MAX_GIT_CHANGED_FILES,
91
+ HARD_MAX_GIT_CHANGED_FILES,
92
+ ),
93
+ maxPatchBytes: validateCodingLimit("maxPatchBytes", options?.maxPatchBytes ?? DEFAULT_MAX_GIT_PATCH_BYTES, HARD_MAX_GIT_PATCH_BYTES),
94
+ maxWorktrees: validateCodingLimit("maxWorktrees", options?.maxWorktrees ?? DEFAULT_MAX_GIT_WORKTREES, HARD_MAX_GIT_WORKTREES),
95
+ maxPrCommits: validateCodingLimit("maxPrCommits", options?.maxPrCommits ?? DEFAULT_MAX_PR_COMMITS, HARD_MAX_PR_COMMITS),
96
+ maxPrHandoffBytes: validateCodingLimit(
97
+ "maxPrHandoffBytes",
98
+ options?.maxPrHandoffBytes ?? DEFAULT_MAX_PR_HANDOFF_BYTES,
99
+ HARD_MAX_PR_HANDOFF_BYTES,
100
+ ),
101
+ };
102
+ }
103
+
104
+ export interface ArtifactReference {
105
+ readonly kind: "patch" | "bundle" | "diff" | "other";
106
+ readonly uri: string;
107
+ readonly sha256: string;
108
+ readonly bytes: number;
109
+ }
110
+
111
+ export type ArtifactWriter = (input: {
112
+ readonly kind: ArtifactReference["kind"];
113
+ readonly filename: string;
114
+ readonly bytes: Buffer;
115
+ }) => Promise<ArtifactReference>;
116
+
117
+ export interface PrHandoff {
118
+ readonly base: string;
119
+ readonly head: string;
120
+ readonly commits: readonly { sha: string; subject: string }[];
121
+ readonly changedPaths: readonly string[];
122
+ readonly diffstat: string;
123
+ readonly checks: readonly { name: string; exitCode: number; summary: string }[];
124
+ readonly artifact?: ArtifactReference;
125
+ }
126
+
127
+ /** One entry from `git worktree list --porcelain -z` (plan 026 Task 3). */
128
+ export interface GitWorktreeEntry {
129
+ path: string;
130
+ head?: string;
131
+ branch?: string;
132
+ /** Porcelain `locked` record present. */
133
+ locked?: boolean;
134
+ /** Lock reason text when the porcelain record carries one. */
135
+ lockReason?: string;
136
+ }
137
+
138
+ export interface GitFingerprint {
139
+ /** SHA-256 of the credential-free remote URL plus default branch; never the URL itself. */
140
+ readonly remoteFingerprint: string;
141
+ readonly defaultBranch?: string;
142
+ }
143
+
144
+ export interface GitOperations {
145
+ status(options?: { includeIgnored?: boolean; signal?: AbortSignal }): Promise<GitStatusResult>;
146
+ diff(options?: {
147
+ staged?: boolean;
148
+ /** Diff the worktree against this commit-ish instead of the index (`git diff <base>`). */
149
+ base?: string;
150
+ paths?: readonly string[];
151
+ signal?: AbortSignal;
152
+ }): Promise<{ text: string; truncated: boolean; lineCount: number; artifact?: ArtifactReference }>;
153
+ branch(options: {
154
+ action: "validate" | "create" | "switch" | "list" | "delete";
155
+ name?: string;
156
+ /** `delete` only: `-D` instead of the merged-only default `-d`. */
157
+ force?: boolean;
158
+ createCheckpoint?: boolean;
159
+ signal?: AbortSignal;
160
+ }): Promise<{ refs?: string[]; name?: string; checkpoint?: string }>;
161
+ worktree(options: {
162
+ action: "list" | "add" | "remove" | "lock" | "unlock";
163
+ path?: string;
164
+ branch?: string;
165
+ /** `add` only: the commit-ish the new branch starts at (default `HEAD`). */
166
+ base?: string;
167
+ force?: boolean;
168
+ reason?: string;
169
+ signal?: AbortSignal;
170
+ }): Promise<{ worktrees: readonly GitWorktreeEntry[]; path?: string }>;
171
+ /**
172
+ * Read-only merge probe (`merge-tree --write-tree --name-only`). Returns the merged tree oid
173
+ * and the conflicted paths; a clean merge reports none. Never touches a ref, the index, or a
174
+ * worktree, so a conflict leaves every tree byte-for-byte unchanged.
175
+ */
176
+ mergeTree(options: { into: string; from: string; signal?: AbortSignal }): Promise<{ tree: string; conflicts: readonly string[] }>;
177
+ /**
178
+ * Create a commit object from an existing tree (`commit-tree`). Writes one loose object and no
179
+ * ref. The repository's configured identity is used; with none configured this fails.
180
+ */
181
+ commitTree(options: { tree: string; parents: readonly string[]; message: string; signal?: AbortSignal }): Promise<string>;
182
+ /**
183
+ * Stage every change in the worktree, including untracked files (`add -A`). Mutates the index;
184
+ * callers that need it unchanged snapshot it first with {@link GitOperations.writeTree}.
185
+ */
186
+ stageAll(options?: { signal?: AbortSignal }): Promise<void>;
187
+ /** The tree object of the current index (`write-tree`). Writes one loose object and no ref. */
188
+ writeTree(options?: { signal?: AbortSignal }): Promise<string>;
189
+ /** Replace the index with `tree` (`read-tree`), leaving the worktree untouched. */
190
+ readTree(options: { tree: string; signal?: AbortSignal }): Promise<void>;
191
+ /** Compare-and-swap a ref (`update-ref`). `expected` makes the update fail when the ref moved. */
192
+ updateRef(options: { ref: string; to: string; expected?: string; signal?: AbortSignal }): Promise<void>;
193
+ /**
194
+ * Fast-forward the checked-out branch to `to` (`merge --ff-only`), updating the index and the
195
+ * worktree so they stay consistent with HEAD. Refuses when `to` is not a descendant.
196
+ */
197
+ fastForward(options: { to: string; signal?: AbortSignal }): Promise<void>;
198
+ /** Untracked, non-ignored paths (`ls-files --others --exclude-standard`), worktree-relative. */
199
+ untrackedFiles(options?: { signal?: AbortSignal }): Promise<readonly string[]>;
200
+ /**
201
+ * Repository identity for durable correlation: credential-free remote
202
+ * fingerprint plus default branch. Runs bounded read-only git queries only.
203
+ */
204
+ fingerprint(options?: { signal?: AbortSignal }): Promise<GitFingerprint>;
205
+ apply(options: {
206
+ patch: string;
207
+ action: "check" | "apply" | "reverse";
208
+ createCheckpoint?: boolean;
209
+ signal?: AbortSignal;
210
+ }): Promise<{ ok: boolean; checkpoint?: string; restored?: boolean; output: string }>;
211
+ commit(options: {
212
+ paths: readonly string[];
213
+ message: string;
214
+ createCheckpoint?: boolean;
215
+ signal?: AbortSignal;
216
+ }): Promise<{ sha: string; checkpoint?: string }>;
217
+ prHandoff(options: {
218
+ base: string;
219
+ head?: string;
220
+ checks?: readonly { name: string; exitCode: number; summary: string }[];
221
+ includeBundle?: boolean;
222
+ signal?: AbortSignal;
223
+ }): Promise<PrHandoff>;
224
+ }
225
+
226
+ function byteLength(text: string): number {
227
+ return Buffer.byteLength(text, "utf8");
228
+ }
229
+
230
+ async function validateBranchName(
231
+ runner: BoundGitRunner,
232
+ cwd: string,
233
+ name: string,
234
+ limits: ResolvedGitLimits,
235
+ signal?: AbortSignal,
236
+ ): Promise<string> {
237
+ if (typeof name !== "string" || name.length === 0) throw new GitError("branch name is required");
238
+ if (byteLength(name) > limits.maxRefBytes) throw new GitError(`branch name exceeds ${limits.maxRefBytes} byte limit`);
239
+ if (name.includes("\0") || name.includes("\n") || name.includes("\r") || name.startsWith("-")) {
240
+ throw new GitError("branch name must not start with '-' or contain NUL/newlines");
241
+ }
242
+ const result = await runner.exec({
243
+ args: ["check-ref-format", "--branch", name],
244
+ cwd,
245
+ signal,
246
+ maxOutputBytes: 64 * 1024,
247
+ });
248
+ if (result.exitCode !== 0) {
249
+ throw new GitError(`invalid branch name: ${name}`);
250
+ }
251
+ return name;
252
+ }
253
+
254
+ async function validateRef(
255
+ runner: BoundGitRunner,
256
+ cwd: string,
257
+ ref: string,
258
+ limits: ResolvedGitLimits,
259
+ signal?: AbortSignal,
260
+ ): Promise<string> {
261
+ if (typeof ref !== "string" || !ref.startsWith("refs/")) throw new GitError("ref must be a full refs/ name");
262
+ if (byteLength(ref) > limits.maxRefBytes) throw new GitError(`ref exceeds ${limits.maxRefBytes} byte limit`);
263
+ if (ref.includes("\0") || ref.includes("\n") || ref.includes("\r") || ref.includes("..")) {
264
+ throw new GitError("ref must not contain NUL/newlines or '..'");
265
+ }
266
+ const result = await runner.exec({ args: ["check-ref-format", ref], cwd, signal, maxOutputBytes: 64 * 1024 });
267
+ if (result.exitCode !== 0) throw new GitError(`invalid ref: ${ref}`);
268
+ return ref;
269
+ }
270
+
271
+ /**
272
+ * Validate a commit-ish (`HEAD`, a ref, an oid, `HEAD~1`) for use as a git argument. A leading
273
+ * `-` is refused so the value can never be read as an option, and NUL/newlines are refused.
274
+ */
275
+ function validateCommitish(value: string, limits: ResolvedGitLimits): string {
276
+ if (typeof value !== "string" || value.length === 0) throw new GitError("commit-ish is required");
277
+ if (byteLength(value) > limits.maxRefBytes) throw new GitError(`commit-ish exceeds ${limits.maxRefBytes} byte limit`);
278
+ if (value.startsWith("-") || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
279
+ throw new GitError("commit-ish must not start with '-' or contain NUL/newlines");
280
+ }
281
+ return value;
282
+ }
283
+
284
+ function validatePaths(paths: readonly string[], limits: ResolvedGitLimits): string[] {
285
+ if (!Array.isArray(paths) || paths.length === 0) {
286
+ throw new GitError("paths must be a non-empty array");
287
+ }
288
+ if (paths.length > limits.maxPaths) {
289
+ throw new GitError(`paths exceed ${limits.maxPaths} entry limit`);
290
+ }
291
+ const out: string[] = [];
292
+ for (const path of paths) {
293
+ if (typeof path !== "string" || path.length === 0) {
294
+ throw new GitError("each path must be a non-empty string");
295
+ }
296
+ if (path.includes("\0")) throw new GitError("path must not contain NUL");
297
+ // Keep leading-dash paths as data; always pass after `--`.
298
+ out.push(path);
299
+ }
300
+ return out;
301
+ }
302
+
303
+ function truncateLines(text: string, maxLines: number): { text: string; truncated: boolean; lineCount: number } {
304
+ if (text.length === 0) return { text: "", truncated: false, lineCount: 0 };
305
+ const endsWithNewline = text.endsWith("\n");
306
+ const lines = text.split("\n");
307
+ if (endsWithNewline) lines.pop();
308
+ const lineCount = lines.length;
309
+ if (lineCount <= maxLines) return { text, truncated: false, lineCount };
310
+ const kept = `${lines.slice(0, maxLines).join("\n")}\n`;
311
+ return { text: kept, truncated: true, lineCount };
312
+ }
313
+
314
+ async function withTempFile<T>(prefix: string, contents: string | Buffer, fn: (path: string) => Promise<T>): Promise<T> {
315
+ const dir = await mkdtemp(join(tmpdir(), prefix));
316
+ const filePath = join(dir, "payload");
317
+ try {
318
+ await writeFile(filePath, contents, { mode: 0o600 });
319
+ return await fn(filePath);
320
+ } finally {
321
+ await rm(dir, { recursive: true, force: true });
322
+ }
323
+ }
324
+
325
+ export interface CreateGitOperationsOptions extends CreateGitRunnerOptions, GitLimitOptions {
326
+ readonly cwd: string;
327
+ readonly artifactWriter?: ArtifactWriter;
328
+ /** Required for `commit` when the repository has no usable identity. */
329
+ readonly commitIdentity?: { readonly name: string; readonly email: string };
330
+ }
331
+
332
+ export async function createGitOperations(options: CreateGitOperationsOptions): Promise<GitOperations> {
333
+ const cwd = resolveToCwd(options.cwd, process.cwd());
334
+ const limits = resolveGitLimits(options);
335
+ const runner = await createBoundGitRunner(options);
336
+ const artifacts = options.artifactWriter;
337
+
338
+ async function status(request?: { includeIgnored?: boolean; signal?: AbortSignal }): Promise<GitStatusResult> {
339
+ const args = ["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=all"];
340
+ if (request?.includeIgnored) args.push("--ignored=traditional");
341
+ const result = await gitRequireOk(
342
+ runner,
343
+ {
344
+ args,
345
+ cwd,
346
+ signal: request?.signal,
347
+ maxOutputBytes: limits.maxOutputBytes,
348
+ },
349
+ "git status",
350
+ );
351
+ return parsePorcelainV2(result.stdout, { maxEntries: limits.maxChangedFiles });
352
+ }
353
+
354
+ async function ensureCleanOrCheckpoint(
355
+ createCheckpoint: boolean | undefined,
356
+ signal: AbortSignal | undefined,
357
+ label: string,
358
+ allowPaths?: ReadonlySet<string>,
359
+ ): Promise<string | undefined> {
360
+ const current = await status({ signal });
361
+ const blocking = current.entries.filter((entry) => {
362
+ if (entry.kind === "ignored") return false;
363
+ if (!allowPaths) return true;
364
+ if (allowPaths.has(entry.path)) return false;
365
+ if (entry.origPath && allowPaths.has(entry.origPath)) return false;
366
+ return true;
367
+ });
368
+ if (blocking.length === 0) return undefined;
369
+ if (!createCheckpoint) {
370
+ throw new GitError(
371
+ `${label} refused: worktree is dirty. Pass createCheckpoint=true to stash a bounded checkpoint first, or use a disposable worktree.`,
372
+ );
373
+ }
374
+ const stashPaths = [...new Set(blocking.flatMap((entry) => (entry.origPath ? [entry.path, entry.origPath] : [entry.path])))];
375
+ if (stashPaths.length > limits.maxPaths) {
376
+ throw new GitError(`checkpoint paths exceed ${limits.maxPaths} entry limit`);
377
+ }
378
+ await gitRequireOk(
379
+ runner,
380
+ {
381
+ args: ["stash", "push", "-u", "-m", "prism-git-checkpoint", "--", ...stashPaths],
382
+ cwd,
383
+ signal,
384
+ maxOutputBytes: limits.maxOutputBytes,
385
+ },
386
+ "git stash checkpoint",
387
+ );
388
+ const top = await gitRequireOk(
389
+ runner,
390
+ { args: ["rev-parse", "-q", "--verify", "refs/stash"], cwd, signal, maxOutputBytes: 64 * 1024 },
391
+ "git rev-parse stash",
392
+ );
393
+ return gitText(top).trim() || "refs/stash";
394
+ }
395
+
396
+ async function restoreCheckpoint(checkpoint: string | undefined, signal?: AbortSignal): Promise<boolean> {
397
+ if (!checkpoint) return false;
398
+ await gitRequireOk(
399
+ runner,
400
+ {
401
+ args: ["stash", "pop", "--index"],
402
+ cwd,
403
+ signal,
404
+ maxOutputBytes: limits.maxOutputBytes,
405
+ },
406
+ "git stash pop",
407
+ );
408
+ return true;
409
+ }
410
+
411
+ async function diff(request?: {
412
+ staged?: boolean;
413
+ base?: string;
414
+ paths?: readonly string[];
415
+ signal?: AbortSignal;
416
+ }): Promise<{ text: string; truncated: boolean; lineCount: number; artifact?: ArtifactReference }> {
417
+ const built = ["diff", "--no-ext-diff", "--no-textconv", "--no-color"];
418
+ if (request?.staged) built.push("--cached");
419
+ if (request?.base !== undefined) {
420
+ built.push(validateCommitish(request.base, limits));
421
+ }
422
+ built.push("--");
423
+ if (request?.paths) {
424
+ built.push(...validatePaths(request.paths, limits));
425
+ }
426
+ const result = await gitRequireOk(
427
+ runner,
428
+ { args: built, cwd, signal: request?.signal, maxOutputBytes: limits.maxOutputBytes },
429
+ "git diff",
430
+ );
431
+ const raw = gitText(result);
432
+ const trimmed = truncateLines(raw, limits.maxDiffLines);
433
+ let artifact: ArtifactReference | undefined;
434
+ if (trimmed.truncated && artifacts) {
435
+ artifact = await artifacts({
436
+ kind: "diff",
437
+ filename: "diff.patch",
438
+ bytes: Buffer.from(raw, "utf8"),
439
+ });
440
+ }
441
+ return { ...trimmed, artifact };
442
+ }
443
+
444
+ async function branch(request: {
445
+ action: "validate" | "create" | "switch" | "list" | "delete";
446
+ name?: string;
447
+ force?: boolean;
448
+ createCheckpoint?: boolean;
449
+ signal?: AbortSignal;
450
+ }): Promise<{ refs?: string[]; name?: string; checkpoint?: string }> {
451
+ if (request.action === "list") {
452
+ const result = await gitRequireOk(
453
+ runner,
454
+ {
455
+ args: ["for-each-ref", "--format=%(refname:short)", "refs/heads"],
456
+ cwd,
457
+ signal: request.signal,
458
+ maxOutputBytes: limits.maxOutputBytes,
459
+ },
460
+ "git for-each-ref",
461
+ );
462
+ const refs = gitText(result)
463
+ .split("\n")
464
+ .map((line) => line.trim())
465
+ .filter(Boolean);
466
+ return { refs };
467
+ }
468
+
469
+ const name = await validateBranchName(runner, cwd, request.name ?? "", limits, request.signal);
470
+ if (request.action === "validate") return { name };
471
+
472
+ if (request.action === "create") {
473
+ await gitRequireOk(
474
+ runner,
475
+ { args: ["branch", "--", name], cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes },
476
+ "git branch create",
477
+ );
478
+ return { name };
479
+ }
480
+
481
+ if (request.action === "delete") {
482
+ // `-d` (default) refuses a branch that is not fully merged; `-D` is the caller's explicit choice.
483
+ await gitRequireOk(
484
+ runner,
485
+ {
486
+ args: ["branch", request.force === true ? "-D" : "-d", "--", name],
487
+ cwd,
488
+ signal: request.signal,
489
+ maxOutputBytes: limits.maxOutputBytes,
490
+ },
491
+ "git branch delete",
492
+ );
493
+ return { name };
494
+ }
495
+
496
+ // switch
497
+ const checkpoint = await ensureCleanOrCheckpoint(request.createCheckpoint, request.signal, "git switch");
498
+ try {
499
+ await gitRequireOk(
500
+ runner,
501
+ { args: ["switch", "--", name], cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes },
502
+ "git switch",
503
+ );
504
+ return { name, checkpoint };
505
+ } catch (error) {
506
+ if (checkpoint) await restoreCheckpoint(checkpoint, request.signal).catch(() => undefined);
507
+ throw error;
508
+ }
509
+ }
510
+
511
+ async function worktree(request: {
512
+ action: "list" | "add" | "remove" | "lock" | "unlock";
513
+ path?: string;
514
+ branch?: string;
515
+ base?: string;
516
+ force?: boolean;
517
+ reason?: string;
518
+ signal?: AbortSignal;
519
+ }): Promise<{ worktrees: readonly GitWorktreeEntry[]; path?: string }> {
520
+ if (request.action === "list") {
521
+ const result = await gitRequireOk(
522
+ runner,
523
+ {
524
+ args: ["worktree", "list", "--porcelain", "-z"],
525
+ cwd,
526
+ signal: request.signal,
527
+ maxOutputBytes: limits.maxOutputBytes,
528
+ },
529
+ "git worktree list",
530
+ );
531
+ const records = gitText(result).split("\0").filter(Boolean);
532
+ const worktrees: GitWorktreeEntry[] = [];
533
+ let current: GitWorktreeEntry | undefined;
534
+ for (const record of records) {
535
+ if (record.startsWith("worktree ")) {
536
+ if (current) worktrees.push(current);
537
+ current = { path: record.slice("worktree ".length) };
538
+ } else if (current && record.startsWith("HEAD ")) {
539
+ current.head = record.slice("HEAD ".length);
540
+ } else if (current && record.startsWith("branch ")) {
541
+ current.branch = record.slice("branch ".length);
542
+ } else if (current && record.startsWith("locked")) {
543
+ current.locked = true;
544
+ const reason = record.slice("locked".length).trim();
545
+ if (reason) current.lockReason = reason;
546
+ }
547
+ }
548
+ if (current) worktrees.push(current);
549
+ if (worktrees.length > limits.maxWorktrees) {
550
+ return { worktrees: worktrees.slice(0, limits.maxWorktrees) };
551
+ }
552
+ return { worktrees };
553
+ }
554
+
555
+ if (request.action === "lock" || request.action === "unlock") {
556
+ if (!request.path) throw new GitError("worktree path is required");
557
+ if (request.path.includes("\0") || request.path.startsWith("-")) {
558
+ throw new GitError("worktree path must not start with '-' or contain NUL");
559
+ }
560
+ const args = ["worktree", request.action];
561
+ if (request.action === "lock" && request.reason) {
562
+ if (request.reason.includes("\0") || Buffer.byteLength(request.reason, "utf8") > 1024) {
563
+ throw new GitError("worktree lock reason must be bounded and NUL-free");
564
+ }
565
+ args.push("--reason", request.reason);
566
+ }
567
+ args.push("--", request.path);
568
+ await gitRequireOk(
569
+ runner,
570
+ { args, cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes },
571
+ `git worktree ${request.action}`,
572
+ );
573
+ return { worktrees: (await worktree({ action: "list", signal: request.signal })).worktrees, path: request.path };
574
+ }
575
+
576
+ if (request.action === "add") {
577
+ const existing = await worktree({ action: "list", signal: request.signal });
578
+ if (existing.worktrees.length >= limits.maxWorktrees) {
579
+ throw new GitError(`worktree count would exceed ${limits.maxWorktrees} limit`);
580
+ }
581
+ if (!request.path) throw new GitError("worktree path is required");
582
+ if (request.path.includes("\0") || request.path.startsWith("-")) {
583
+ throw new GitError("worktree path must not start with '-' or contain NUL");
584
+ }
585
+ const args = ["worktree", "add"];
586
+ if (request.branch) {
587
+ const branchName = await validateBranchName(runner, cwd, request.branch, limits, request.signal);
588
+ args.push("-b", branchName);
589
+ }
590
+ args.push("--", request.path);
591
+ if (request.base !== undefined) {
592
+ args.push(validateCommitish(request.base, limits));
593
+ }
594
+ await gitRequireOk(runner, { args, cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes }, "git worktree add");
595
+ return { worktrees: (await worktree({ action: "list", signal: request.signal })).worktrees, path: request.path };
596
+ }
597
+
598
+ // remove
599
+ if (!request.path) throw new GitError("worktree path is required");
600
+ const args = ["worktree", "remove"];
601
+ if (request.force) args.push("--force");
602
+ args.push("--", request.path);
603
+ await gitRequireOk(runner, { args, cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes }, "git worktree remove");
604
+ return { worktrees: (await worktree({ action: "list", signal: request.signal })).worktrees, path: request.path };
605
+ }
606
+
607
+ async function mergeTree(request: {
608
+ into: string;
609
+ from: string;
610
+ signal?: AbortSignal;
611
+ }): Promise<{ tree: string; conflicts: readonly string[] }> {
612
+ const into = await validateBranchName(runner, cwd, request.into, limits, request.signal);
613
+ const from = await validateBranchName(runner, cwd, request.from, limits, request.signal);
614
+ const result = await runner.exec({
615
+ args: ["merge-tree", "--write-tree", "--name-only", into, from],
616
+ cwd,
617
+ signal: request.signal,
618
+ maxOutputBytes: limits.maxOutputBytes,
619
+ });
620
+ if (result.timedOut) throw new GitError("git merge-tree timed out", result.exitCode);
621
+ if (result.aborted) throw new GitError("git merge-tree aborted", result.exitCode);
622
+ // 0 = clean, 1 = conflicts (both report a tree); anything else is a usage/version failure the
623
+ // caller maps to an unsupported-git error rather than a merge result.
624
+ if (result.exitCode !== 0 && result.exitCode !== 1) {
625
+ const detail = gitText(result, "stderr").trim() || gitText(result).trim() || `exit ${result.exitCode}`;
626
+ throw new GitError(`git merge-tree failed: ${detail}`, result.exitCode);
627
+ }
628
+ const lines = gitText(result).split("\n");
629
+ const tree = (lines.shift() ?? "").trim();
630
+ if (!/^[0-9a-f]{40,64}$/.test(tree)) throw new GitError("git merge-tree did not report a tree");
631
+ if (result.exitCode === 0) return { tree, conflicts: [] };
632
+ // Conflicted paths come first, then a blank line, then informational messages.
633
+ const conflicts: string[] = [];
634
+ for (const line of lines) {
635
+ if (line.trim() === "") break;
636
+ conflicts.push(line.trim());
637
+ }
638
+ return { tree, conflicts };
639
+ }
640
+
641
+ async function commitTree(request: { tree: string; parents: readonly string[]; message: string; signal?: AbortSignal }): Promise<string> {
642
+ if (!/^[0-9a-f]{40,64}$/.test(request.tree)) throw new GitError("commit-tree requires a tree oid");
643
+ if (!Array.isArray(request.parents) || request.parents.length === 0) {
644
+ throw new GitError("commit-tree requires at least one parent");
645
+ }
646
+ if (typeof request.message !== "string" || request.message.trim().length === 0) {
647
+ throw new GitError("commit message is required");
648
+ }
649
+ if (byteLength(request.message) > limits.maxMessageBytes) {
650
+ throw new GitError(`commit message exceeds ${limits.maxMessageBytes} byte limit`);
651
+ }
652
+ const args = ["commit-tree", request.tree];
653
+ for (const parent of request.parents) {
654
+ args.push("-p", await validateBranchName(runner, cwd, parent, limits, request.signal));
655
+ }
656
+ args.push("-m", request.message);
657
+ const result = await gitRequireOk(runner, { args, cwd, signal: request.signal, maxOutputBytes: 64 * 1024 }, "git commit-tree");
658
+ const sha = gitText(result).trim();
659
+ if (!/^[0-9a-f]{40,64}$/.test(sha)) throw new GitError("git commit-tree did not report a commit");
660
+ return sha;
661
+ }
662
+
663
+ async function stageAll(options?: { signal?: AbortSignal }): Promise<void> {
664
+ await gitRequireOk(runner, { args: ["add", "-A"], cwd, signal: options?.signal, maxOutputBytes: limits.maxOutputBytes }, "git add -A");
665
+ }
666
+
667
+ async function writeTree(options?: { signal?: AbortSignal }): Promise<string> {
668
+ const result = await gitRequireOk(
669
+ runner,
670
+ { args: ["write-tree"], cwd, signal: options?.signal, maxOutputBytes: 64 * 1024 },
671
+ "git write-tree",
672
+ );
673
+ const tree = gitText(result).trim();
674
+ if (!/^[0-9a-f]{40,64}$/.test(tree)) throw new GitError("git write-tree did not report a tree");
675
+ return tree;
676
+ }
677
+
678
+ async function readTree(request: { tree: string; signal?: AbortSignal }): Promise<void> {
679
+ await gitRequireOk(
680
+ runner,
681
+ { args: ["read-tree", validateCommitish(request.tree, limits)], cwd, signal: request.signal, maxOutputBytes: 64 * 1024 },
682
+ "git read-tree",
683
+ );
684
+ }
685
+
686
+ async function updateRef(request: { ref: string; to: string; expected?: string; signal?: AbortSignal }): Promise<void> {
687
+ const ref = await validateRef(runner, cwd, request.ref, limits, request.signal);
688
+ const to = await validateBranchName(runner, cwd, request.to, limits, request.signal);
689
+ const args = ["update-ref", ref, to];
690
+ if (request.expected !== undefined) {
691
+ args.push(await validateBranchName(runner, cwd, request.expected, limits, request.signal));
692
+ }
693
+ await gitRequireOk(runner, { args, cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes }, "git update-ref");
694
+ }
695
+
696
+ async function fastForward(request: { to: string; signal?: AbortSignal }): Promise<void> {
697
+ const to = await validateBranchName(runner, cwd, request.to, limits, request.signal);
698
+ await gitRequireOk(
699
+ runner,
700
+ { args: ["merge", "--ff-only", "--no-edit", to], cwd, signal: request.signal, maxOutputBytes: limits.maxOutputBytes },
701
+ "git merge --ff-only",
702
+ );
703
+ }
704
+
705
+ async function untrackedFiles(options?: { signal?: AbortSignal }): Promise<readonly string[]> {
706
+ const result = await gitRequireOk(
707
+ runner,
708
+ {
709
+ args: ["ls-files", "--others", "--exclude-standard", "-z"],
710
+ cwd,
711
+ signal: options?.signal,
712
+ maxOutputBytes: limits.maxOutputBytes,
713
+ },
714
+ "git ls-files --others",
715
+ );
716
+ const paths = gitText(result).split("\0").filter(Boolean);
717
+ return paths.length > limits.maxChangedFiles ? paths.slice(0, limits.maxChangedFiles) : paths;
718
+ }
719
+
720
+ async function fingerprint(options?: { signal?: AbortSignal }): Promise<GitFingerprint> {
721
+ const optional = async (args: readonly string[]): Promise<string | undefined> => {
722
+ const result = await runner.exec({ args, cwd, signal: options?.signal, maxOutputBytes: 64 * 1024 });
723
+ if (result.exitCode !== 0) return undefined;
724
+ const text = gitText(result).trim();
725
+ return text.length > 0 ? text : undefined;
726
+ };
727
+ const remoteUrl = await optional(["config", "--get", "remote.origin.url"]);
728
+ const originHead = await optional(["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"]);
729
+ const initDefault = await optional(["config", "--get", "init.defaultBranch"]);
730
+ const defaultBranch = originHead ? originHead.replace(/^origin\//, "") : initDefault;
731
+ const redacted = redactRemoteUrl(remoteUrl ?? "");
732
+ const fingerprintSource = Buffer.from(`${redacted}\0${defaultBranch ?? ""}`, "utf8");
733
+ return { remoteFingerprint: sha256Hex(fingerprintSource), defaultBranch };
734
+ }
735
+
736
+ async function apply(request: {
737
+ patch: string;
738
+ action: "check" | "apply" | "reverse";
739
+ createCheckpoint?: boolean;
740
+ signal?: AbortSignal;
741
+ }): Promise<{ ok: boolean; checkpoint?: string; restored?: boolean; output: string }> {
742
+ if (typeof request.patch !== "string") throw new GitError("patch must be a string");
743
+ const patchBytes = byteLength(request.patch);
744
+ if (patchBytes < 1) throw new GitError("patch must be non-empty");
745
+ if (patchBytes > limits.maxPatchBytes) {
746
+ throw new GitError(`patch exceeds ${limits.maxPatchBytes} byte limit`);
747
+ }
748
+
749
+ const runApply = async (args: string[], filePath: string): Promise<GitExecResult> =>
750
+ runner.exec({
751
+ args: [...args, "--", filePath],
752
+ cwd,
753
+ signal: request.signal,
754
+ maxOutputBytes: limits.maxOutputBytes,
755
+ });
756
+
757
+ return await withTempFile("prism-git-patch-", request.patch, async (filePath) => {
758
+ if (request.action === "check") {
759
+ const result = await runApply(["apply", "--check"], filePath);
760
+ const output = (gitText(result, "stderr") || gitText(result)).trim();
761
+ if (result.exitCode !== 0) {
762
+ return { ok: false, output: output || `exit ${result.exitCode}` };
763
+ }
764
+ return { ok: true, output: output || "patch applies cleanly" };
765
+ }
766
+
767
+ const checkpoint =
768
+ request.action === "apply"
769
+ ? await ensureCleanOrCheckpoint(request.createCheckpoint, request.signal, `git apply ${request.action}`)
770
+ : undefined;
771
+
772
+ // Always check first for apply/reverse.
773
+ const checkArgs = request.action === "reverse" ? ["apply", "--reverse", "--check"] : ["apply", "--check"];
774
+ const check = await runApply(checkArgs, filePath);
775
+ if (check.exitCode !== 0) {
776
+ const output = (gitText(check, "stderr") || gitText(check)).trim();
777
+ if (checkpoint) await restoreCheckpoint(checkpoint, request.signal).catch(() => undefined);
778
+ return { ok: false, checkpoint, restored: Boolean(checkpoint), output: output || "patch check failed" };
779
+ }
780
+
781
+ const applyArgs = request.action === "reverse" ? ["apply", "--reverse"] : ["apply"];
782
+ const result = await runApply(applyArgs, filePath);
783
+ if (result.exitCode !== 0) {
784
+ const output = (gitText(result, "stderr") || gitText(result)).trim();
785
+ let restored = false;
786
+ if (checkpoint) {
787
+ restored = await restoreCheckpoint(checkpoint, request.signal).catch(() => false);
788
+ } else {
789
+ // Best-effort restore of tracked files when no checkpoint was taken (clean tree).
790
+ await runner
791
+ .exec({
792
+ args: ["checkout", "--", "."],
793
+ cwd,
794
+ signal: request.signal,
795
+ maxOutputBytes: limits.maxOutputBytes,
796
+ })
797
+ .catch(() => undefined);
798
+ restored = true;
799
+ }
800
+ return { ok: false, checkpoint, restored, output: output || `apply failed with exit ${result.exitCode}` };
801
+ }
802
+ return {
803
+ ok: true,
804
+ checkpoint,
805
+ output: (gitText(result, "stderr") || gitText(result) || "patch applied").trim(),
806
+ };
807
+ });
808
+ }
809
+
810
+ async function commit(request: {
811
+ paths: readonly string[];
812
+ message: string;
813
+ createCheckpoint?: boolean;
814
+ signal?: AbortSignal;
815
+ }): Promise<{ sha: string; checkpoint?: string }> {
816
+ const paths = validatePaths(request.paths, limits);
817
+ if (typeof request.message !== "string" || request.message.trim().length === 0) {
818
+ throw new GitError("commit message is required");
819
+ }
820
+ if (byteLength(request.message) > limits.maxMessageBytes) {
821
+ throw new GitError(`commit message exceeds ${limits.maxMessageBytes} byte limit`);
822
+ }
823
+
824
+ const identity = options.commitIdentity;
825
+ if (!identity?.name?.trim() || !identity?.email?.trim()) {
826
+ throw new GitError("commitIdentity name and email are required for git commit");
827
+ }
828
+ if (identity.name.includes("\n") || identity.email.includes("\n")) {
829
+ throw new GitError("commitIdentity must not contain newlines");
830
+ }
831
+
832
+ const checkpoint = await ensureCleanOrCheckpoint(request.createCheckpoint, request.signal, "git commit", new Set(paths));
833
+
834
+ try {
835
+ await gitRequireOk(
836
+ runner,
837
+ {
838
+ args: ["add", "--", ...paths],
839
+ cwd,
840
+ signal: request.signal,
841
+ maxOutputBytes: limits.maxOutputBytes,
842
+ },
843
+ "git add",
844
+ );
845
+
846
+ await withTempFile("prism-git-msg-", request.message, async (messageFile) => {
847
+ await gitRequireOk(
848
+ runner,
849
+ {
850
+ args: [
851
+ "-c",
852
+ `user.name=${identity.name}`,
853
+ "-c",
854
+ `user.email=${identity.email}`,
855
+ "commit",
856
+ "--no-verify",
857
+ "-F",
858
+ messageFile,
859
+ "--",
860
+ ...paths,
861
+ ],
862
+ cwd,
863
+ signal: request.signal,
864
+ maxOutputBytes: limits.maxOutputBytes,
865
+ },
866
+ "git commit",
867
+ );
868
+ });
869
+
870
+ const shaResult = await gitRequireOk(
871
+ runner,
872
+ { args: ["rev-parse", "HEAD"], cwd, signal: request.signal, maxOutputBytes: 64 * 1024 },
873
+ "git rev-parse HEAD",
874
+ );
875
+ return { sha: gitText(shaResult).trim(), checkpoint };
876
+ } catch (error) {
877
+ // Reset index for the attempted paths; never drop pre-existing dirty work unless checkpointed.
878
+ await runner
879
+ .exec({
880
+ args: ["reset", "-q", "HEAD", "--", ...paths],
881
+ cwd,
882
+ signal: request.signal,
883
+ maxOutputBytes: limits.maxOutputBytes,
884
+ })
885
+ .catch(() => undefined);
886
+ if (checkpoint) await restoreCheckpoint(checkpoint, request.signal).catch(() => undefined);
887
+ throw error;
888
+ }
889
+ }
890
+
891
+ async function prHandoff(request: {
892
+ base: string;
893
+ head?: string;
894
+ checks?: readonly { name: string; exitCode: number; summary: string }[];
895
+ includeBundle?: boolean;
896
+ signal?: AbortSignal;
897
+ }): Promise<PrHandoff> {
898
+ const base = request.base;
899
+ if (!base || byteLength(base) > limits.maxRefBytes) {
900
+ throw new GitError("base ref is required and must be within ref byte limits");
901
+ }
902
+ const headResult = await gitRequireOk(
903
+ runner,
904
+ {
905
+ args: ["rev-parse", "--verify", request.head ?? "HEAD"],
906
+ cwd,
907
+ signal: request.signal,
908
+ maxOutputBytes: 64 * 1024,
909
+ },
910
+ "git rev-parse head",
911
+ );
912
+ const head = gitText(headResult).trim();
913
+
914
+ const baseShaResult = await gitRequireOk(
915
+ runner,
916
+ { args: ["rev-parse", "--verify", base], cwd, signal: request.signal, maxOutputBytes: 64 * 1024 },
917
+ "git rev-parse base",
918
+ );
919
+ const baseSha = gitText(baseShaResult).trim();
920
+
921
+ const log = await gitRequireOk(
922
+ runner,
923
+ {
924
+ args: ["log", "--format=%H%x09%s", `${baseSha}..${head}`],
925
+ cwd,
926
+ signal: request.signal,
927
+ maxOutputBytes: limits.maxOutputBytes,
928
+ },
929
+ "git log",
930
+ );
931
+ const commits = gitText(log)
932
+ .split("\n")
933
+ .map((line) => line.trim())
934
+ .filter(Boolean)
935
+ .slice(0, limits.maxPrCommits)
936
+ .map((line) => {
937
+ const tab = line.indexOf("\t");
938
+ if (tab < 0) return { sha: line, subject: "" };
939
+ return { sha: line.slice(0, tab), subject: line.slice(tab + 1) };
940
+ });
941
+
942
+ const nameStatus = await gitRequireOk(
943
+ runner,
944
+ {
945
+ args: ["diff", "--no-ext-diff", "--no-textconv", "--name-only", `${baseSha}...${head}`],
946
+ cwd,
947
+ signal: request.signal,
948
+ maxOutputBytes: limits.maxOutputBytes,
949
+ },
950
+ "git diff name-only",
951
+ );
952
+ const changedPaths = gitText(nameStatus)
953
+ .split("\n")
954
+ .map((line) => line.trim())
955
+ .filter(Boolean)
956
+ .slice(0, limits.maxChangedFiles)
957
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
958
+
959
+ const stat = await gitRequireOk(
960
+ runner,
961
+ {
962
+ args: ["diff", "--no-ext-diff", "--no-textconv", "--stat", `${baseSha}...${head}`],
963
+ cwd,
964
+ signal: request.signal,
965
+ maxOutputBytes: limits.maxOutputBytes,
966
+ },
967
+ "git diff --stat",
968
+ );
969
+ const diffstat = truncateLines(gitText(stat), 200).text.trim();
970
+
971
+ let artifact: ArtifactReference | undefined;
972
+ if (artifacts) {
973
+ if (request.includeBundle) {
974
+ const bundleDir = await mkdtemp(join(tmpdir(), "prism-git-bundle-"));
975
+ const bundlePath = join(bundleDir, "handoff.bundle");
976
+ try {
977
+ await gitRequireOk(
978
+ runner,
979
+ {
980
+ args: ["bundle", "create", bundlePath, `${baseSha}..${head}`],
981
+ cwd,
982
+ signal: request.signal,
983
+ maxOutputBytes: limits.maxOutputBytes,
984
+ },
985
+ "git bundle create",
986
+ );
987
+ const { readFile } = await import("node:fs/promises");
988
+ const bytes = await readFile(bundlePath);
989
+ artifact = await artifacts({ kind: "bundle", filename: "handoff.bundle", bytes });
990
+ } finally {
991
+ await rm(bundleDir, { recursive: true, force: true });
992
+ }
993
+ } else {
994
+ const patch = await gitRequireOk(
995
+ runner,
996
+ {
997
+ args: ["diff", "--no-ext-diff", "--no-textconv", "--binary", `${baseSha}...${head}`],
998
+ cwd,
999
+ signal: request.signal,
1000
+ maxOutputBytes: limits.maxOutputBytes,
1001
+ },
1002
+ "git diff patch",
1003
+ );
1004
+ artifact = await artifacts({
1005
+ kind: "patch",
1006
+ filename: "handoff.patch",
1007
+ bytes: patch.stdout,
1008
+ });
1009
+ }
1010
+ }
1011
+
1012
+ const handoff: PrHandoff = {
1013
+ base: baseSha,
1014
+ head,
1015
+ commits,
1016
+ changedPaths,
1017
+ diffstat,
1018
+ checks: [...(request.checks ?? [])],
1019
+ artifact,
1020
+ };
1021
+
1022
+ const encoded = Buffer.from(JSON.stringify(handoff), "utf8");
1023
+ if (encoded.length > limits.maxPrHandoffBytes) {
1024
+ throw new GitError(`PR handoff JSON exceeds ${limits.maxPrHandoffBytes} byte limit`);
1025
+ }
1026
+ return handoff;
1027
+ }
1028
+
1029
+ return {
1030
+ status,
1031
+ diff,
1032
+ branch,
1033
+ worktree,
1034
+ mergeTree,
1035
+ commitTree,
1036
+ updateRef,
1037
+ fastForward,
1038
+ stageAll,
1039
+ writeTree,
1040
+ readTree,
1041
+ untrackedFiles,
1042
+ fingerprint,
1043
+ apply,
1044
+ commit,
1045
+ prHandoff,
1046
+ };
1047
+ }
1048
+
1049
+ /** Strip credentials from a remote URL, keeping scheme and host/path (plan 026 Task 3). */
1050
+ export function redactRemoteUrl(url: string): string {
1051
+ if (typeof url !== "string" || url.length === 0) return "";
1052
+ let rest = url;
1053
+ let scheme = "";
1054
+ const schemeMatch = /^([a-z][a-z0-9+.-]*:\/\/)/i.exec(url);
1055
+ if (schemeMatch) {
1056
+ scheme = schemeMatch[1];
1057
+ rest = url.slice(scheme.length);
1058
+ }
1059
+ const at = rest.lastIndexOf("@");
1060
+ const slash = rest.indexOf("/");
1061
+ if (at >= 0 && (slash < 0 || at < slash)) rest = rest.slice(at + 1);
1062
+ return scheme + rest;
1063
+ }
1064
+
1065
+ export type { BoundGitRunner, CreateGitRunnerOptions, GitExecRequest, GitExecResult, GitRunner } from "./git-exec.js";
1066
+ export {
1067
+ createBoundGitRunner,
1068
+ GitError,
1069
+ runGitCli,
1070
+ SAFE_GIT_CONFIG_ARGS,
1071
+ SAFE_GIT_ENV,
1072
+ } from "./git-exec.js";
1073
+ export type { GitStatusBranch, GitStatusEntry, GitStatusEntryKind, GitStatusResult } from "./git-status.js";
1074
+ export { parsePorcelainV2 } from "./git-status.js";