@pugi/cli 0.1.0-beta.9 → 0.1.0-beta.91

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 (411) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/deploy.js +40 -40
  6. package/dist/commands/flatten.js +191 -0
  7. package/dist/commands/jobs-watch.js +201 -0
  8. package/dist/commands/jobs.js +42 -27
  9. package/dist/commands/smoke.js +133 -0
  10. package/dist/core/agent-progress/cleanup.js +134 -0
  11. package/dist/core/agent-progress/schema.js +144 -0
  12. package/dist/core/agent-progress/writer.js +101 -0
  13. package/dist/core/agents/adaptive-router.js +330 -0
  14. package/dist/core/agents/query-decomposer.js +297 -0
  15. package/dist/core/agents/registry.js +3 -3
  16. package/dist/core/approvals/shortcut-resolver.js +98 -0
  17. package/dist/core/artifact-chain/dispatcher.js +148 -0
  18. package/dist/core/artifact-chain/exporter.js +164 -0
  19. package/dist/core/artifact-chain/state.js +243 -0
  20. package/dist/core/artifact-chain/steps.js +169 -0
  21. package/dist/core/ask-user/question.js +92 -0
  22. package/dist/core/audit/audit-trail.js +275 -0
  23. package/dist/core/auth/ensure-authenticated.js +129 -0
  24. package/dist/core/auth/env-provider.js +238 -0
  25. package/dist/core/auto-open-browser.js +4 -4
  26. package/dist/core/auto-update/channels.js +122 -0
  27. package/dist/core/auto-update/checker.js +241 -0
  28. package/dist/core/auto-update/state.js +235 -0
  29. package/dist/core/bare-mode/index.js +107 -0
  30. package/dist/core/bash/redirect.js +281 -0
  31. package/dist/core/bash-classifier.js +436 -40
  32. package/dist/core/checkpoint/resumer.js +149 -0
  33. package/dist/core/checkpoint/rewinder.js +291 -0
  34. package/dist/core/checkpoints/shadow-git.js +670 -0
  35. package/dist/core/citations/parser.js +109 -0
  36. package/dist/core/classifier/yolo-classifier.js +88 -0
  37. package/dist/core/codegraph/decision-store.js +248 -0
  38. package/dist/core/codegraph/detect-repo.js +459 -0
  39. package/dist/core/codegraph/install.js +134 -0
  40. package/dist/core/codegraph/offer-hook.js +220 -0
  41. package/dist/core/compact/auto-trigger.js +96 -0
  42. package/dist/core/compact/buffer-rewriter.js +115 -0
  43. package/dist/core/compact/summarizer.js +208 -0
  44. package/dist/core/compact/token-counter.js +108 -0
  45. package/dist/core/consensus/anvil-fanout.js +25 -25
  46. package/dist/core/consensus/diff-capture.js +121 -12
  47. package/dist/core/consensus/rubric.js +21 -21
  48. package/dist/core/context/builder.js +6 -6
  49. package/dist/core/context/compaction-events.js +8 -8
  50. package/dist/core/context/compaction.js +31 -31
  51. package/dist/core/context/index.js +15 -8
  52. package/dist/core/context/invariants.js +51 -51
  53. package/dist/core/context/markdown-loader.js +28 -10
  54. package/dist/core/context/markdown-traverse.js +255 -0
  55. package/dist/core/context/pugiignore.js +41 -41
  56. package/dist/core/context/repo-skeleton.js +37 -37
  57. package/dist/core/context/tool-eviction.js +55 -0
  58. package/dist/core/context/watcher.js +32 -32
  59. package/dist/core/context/working-set.js +23 -23
  60. package/dist/core/coordinator/agent-tools.js +77 -0
  61. package/dist/core/coordinator/agent-toolset.js +65 -0
  62. package/dist/core/coordinator/fsm.js +73 -0
  63. package/dist/core/coordinator/mode-fsm.js +70 -0
  64. package/dist/core/cost/rate-card.js +129 -0
  65. package/dist/core/cost/tracker.js +221 -0
  66. package/dist/core/credentials.js +13 -13
  67. package/dist/core/cron/scheduler.js +138 -0
  68. package/dist/core/denial-tracking/index.js +8 -0
  69. package/dist/core/denial-tracking/state.js +264 -0
  70. package/dist/core/diagnostics/probe-runner.js +93 -0
  71. package/dist/core/diagnostics/probes/api.js +46 -0
  72. package/dist/core/diagnostics/probes/auth.js +93 -0
  73. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  74. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  75. package/dist/core/diagnostics/probes/config.js +72 -0
  76. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  77. package/dist/core/diagnostics/probes/disk.js +81 -0
  78. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  79. package/dist/core/diagnostics/probes/git.js +65 -0
  80. package/dist/core/diagnostics/probes/hooks.js +118 -0
  81. package/dist/core/diagnostics/probes/mcp.js +75 -0
  82. package/dist/core/diagnostics/probes/node.js +59 -0
  83. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  84. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  85. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  86. package/dist/core/diagnostics/probes/session.js +74 -0
  87. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  88. package/dist/core/diagnostics/probes/workspace.js +63 -0
  89. package/dist/core/diagnostics/types.js +70 -0
  90. package/dist/core/dispatch/cache-cleanup.js +197 -0
  91. package/dist/core/dispatch/cache-handoff.js +295 -0
  92. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  93. package/dist/core/edits/dispatch.js +333 -7
  94. package/dist/core/edits/format-detector.js +260 -0
  95. package/dist/core/edits/format-matrix.js +26 -0
  96. package/dist/core/edits/fuzzy-ladder.js +650 -0
  97. package/dist/core/edits/index.js +5 -1
  98. package/dist/core/edits/journal.js +199 -0
  99. package/dist/core/edits/layer-a-apply.js +15 -15
  100. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  101. package/dist/core/edits/layer-b-apply.js +9 -9
  102. package/dist/core/edits/layer-c-apply.js +6 -6
  103. package/dist/core/edits/layer-d-ast.js +557 -14
  104. package/dist/core/edits/marker-parser.js +12 -12
  105. package/dist/core/edits/security-gate.js +27 -27
  106. package/dist/core/edits/verify-hook.js +273 -0
  107. package/dist/core/edits/worktree.js +29 -29
  108. package/dist/core/engine/anvil-client.js +214 -26
  109. package/dist/core/engine/auto-compact.js +179 -0
  110. package/dist/core/engine/budgets.js +186 -0
  111. package/dist/core/engine/context-prefix.js +155 -0
  112. package/dist/core/engine/index.js +1 -1
  113. package/dist/core/engine/intensity.js +158 -0
  114. package/dist/core/engine/intent.js +260 -0
  115. package/dist/core/engine/native-pugi.js +1295 -227
  116. package/dist/core/engine/prompts.js +129 -19
  117. package/dist/core/engine/strip-internal-fields.js +124 -0
  118. package/dist/core/engine/tool-bridge.js +1792 -59
  119. package/dist/core/evaluation/golden-dataset.js +293 -0
  120. package/dist/core/feedback/queue.js +177 -0
  121. package/dist/core/feedback/submitter.js +145 -0
  122. package/dist/core/file-cache.js +113 -1
  123. package/dist/core/flatten/flatten-repo.js +439 -0
  124. package/dist/core/format/osc8-link.js +28 -0
  125. package/dist/core/hook-chains.js +392 -0
  126. package/dist/core/hooks/citation-verify-hook.js +138 -0
  127. package/dist/core/hooks/citation-verify.js +112 -0
  128. package/dist/core/hooks/events.js +46 -0
  129. package/dist/core/hooks/index.js +15 -0
  130. package/dist/core/hooks/registry.js +216 -0
  131. package/dist/core/hooks/runner.js +236 -0
  132. package/dist/core/hooks/v2/event-emitter.js +115 -0
  133. package/dist/core/hooks/v2/executor.js +282 -0
  134. package/dist/core/hooks/v2/index.js +25 -0
  135. package/dist/core/hooks/v2/lifecycle.js +104 -0
  136. package/dist/core/hooks/v2/loader.js +216 -0
  137. package/dist/core/hooks/v2/matcher.js +125 -0
  138. package/dist/core/hooks/v2/trust.js +143 -0
  139. package/dist/core/hooks/v2/types.js +86 -0
  140. package/dist/core/hooks/worktree-events.js +158 -0
  141. package/dist/core/image/renderer.js +71 -0
  142. package/dist/core/init/detector.js +582 -0
  143. package/dist/core/init/template-renderer.js +242 -0
  144. package/dist/core/jobs/registry.js +18 -18
  145. package/dist/core/ledger/results-tsv.js +142 -0
  146. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  147. package/dist/core/lsp/cache.js +105 -0
  148. package/dist/core/lsp/client.js +551 -41
  149. package/dist/core/lsp/language-detect.js +66 -0
  150. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  151. package/dist/core/lsp/server-detect.js +173 -0
  152. package/dist/core/lsp/symbol-cache.js +162 -0
  153. package/dist/core/lsp/symbol-tools.js +664 -0
  154. package/dist/core/mcp/client.js +97 -28
  155. package/dist/core/mcp/http-server.js +553 -0
  156. package/dist/core/mcp/orchestrator-tools.js +662 -0
  157. package/dist/core/mcp/permission.js +190 -0
  158. package/dist/core/mcp/registry.js +39 -17
  159. package/dist/core/mcp/server-tools.js +219 -0
  160. package/dist/core/mcp/server.js +397 -0
  161. package/dist/core/mcp/trust.js +10 -10
  162. package/dist/core/memory/dual-write.js +416 -0
  163. package/dist/core/memory/passive-extract.js +130 -0
  164. package/dist/core/memory/phase1-kinds.js +20 -0
  165. package/dist/core/memory/secret-scanner.js +304 -0
  166. package/dist/core/memory-sync/queue.js +170 -0
  167. package/dist/core/metrics/extract.js +113 -0
  168. package/dist/core/modes/roo-modes.js +68 -0
  169. package/dist/core/onboarding/ensure-initialized.js +133 -0
  170. package/dist/core/onboarding/marker.js +111 -0
  171. package/dist/core/onboarding/telemetry-state.js +108 -0
  172. package/dist/core/output-style/presets.js +176 -0
  173. package/dist/core/output-style/state.js +185 -0
  174. package/dist/core/path-security.js +287 -5
  175. package/dist/core/permission.js +82 -22
  176. package/dist/core/permissions/auto-classifier.js +124 -0
  177. package/dist/core/permissions/bash-parser.js +371 -0
  178. package/dist/core/permissions/circuit-breaker.js +83 -0
  179. package/dist/core/permissions/constrained-edit.js +91 -0
  180. package/dist/core/permissions/gate.js +278 -0
  181. package/dist/core/permissions/index.js +20 -0
  182. package/dist/core/permissions/mode.js +174 -0
  183. package/dist/core/permissions/network-egress.js +137 -0
  184. package/dist/core/permissions/state.js +241 -0
  185. package/dist/core/permissions/tool-class.js +93 -0
  186. package/dist/core/plan-mode/ui-state.js +51 -0
  187. package/dist/core/plans/plan-artifact.js +721 -0
  188. package/dist/core/policy-limits/etag-store.js +122 -0
  189. package/dist/core/prd-check/parser.js +215 -0
  190. package/dist/core/prd-check/reporter.js +127 -0
  191. package/dist/core/prd-check/session-review.js +557 -0
  192. package/dist/core/prd-check/verifiers.js +223 -0
  193. package/dist/core/prompt-cache/client-cache.js +99 -0
  194. package/dist/core/prompts/assembly.js +29 -0
  195. package/dist/core/prompts/registry.js +364 -0
  196. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  197. package/dist/core/pugi-md/context-injector.js +76 -0
  198. package/dist/core/pugi-md/walk-up.js +207 -0
  199. package/dist/core/python/uv-installer.js +270 -0
  200. package/dist/core/python/uv-resolver.js +83 -0
  201. package/dist/core/rate-limit/narrator.js +146 -0
  202. package/dist/core/recipes/cli-types.js +20 -0
  203. package/dist/core/recipes/loader.js +103 -0
  204. package/dist/core/recipes/runner.js +345 -0
  205. package/dist/core/recipes/schema.js +587 -0
  206. package/dist/core/release-notes/parser.js +241 -0
  207. package/dist/core/release-notes/state.js +116 -0
  208. package/dist/core/repl/ask.js +37 -37
  209. package/dist/core/repl/cancellation.js +26 -26
  210. package/dist/core/repl/cap-warning.js +4 -4
  211. package/dist/core/repl/clipboard-read.js +11 -11
  212. package/dist/core/repl/dispatch-fsm.js +12 -12
  213. package/dist/core/repl/history-search.js +15 -15
  214. package/dist/core/repl/history.js +28 -18
  215. package/dist/core/repl/kill-ring.js +5 -5
  216. package/dist/core/repl/model-pricing.js +135 -0
  217. package/dist/core/repl/privacy-banner.js +22 -22
  218. package/dist/core/repl/session.js +2148 -217
  219. package/dist/core/repl/slash-commands.js +501 -41
  220. package/dist/core/repl/store/index.js +1 -1
  221. package/dist/core/repl/store/jsonl-log.js +22 -22
  222. package/dist/core/repl/store/lockfile.js +10 -10
  223. package/dist/core/repl/store/session-store.js +136 -107
  224. package/dist/core/repl/store/types.js +15 -15
  225. package/dist/core/repl/store/uuid-v7.js +12 -12
  226. package/dist/core/repl/workspace-context.js +43 -21
  227. package/dist/core/repo-map/build.js +125 -0
  228. package/dist/core/repo-map/cache.js +185 -0
  229. package/dist/core/repo-map/extractor.js +254 -0
  230. package/dist/core/repo-map/formatter.js +145 -0
  231. package/dist/core/repo-map/page-rank.js +105 -0
  232. package/dist/core/repo-map/scanner.js +211 -0
  233. package/dist/core/retry-budget/budget.js +284 -0
  234. package/dist/core/retry-budget/index.js +5 -0
  235. package/dist/core/retry-budget/retry-cap.js +74 -0
  236. package/dist/core/routing/lead-worker.js +43 -0
  237. package/dist/core/routing/pre-flight-estimator.js +108 -0
  238. package/dist/core/runs/run-tree.js +103 -0
  239. package/dist/core/security/injection-scanner.js +367 -0
  240. package/dist/core/security/output-filter.js +418 -0
  241. package/dist/core/session/env-file.js +105 -0
  242. package/dist/core/session/section-budgets.js +140 -0
  243. package/dist/core/session.js +92 -0
  244. package/dist/core/settings.js +324 -5
  245. package/dist/core/share/formatter.js +271 -0
  246. package/dist/core/share/redactor.js +221 -0
  247. package/dist/core/share/uploader.js +267 -0
  248. package/dist/core/skills/defaults.js +30 -30
  249. package/dist/core/skills/loader.js +22 -22
  250. package/dist/core/skills/sources.js +27 -27
  251. package/dist/core/smoke/headless-driver.js +174 -0
  252. package/dist/core/smoke/orchestrator.js +194 -0
  253. package/dist/core/smoke/runner.js +238 -0
  254. package/dist/core/smoke/scenario-parser.js +316 -0
  255. package/dist/core/statusline.js +99 -0
  256. package/dist/core/subagents/dispatcher-real.js +600 -0
  257. package/dist/core/subagents/dispatcher.js +132 -43
  258. package/dist/core/subagents/index.js +19 -6
  259. package/dist/core/subagents/isolation-matrix.js +213 -0
  260. package/dist/core/subagents/spawn.js +19 -4
  261. package/dist/core/telemetry/emitter.js +229 -0
  262. package/dist/core/telemetry/queue.js +251 -0
  263. package/dist/core/theme/context.js +91 -0
  264. package/dist/core/theme/presets.js +228 -0
  265. package/dist/core/theme/state.js +181 -0
  266. package/dist/core/todos/invariant.js +10 -0
  267. package/dist/core/todos/state.js +177 -0
  268. package/dist/core/tool-schema/compressor.js +89 -0
  269. package/dist/core/transport/version-interceptor.js +166 -0
  270. package/dist/core/trust.js +2 -2
  271. package/dist/core/tui/thinking-block.js +64 -0
  272. package/dist/core/vim/keymap.js +288 -0
  273. package/dist/core/vim/state.js +92 -0
  274. package/dist/core/watch-markers/marker-watcher.js +133 -0
  275. package/dist/core/worktree/include-parser.js +249 -0
  276. package/dist/core/worktree-manager/cleanup.js +123 -0
  277. package/dist/core/worktree-manager/manager.js +303 -0
  278. package/dist/index.js +36 -0
  279. package/dist/runtime/bootstrap.js +190 -0
  280. package/dist/runtime/cli.js +4185 -549
  281. package/dist/runtime/commands/agents.js +31 -31
  282. package/dist/runtime/commands/budget.js +5 -5
  283. package/dist/runtime/commands/cancel.js +231 -0
  284. package/dist/runtime/commands/chain.js +489 -0
  285. package/dist/runtime/commands/codegraph-status.js +227 -0
  286. package/dist/runtime/commands/compact.js +297 -0
  287. package/dist/runtime/commands/config.js +73 -39
  288. package/dist/runtime/commands/cost.js +199 -0
  289. package/dist/runtime/commands/delegate.js +27 -4
  290. package/dist/runtime/commands/dispatch.js +126 -0
  291. package/dist/runtime/commands/doctor.js +579 -0
  292. package/dist/runtime/commands/feedback.js +184 -0
  293. package/dist/runtime/commands/hooks.js +187 -0
  294. package/dist/runtime/commands/init.js +254 -0
  295. package/dist/runtime/commands/lsp.js +200 -38
  296. package/dist/runtime/commands/mcp.js +879 -0
  297. package/dist/runtime/commands/memory.js +582 -0
  298. package/dist/runtime/commands/model.js +237 -0
  299. package/dist/runtime/commands/onboarding.js +275 -0
  300. package/dist/runtime/commands/patch.js +12 -12
  301. package/dist/runtime/commands/permissions.js +112 -0
  302. package/dist/runtime/commands/plan.js +143 -0
  303. package/dist/runtime/commands/prd-check.js +285 -0
  304. package/dist/runtime/commands/privacy.js +17 -17
  305. package/dist/runtime/commands/recipe.js +325 -0
  306. package/dist/runtime/commands/redo-blob-store.js +92 -0
  307. package/dist/runtime/commands/redo.js +361 -0
  308. package/dist/runtime/commands/release-notes.js +229 -0
  309. package/dist/runtime/commands/repo-map.js +95 -0
  310. package/dist/runtime/commands/report.js +299 -0
  311. package/dist/runtime/commands/resume.js +118 -0
  312. package/dist/runtime/commands/review-consensus.js +68 -53
  313. package/dist/runtime/commands/rewind.js +333 -0
  314. package/dist/runtime/commands/roster.js +14 -14
  315. package/dist/runtime/commands/sessions.js +163 -0
  316. package/dist/runtime/commands/share.js +316 -0
  317. package/dist/runtime/commands/skills.js +31 -31
  318. package/dist/runtime/commands/status.js +186 -0
  319. package/dist/runtime/commands/stickers.js +82 -0
  320. package/dist/runtime/commands/style.js +194 -0
  321. package/dist/runtime/commands/theme.js +196 -0
  322. package/dist/runtime/commands/undo.js +54 -22
  323. package/dist/runtime/commands/update.js +289 -0
  324. package/dist/runtime/commands/vim.js +140 -0
  325. package/dist/runtime/commands/worktree.js +8 -8
  326. package/dist/runtime/commands/worktrees.js +155 -0
  327. package/dist/runtime/headless-repl.js +195 -0
  328. package/dist/runtime/headless.js +543 -0
  329. package/dist/runtime/load-hooks-or-exit.js +71 -0
  330. package/dist/runtime/plan-decompose.js +22 -22
  331. package/dist/runtime/sigint-guard.js +272 -0
  332. package/dist/runtime/update-check.js +28 -28
  333. package/dist/runtime/version.js +65 -0
  334. package/dist/runtime/worktree-bootstrap.js +579 -0
  335. package/dist/skills/bundled/batch.js +617 -0
  336. package/dist/skills/bundled/index.js +45 -0
  337. package/dist/skills/bundled/loop.js +358 -0
  338. package/dist/skills/bundled/remember.js +383 -0
  339. package/dist/skills/bundled/simplify.js +289 -0
  340. package/dist/skills/bundled/skillify.js +373 -0
  341. package/dist/skills/bundled/stuck.js +558 -0
  342. package/dist/skills/bundled/verify.js +439 -0
  343. package/dist/testing/vcr.js +486 -0
  344. package/dist/tools/agent-tool.js +229 -0
  345. package/dist/tools/apply-patch.js +89 -28
  346. package/dist/tools/ask-user-question.js +337 -0
  347. package/dist/tools/ask-user.js +115 -0
  348. package/dist/tools/bash.js +624 -46
  349. package/dist/tools/brief.js +224 -0
  350. package/dist/tools/cron.js +433 -0
  351. package/dist/tools/enter-worktree.js +250 -0
  352. package/dist/tools/exit-worktree.js +147 -0
  353. package/dist/tools/file-tools.js +161 -44
  354. package/dist/tools/lsp-tools.js +377 -1
  355. package/dist/tools/mcp-tool.js +260 -0
  356. package/dist/tools/multi-edit.js +361 -0
  357. package/dist/tools/powershell.js +268 -0
  358. package/dist/tools/registry.js +99 -4
  359. package/dist/tools/skill-tool.js +96 -0
  360. package/dist/tools/sleep.js +99 -0
  361. package/dist/tools/synthetic-output.js +133 -0
  362. package/dist/tools/tasks.js +208 -0
  363. package/dist/tools/todo-write.js +184 -0
  364. package/dist/tools/verify-plan-execution.js +295 -0
  365. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  366. package/dist/tools/web-fetch.js +195 -10
  367. package/dist/tools/web-search.js +458 -0
  368. package/dist/tui/agent-progress-card.js +111 -0
  369. package/dist/tui/agent-tree.js +11 -1
  370. package/dist/tui/ask-modal.js +14 -14
  371. package/dist/tui/ask-user-question-chips.js +315 -0
  372. package/dist/tui/ask-user-question-prompt.js +203 -0
  373. package/dist/tui/compact-banner.js +81 -0
  374. package/dist/tui/conversation-pane.js +85 -11
  375. package/dist/tui/cost-table.js +111 -0
  376. package/dist/tui/device-flow.js +2 -2
  377. package/dist/tui/doctor-table.js +46 -0
  378. package/dist/tui/feedback-prompt.js +156 -0
  379. package/dist/tui/input-box.js +247 -32
  380. package/dist/tui/login-picker.js +3 -3
  381. package/dist/tui/markdown-render.js +6 -6
  382. package/dist/tui/multi-file-diff-approval.js +375 -0
  383. package/dist/tui/onboarding-wizard.js +240 -0
  384. package/dist/tui/permissions-picker.js +86 -0
  385. package/dist/tui/render.js +36 -1
  386. package/dist/tui/repl-render.js +176 -25
  387. package/dist/tui/repl-splash-art.js +16 -16
  388. package/dist/tui/repl-splash-mascot.js +48 -24
  389. package/dist/tui/repl-splash.js +22 -22
  390. package/dist/tui/repl.js +125 -45
  391. package/dist/tui/slash-palette.js +6 -6
  392. package/dist/tui/splash.js +2 -2
  393. package/dist/tui/status-bar.js +109 -31
  394. package/dist/tui/status-table.js +7 -0
  395. package/dist/tui/stickers-art.js +136 -0
  396. package/dist/tui/style-table.js +28 -0
  397. package/dist/tui/theme-table.js +29 -0
  398. package/dist/tui/thinking-spinner.js +123 -0
  399. package/dist/tui/tool-stream-pane.js +53 -4
  400. package/dist/tui/update-banner.js +27 -2
  401. package/dist/tui/vim-input.js +267 -0
  402. package/dist/tui/welcome-banner.js +107 -0
  403. package/dist/tui/welcome-data.js +293 -0
  404. package/dist/tui/workspace-context.js +2 -2
  405. package/package.json +31 -16
  406. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  407. package/test/scenarios/compact-force.scenario.txt +12 -0
  408. package/test/scenarios/identity.scenario.txt +12 -0
  409. package/test/scenarios/persona-handoff.scenario.txt +12 -0
  410. package/test/scenarios/walkback.scenario.txt +12 -0
  411. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,670 @@
1
+ /**
2
+ * Per-task shadow git repo — file-state checkpoint surface.
3
+ *
4
+ * Inspired by Cline `CheckpointTracker.ts` (Apache-2.0).
5
+ * independent implementation TypeScript implementation following Pugi conventions.
6
+ *
7
+ * Goal: every Pugi-orchestrated file mutation lands in a per-task
8
+ * shadow git history kept entirely separate from the user's real
9
+ * `.git/`. The operator can list / diff / restore checkpoints with
10
+ * zero pollution of their own commit graph.
11
+ *
12
+ * Design choices:
13
+ *
14
+ * - Shadow `.git` lives at `<cwd>/.pugi/checkpoints/<task-id>/.git`
15
+ * (a regular non-bare repo whose `objects/` + `refs/` stay there).
16
+ * The "shadow repo" is the dir containing `.git`; that parent dir
17
+ * itself is empty — we drive git via `--git-dir=<shadow>/.git
18
+ * --work-tree=<cwd>` so the shadow tracks the user's working tree
19
+ * in-place WITHOUT a separate clone of the file contents.
20
+ *
21
+ * - We never `git init` inside `<cwd>`. The shadow's `.git` dir is
22
+ * under `.pugi/checkpoints/<task-id>/` so the user's real repo
23
+ * (if any) is untouched. Auto-ignore `.pugi/` so the shadow does
24
+ * not recurse into its own metadata.
25
+ *
26
+ * - All git invocations go through `spawnSync` with explicit argv
27
+ * (no shell, no string interpolation). The shadow `.git` dir is
28
+ * chmod 700 so file-content snapshots do not leak to other Unix
29
+ * accounts on shared boxes (memory rule: "shadow `.git` MUST be
30
+ * chmod 700").
31
+ *
32
+ * - Operations are atomic-or-no-op: `initShadowRepo` writes a tmp
33
+ * `.gitignore` and only flips the canonical `.gitignore` once the
34
+ * `git init` + initial commit complete, so a crash between steps
35
+ * leaves a clean "not initialised yet" state on retry.
36
+ *
37
+ * - The commit author / committer is pinned to
38
+ * `Pugi Shadow <shadow@pugi.local>` and `GIT_*_DATE` env vars are
39
+ * set per-commit so the shadow log is reproducible regardless of
40
+ * the host's git config.
41
+ *
42
+ * - Errors that originate from a missing `git` binary surface as
43
+ * `ShadowGitUnavailableError`; the dispatcher's hook treats that
44
+ * as best-effort (logs but does not block the edit).
45
+ */
46
+ import { spawnSync } from 'node:child_process';
47
+ import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
48
+ import { createInterface } from 'node:readline';
49
+ import { join, relative, resolve } from 'node:path';
50
+ /**
51
+ * Reserved task-id used for ad-hoc / "no active task" checkpoints when
52
+ * the dispatcher cannot supply a real task id. Kept as an exported
53
+ * constant so callers in tests and integration glue agree on the
54
+ * fallback bucket.
55
+ */
56
+ export const DEFAULT_TASK_ID = 'default';
57
+ /**
58
+ * Default git author for shadow commits. Kept deterministic so the
59
+ * shadow log is stable across hosts and reproducible in tests.
60
+ */
61
+ const SHADOW_AUTHOR_NAME = 'Pugi Shadow';
62
+ const SHADOW_AUTHOR_EMAIL = 'shadow@pugi.local';
63
+ /**
64
+ * Default ignore template applied to the shadow on init. Excludes the
65
+ * `.pugi/` metadata directory (so shadow operations cannot recurse
66
+ * into their own state) along with the heavy build-output directories
67
+ * that would otherwise turn every dispatch commit into a multi-MB
68
+ * blob churn.
69
+ *
70
+ * Lives as a constant rather than a fixture file so the contents are
71
+ * inspectable from the tests without IO.
72
+ */
73
+ export const SHADOW_GITIGNORE_TEMPLATE = [
74
+ '# Pugi shadow git ignore — managed by core/checkpoints/shadow-git.ts',
75
+ '# Do not edit by hand; reset via `pugi checkpoint reset` once that ships.',
76
+ '.pugi/',
77
+ 'node_modules/',
78
+ '.next/',
79
+ 'dist/',
80
+ 'build/',
81
+ 'coverage/',
82
+ '.turbo/',
83
+ '.cache/',
84
+ '*.log',
85
+ '',
86
+ ].join('\n');
87
+ /**
88
+ * Thrown when `git` is missing or the spawn fails in a way that cannot
89
+ * be recovered. Best-effort callers (the dispatch hook) catch + log
90
+ * + continue; explicit callers (REPL slash commands) surface the
91
+ * message to the operator.
92
+ */
93
+ export class ShadowGitUnavailableError extends Error {
94
+ constructor(message) {
95
+ super(message);
96
+ this.name = 'ShadowGitUnavailableError';
97
+ }
98
+ }
99
+ /** Resolve the on-disk shadow root for a (cwd, taskId) pair. */
100
+ export function shadowRoot(cwd, taskId) {
101
+ return resolve(cwd, '.pugi', 'checkpoints', sanitizeTaskId(taskId));
102
+ }
103
+ /**
104
+ * Resolve the shadow `.git` directory. Convention: regular (non-bare)
105
+ * repo so `git -C <root>` would Just Work if the operator ever needed
106
+ * to introspect it directly.
107
+ */
108
+ export function shadowGitDir(cwd, taskId) {
109
+ return join(shadowRoot(cwd, taskId), '.git');
110
+ }
111
+ /**
112
+ * Idempotent init. When the shadow `.git` already exists this is a
113
+ * no-op (returns `{ created: false }`). On first call:
114
+ *
115
+ * 1. mkdir parent dir tree.
116
+ * 2. `git init` the shadow `.git` (no working tree at the shadow
117
+ * root; we drive every later command with `--work-tree=<cwd>`).
118
+ * 3. Write the canonical `.gitignore` to the shadow root + chmod
119
+ * the shadow `.git` to 0o700 to keep snapshot content owner-only.
120
+ * 4. Stage `.gitignore` + commit an initial "shadow init" so later
121
+ * `git log` always has at least one entry to compare against.
122
+ *
123
+ * Throws `ShadowGitUnavailableError` when `git` is missing.
124
+ */
125
+ export function initShadowRepo(cwd, taskId) {
126
+ const root = shadowRoot(cwd, taskId);
127
+ const gitDir = shadowGitDir(cwd, taskId);
128
+ if (existsSync(gitDir)) {
129
+ return { created: false, root, gitDir };
130
+ }
131
+ mkdirSync(root, { recursive: true });
132
+ // `git init <root>` creates `<root>/.git/` as the git dir. We can't
133
+ // pass `gitDir` directly because that would nest as
134
+ // `<root>/.git/.git/`. Init from `root` then verify `gitDir` exists.
135
+ const initRes = spawnGit(['init', '--quiet', root], { cwd: root });
136
+ if (initRes.error || initRes.status !== 0) {
137
+ throw new ShadowGitUnavailableError(`git init failed for ${root}: ${describeSpawn(initRes)}`);
138
+ }
139
+ try {
140
+ chmodSync(gitDir, 0o700);
141
+ }
142
+ catch {
143
+ // Non-fatal on platforms (Windows / restrictive sandboxes) where
144
+ // chmod does not have effect. The bare path is still under the
145
+ // user's $HOME so the practical exposure is unchanged.
146
+ }
147
+ // Write the exclude template at the shadow root. This file is
148
+ // referenced via `core.excludesFile` on every shadow command — it
149
+ // is NOT committed into the shadow's tree, so it never appears as
150
+ // a phantom delete when `--work-tree` points at the user's `cwd`.
151
+ writeFileSync(join(root, '.gitignore'), SHADOW_GITIGNORE_TEMPLATE);
152
+ // Empty initial commit so `git log` always has at least one anchor.
153
+ // We point `--work-tree` at the user's `cwd` even for the init —
154
+ // `--allow-empty` + nothing staged means no files are touched.
155
+ const commitInit = spawnGit([
156
+ '--git-dir',
157
+ gitDir,
158
+ '--work-tree',
159
+ cwd,
160
+ 'commit',
161
+ '--quiet',
162
+ '--allow-empty',
163
+ '-m',
164
+ 'pugi shadow init',
165
+ ], { cwd, env: shadowEnv() });
166
+ if (commitInit.status !== 0) {
167
+ throw new ShadowGitUnavailableError(`shadow init commit failed: ${describeSpawn(commitInit)}`);
168
+ }
169
+ return { created: true, root, gitDir };
170
+ }
171
+ /**
172
+ * Stage everything in `cwd` (respecting `.gitignore`) and commit a
173
+ * snapshot to the shadow. Returns the new commit SHA, or `null` when
174
+ * the snapshot was a no-op (working tree clean vs. last shadow head).
175
+ *
176
+ * The dispatcher hook calls this AFTER every successful edit. We
177
+ * deliberately tolerate a no-op (no SHA) so a tool that ran but did
178
+ * not actually touch a tracked file does not litter the shadow log
179
+ * with empty commits.
180
+ *
181
+ * `message` is the operator-facing commit body. Convention:
182
+ *
183
+ * pugi <task-id> step <n>: <tool-name> <relative-path>
184
+ *
185
+ * The dispatcher composes this; the shadow module stores it verbatim.
186
+ */
187
+ export function commitCheckpoint(cwd, taskId, message) {
188
+ const gitDir = shadowGitDir(cwd, taskId);
189
+ if (!existsSync(gitDir)) {
190
+ initShadowRepo(cwd, taskId);
191
+ }
192
+ // Apply our exclude template through `core.excludesFile` so the
193
+ // shadow does not pull in `.pugi/`, `node_modules/`, `dist/`, etc.
194
+ // Cannot just place a `.gitignore` in the working tree — that would
195
+ // pollute the user's repo. The exclude file lives under the shadow
196
+ // root and is referenced per-command.
197
+ const excludeFile = join(shadowRoot(cwd, taskId), '.gitignore');
198
+ const add = spawnGit([
199
+ '-c',
200
+ `core.excludesFile=${excludeFile}`,
201
+ '--git-dir',
202
+ gitDir,
203
+ '--work-tree',
204
+ cwd,
205
+ 'add',
206
+ '-A',
207
+ ], { cwd, env: shadowEnv() });
208
+ if (add.status !== 0) {
209
+ throw new ShadowGitUnavailableError(`shadow add failed: ${describeSpawn(add)}`);
210
+ }
211
+ // Check whether there is anything staged. `git diff --cached
212
+ // --quiet` exits 0 iff the index matches HEAD; non-zero (specifically
213
+ // 1) means we have content to commit. Any other exit is an error.
214
+ const diff = spawnGit(['--git-dir', gitDir, '--work-tree', cwd, 'diff', '--cached', '--quiet'], { cwd, env: shadowEnv() });
215
+ if (diff.status === 0) {
216
+ return null;
217
+ }
218
+ if (diff.status !== 1) {
219
+ throw new ShadowGitUnavailableError(`shadow diff probe failed: ${describeSpawn(diff)}`);
220
+ }
221
+ const commit = spawnGit([
222
+ '--git-dir',
223
+ gitDir,
224
+ '--work-tree',
225
+ cwd,
226
+ 'commit',
227
+ '--quiet',
228
+ '-m',
229
+ message,
230
+ ], { cwd, env: shadowEnv() });
231
+ if (commit.status !== 0) {
232
+ throw new ShadowGitUnavailableError(`shadow commit failed: ${describeSpawn(commit)}`);
233
+ }
234
+ const sha = spawnGit(['--git-dir', gitDir, '--work-tree', cwd, 'rev-parse', 'HEAD'], { cwd, env: shadowEnv() });
235
+ if (sha.status !== 0) {
236
+ throw new ShadowGitUnavailableError(`shadow rev-parse HEAD failed: ${describeSpawn(sha)}`);
237
+ }
238
+ return sha.stdout.trim();
239
+ }
240
+ /**
241
+ * List recent checkpoints for `taskId`, newest-first. Returns an
242
+ * empty array when the shadow does not exist (no edits committed
243
+ * yet) — the operator hint ("no checkpoints recorded") lives in the
244
+ * REPL renderer, not here.
245
+ *
246
+ * `limit` is clamped to [1, 200].
247
+ */
248
+ export function listCheckpoints(cwd, taskId, limit = 20) {
249
+ const gitDir = shadowGitDir(cwd, taskId);
250
+ if (!existsSync(gitDir))
251
+ return [];
252
+ const clamped = Math.max(1, Math.min(200, Math.floor(limit)));
253
+ const log = spawnGit([
254
+ '--git-dir',
255
+ gitDir,
256
+ '--work-tree',
257
+ cwd,
258
+ 'log',
259
+ `--max-count=${clamped}`,
260
+ // Tab-delimited so we can split on a char that does not appear
261
+ // in commit messages (we sanitise tabs out when composing the
262
+ // dispatcher message).
263
+ '--pretty=format:%H%x09%ct%x09%s',
264
+ ], { cwd, env: shadowEnv() });
265
+ if (log.status !== 0) {
266
+ // The shadow exists but `git log` failed (likely corrupt). Return
267
+ // empty — the REPL renderer surfaces "no checkpoints" rather than
268
+ // tearing through error UX. Callers that need to know about the
269
+ // corruption should use `verifyShadow` (future).
270
+ return [];
271
+ }
272
+ const out = [];
273
+ for (const line of log.stdout.split('\n')) {
274
+ if (line.length === 0)
275
+ continue;
276
+ const [sha, ctRaw, ...rest] = line.split('\t');
277
+ if (!sha || !ctRaw)
278
+ continue;
279
+ const ct = Number.parseInt(ctRaw, 10);
280
+ if (!Number.isFinite(ct))
281
+ continue;
282
+ out.push({
283
+ sha,
284
+ shortSha: sha.slice(0, 7),
285
+ message: rest.join('\t'),
286
+ committedAt: ct * 1000,
287
+ });
288
+ }
289
+ return out;
290
+ }
291
+ /**
292
+ * Thrown when the operator declines the confirmation prompt OR when
293
+ * the prompt times out. Distinct from `ShadowGitUnavailableError` so
294
+ * callers can differentiate "operator-said-no" from "git-broke".
295
+ */
296
+ export class CheckpointRestoreCancelledError extends Error {
297
+ constructor(message) {
298
+ super(message);
299
+ this.name = 'CheckpointRestoreCancelledError';
300
+ }
301
+ }
302
+ const DEFAULT_RESTORE_PROMPT_TIMEOUT_MS = 30_000;
303
+ /**
304
+ * Restore the working tree to the snapshot at `sha`. Destructive:
305
+ * runs `git checkout --` against every tracked path, overwriting
306
+ * local changes.
307
+ *
308
+ * Triple-review P1-3 — before this fix `restoreCheckpoint` ran
309
+ * without any confirmation. The slash-handler enforced UX out-of-band,
310
+ * but a direct API caller (a future `pugi-mcp` tool, a test, an agent
311
+ * toolcall) would happily overwrite the work tree. The gate here keeps
312
+ * the function safe-by-default while still letting callers opt out
313
+ * with `--yes` or by piping stdin (non-TTY auto-yes — matches every
314
+ * Unix `rm -i` convention).
315
+ *
316
+ * Implementation note: we use `git checkout <sha> -- :/` (path pattern
317
+ * matching the whole work tree) rather than `reset --hard` because
318
+ * `reset --hard` would move the shadow's HEAD, breaking the
319
+ * "checkpoint <sha>" anchor for any later `restoreCheckpoint` call.
320
+ * Operators expect к be able to restore checkpoint A, then checkpoint
321
+ * B, then checkpoint A again — `checkout` preserves that property
322
+ * because HEAD stays put.
323
+ */
324
+ export async function restoreCheckpoint(cwd, taskId, sha, options = {}) {
325
+ const gitDir = shadowGitDir(cwd, taskId);
326
+ if (!existsSync(gitDir)) {
327
+ throw new ShadowGitUnavailableError(`no shadow repo for task ${sanitizeTaskId(taskId)}`);
328
+ }
329
+ if (!/^[0-9a-f]{4,40}$/i.test(sha)) {
330
+ throw new ShadowGitUnavailableError(`refuse to restore: '${sha}' does not look like a git SHA`);
331
+ }
332
+ // Confirmation gate. `--yes` skips. Non-TTY stdin auto-confirms
333
+ // (scripted contexts must not block indefinitely on a prompt). When
334
+ // stdin is a TTY we ask y/N with a 30s timeout — default-N to
335
+ // "did nothing destructive" rather than "operator presumed agreement".
336
+ if (!options.yes && process.stdin.isTTY) {
337
+ const timeoutMs = options.timeoutMs ?? DEFAULT_RESTORE_PROMPT_TIMEOUT_MS;
338
+ const question = `restore shadow checkpoint ${sha.slice(0, 7)}? destroys local changes in ${cwd}. [y/N]: `;
339
+ const reply = options.prompt
340
+ ? await options.prompt(question, timeoutMs)
341
+ : await defaultPrompt(question, timeoutMs);
342
+ if (!/^y(es)?$/i.test(reply.trim())) {
343
+ throw new CheckpointRestoreCancelledError(`restore cancelled by operator (answered '${reply.trim()}')`);
344
+ }
345
+ }
346
+ // Verify the SHA resolves to a real commit before we touch the work
347
+ // tree. `git cat-file -t <sha>` returns "commit" for a commit;
348
+ // anything else (or non-zero exit) we refuse.
349
+ const probe = spawnGit(['--git-dir', gitDir, '--work-tree', cwd, 'cat-file', '-t', sha], { cwd, env: shadowEnv() });
350
+ if (probe.status !== 0 || probe.stdout.trim() !== 'commit') {
351
+ throw new ShadowGitUnavailableError(`refuse to restore: ${sha} is not a commit in shadow repo`);
352
+ }
353
+ const checkout = spawnGit([
354
+ '--git-dir',
355
+ gitDir,
356
+ '--work-tree',
357
+ cwd,
358
+ 'checkout',
359
+ sha,
360
+ '--',
361
+ ':/',
362
+ ], { cwd, env: shadowEnv() });
363
+ if (checkout.status !== 0) {
364
+ throw new ShadowGitUnavailableError(`shadow restore failed: ${describeSpawn(checkout)}`);
365
+ }
366
+ }
367
+ /**
368
+ * Return the diff between `sha` and the current working tree (HEAD
369
+ * of shadow). Format is unified diff text; the REPL renders it
370
+ * verbatim with monospace + colour gates. Empty string when there
371
+ * are no differences.
372
+ */
373
+ export function diffCheckpoint(cwd, taskId, sha) {
374
+ const gitDir = shadowGitDir(cwd, taskId);
375
+ if (!existsSync(gitDir)) {
376
+ throw new ShadowGitUnavailableError(`no shadow repo for task ${sanitizeTaskId(taskId)}`);
377
+ }
378
+ if (!/^[0-9a-f]{4,40}$/i.test(sha)) {
379
+ throw new ShadowGitUnavailableError(`refuse to diff: '${sha}' does not look like a git SHA`);
380
+ }
381
+ const diff = spawnGit([
382
+ '--git-dir',
383
+ gitDir,
384
+ '--work-tree',
385
+ cwd,
386
+ 'diff',
387
+ '--no-color',
388
+ sha,
389
+ '--',
390
+ ], { cwd, env: shadowEnv() });
391
+ if (diff.status !== 0 && diff.status !== 1) {
392
+ // Git diff exit 0 = no diff, 1 = diff present; anything else is an
393
+ // error.
394
+ throw new ShadowGitUnavailableError(`shadow diff failed: ${describeSpawn(diff)}`);
395
+ }
396
+ return diff.stdout;
397
+ }
398
+ /**
399
+ * Garbage-collect old shadow repos under `<cwd>/.pugi/checkpoints/`.
400
+ * Removes a directory iff:
401
+ *
402
+ * - Its mtime is older than `maxAgeDays` days, OR
403
+ * - The aggregate disk usage of the checkpoints root exceeds
404
+ * `maxSizeMB` (oldest entries removed first until back under).
405
+ *
406
+ * Returns a `PruneReport`. Best-effort: per-entry failures are
407
+ * swallowed and excluded from `removedIds`, but the scan continues.
408
+ */
409
+ export function pruneOldShadows(cwd, maxAgeDays = 7, maxSizeMB = 100) {
410
+ const root = resolve(cwd, '.pugi', 'checkpoints');
411
+ if (!existsSync(root)) {
412
+ return { scanned: 0, removed: 0, removedIds: [], totalBytesFreed: 0 };
413
+ }
414
+ const rawEntries = readdirSync(root, { withFileTypes: true });
415
+ const entries = rawEntries
416
+ .filter((d) => d.isDirectory())
417
+ .map((d) => {
418
+ const name = d.name;
419
+ const p = join(root, name);
420
+ const s = safeStat(p);
421
+ const size = computeDirSize(p);
422
+ return {
423
+ id: name,
424
+ path: p,
425
+ mtimeMs: s ? Number(s.mtimeMs) : 0,
426
+ sizeBytes: size,
427
+ };
428
+ });
429
+ // Triple-review P1-2 — explicit mtime ascending sort so the
430
+ // oldest entries land at index 0 and both passes (age + size) prune
431
+ // the right ones. The sort key is `statSync(p).mtimeMs` (via
432
+ // `safeStat` for the directory itself); stat-failed entries get
433
+ // mtime=0 and naturally sort first (preferred — broken checkpoints
434
+ // should be cleaned up before well-formed ones). Tie-breaker by id
435
+ // makes the sweep order deterministic for tests. We re-sort
436
+ // defensively before the size pass to keep the invariant if a
437
+ // future refactor reorders the array between passes.
438
+ const mtimeSort = (a, b) => a.mtimeMs - b.mtimeMs || a.id.localeCompare(b.id);
439
+ entries.sort(mtimeSort);
440
+ const ageCutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
441
+ const removed = [];
442
+ let freed = 0;
443
+ const remove = (idx) => {
444
+ const entry = entries[idx];
445
+ if (!entry)
446
+ return;
447
+ try {
448
+ rmSync(entry.path, { recursive: true, force: true });
449
+ removed.push(entry.id);
450
+ freed += entry.sizeBytes;
451
+ entries.splice(idx, 1);
452
+ }
453
+ catch {
454
+ // Skip — operator can clean up by hand.
455
+ }
456
+ };
457
+ // Pass 1 — age. Walk forwards-with-splice so indices stay valid.
458
+ let i = 0;
459
+ while (i < entries.length) {
460
+ const entry = entries[i];
461
+ if (entry && entry.mtimeMs > 0 && entry.mtimeMs < ageCutoffMs) {
462
+ remove(i);
463
+ }
464
+ else {
465
+ i += 1;
466
+ }
467
+ }
468
+ // Pass 2 — size cap. Defensive re-sort: pass 1 used splice к remove
469
+ // entries which preserves relative order, but a future refactor
470
+ // (parallel-scan, async stat) might reorder. Re-sorting before the
471
+ // size pass guarantees `entries[0]` remains the oldest survivor.
472
+ entries.sort(mtimeSort);
473
+ const maxBytes = maxSizeMB * 1024 * 1024;
474
+ let total = entries.reduce((sum, e) => sum + e.sizeBytes, 0);
475
+ while (total > maxBytes && entries.length > 0) {
476
+ const before = entries[0]?.sizeBytes ?? 0;
477
+ remove(0);
478
+ total -= before;
479
+ }
480
+ return {
481
+ scanned: removed.length + entries.length,
482
+ removed: removed.length,
483
+ removedIds: removed,
484
+ totalBytesFreed: freed,
485
+ };
486
+ }
487
+ /**
488
+ * Compute on-disk usage for a directory tree. Pure utility — exposed
489
+ * so `pruneOldShadows` can be tested deterministically and so future
490
+ * `pugi doctor` surfaces can read the same number.
491
+ */
492
+ export function computeDirSize(dir) {
493
+ if (!existsSync(dir))
494
+ return 0;
495
+ let total = 0;
496
+ const stack = [dir];
497
+ while (stack.length > 0) {
498
+ const next = stack.pop();
499
+ if (!next)
500
+ break;
501
+ let kids = [];
502
+ try {
503
+ kids = readdirSync(next, { withFileTypes: true });
504
+ }
505
+ catch {
506
+ continue;
507
+ }
508
+ for (const k of kids) {
509
+ const name = k.name;
510
+ const p = join(next, name);
511
+ if (k.isDirectory()) {
512
+ stack.push(p);
513
+ continue;
514
+ }
515
+ const s = safeStat(p);
516
+ if (s)
517
+ total += Number(s.size);
518
+ }
519
+ }
520
+ return total;
521
+ }
522
+ /**
523
+ * Sanitise an operator-supplied taskId so it cannot escape the
524
+ * checkpoints directory tree. Reject anything containing path
525
+ * separators or `..` segments; fall back to `DEFAULT_TASK_ID`.
526
+ */
527
+ export function sanitizeTaskId(taskId) {
528
+ const trimmed = (taskId ?? '').trim();
529
+ if (trimmed.length === 0)
530
+ return DEFAULT_TASK_ID;
531
+ if (trimmed.includes('/') || trimmed.includes('\\') || trimmed.includes('..')) {
532
+ return DEFAULT_TASK_ID;
533
+ }
534
+ // Whitelist: alphanumerics, dot, dash, underscore.
535
+ if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) {
536
+ return DEFAULT_TASK_ID;
537
+ }
538
+ return trimmed;
539
+ }
540
+ /**
541
+ * Compose the canonical commit message for a dispatcher hook step.
542
+ * Exposed as a helper so the dispatcher and tests agree on the
543
+ * exact format the slash list renderer can parse back.
544
+ *
545
+ * Format: `pugi <task-id> step <n>: <tool-name> <relative-path>`
546
+ *
547
+ * `relativePath` is normalised against `cwd` so absolute paths from
548
+ * the applicator turn into workspace-relative entries.
549
+ */
550
+ export function formatCheckpointMessage(input) {
551
+ const safeTask = sanitizeTaskId(input.taskId);
552
+ const rel = relative(input.cwd, input.absPath) || input.absPath;
553
+ // Sanitise control chars (tabs / newlines) so the `git log` parser
554
+ // can rely on tab-delimited output (see `listCheckpoints`).
555
+ const safeTool = input.toolName.replace(/[\s\t\r\n]+/g, '_');
556
+ const safeRel = rel.replace(/[\t\r\n]+/g, ' ');
557
+ return `pugi ${safeTask} step ${input.step}: ${safeTool} ${safeRel}`;
558
+ }
559
+ /* ------------------------------ internals ------------------------------ */
560
+ /**
561
+ * Per-call env for shadow git invocations. Pins author + committer +
562
+ * dates к deterministic values so the shadow log is reproducible and
563
+ * never leaks the operator's host identity. We do NOT inherit the
564
+ * user's `process.env` GIT_* overrides; the spawn wrapper merges this
565
+ * onto the parent env explicitly.
566
+ *
567
+ * Triple-review P1-1 — `/dev/null` is the Unix bit-bucket; Windows
568
+ * uses `NUL`. Passing `/dev/null` к `GIT_CONFIG_*` on Windows makes
569
+ * git try to open a literal file named `/dev/null`, which either
570
+ * silently fails or, worse, on a misconfigured CI box succeeds and
571
+ * gets created as a file under the shadow root. The fix routes by
572
+ * `process.platform`.
573
+ *
574
+ * Triple-review P1-4 — pin `GIT_AUTHOR_DATE` + `GIT_COMMITTER_DATE`
575
+ * to the supplied timestamp (ISO 8601) so `git log` output is
576
+ * reproducible across hosts. When no timestamp is supplied the caller
577
+ * is expected to be `initShadowRepo` whose commit is bootstrap-only;
578
+ * we still emit a stable ISO date so test corpora hash-stable.
579
+ */
580
+ function shadowEnv(now = Date.now()) {
581
+ const nullDevice = process.platform === 'win32' ? 'NUL' : '/dev/null';
582
+ const isoDate = new Date(now).toISOString();
583
+ return {
584
+ ...process.env,
585
+ GIT_AUTHOR_NAME: SHADOW_AUTHOR_NAME,
586
+ GIT_AUTHOR_EMAIL: SHADOW_AUTHOR_EMAIL,
587
+ GIT_COMMITTER_NAME: SHADOW_AUTHOR_NAME,
588
+ GIT_COMMITTER_EMAIL: SHADOW_AUTHOR_EMAIL,
589
+ // Pin commit dates for reproducible shadow logs. ISO 8601 is the
590
+ // format git natively accepts; the GIT_*_DATE pair propagates к
591
+ // both `git commit -m` and `git --git-dir … commit` calls.
592
+ GIT_AUTHOR_DATE: isoDate,
593
+ GIT_COMMITTER_DATE: isoDate,
594
+ // Disable any global hook config so the shadow can't fire the
595
+ // operator's commit-msg / pre-commit hooks from the user's repo.
596
+ GIT_CONFIG_GLOBAL: nullDevice,
597
+ GIT_CONFIG_SYSTEM: nullDevice,
598
+ };
599
+ }
600
+ function spawnGit(args, opts) {
601
+ const res = spawnSync('git', args, {
602
+ cwd: opts.cwd,
603
+ env: opts.env ?? process.env,
604
+ encoding: 'utf8',
605
+ });
606
+ return {
607
+ status: res.status,
608
+ stdout: typeof res.stdout === 'string' ? res.stdout : String(res.stdout ?? ''),
609
+ stderr: typeof res.stderr === 'string' ? res.stderr : String(res.stderr ?? ''),
610
+ error: res.error,
611
+ };
612
+ }
613
+ function describeSpawn(res) {
614
+ const stderr = res.stderr.trim();
615
+ const stdout = res.stdout.trim();
616
+ if (res.error)
617
+ return `${res.error.message}${stderr ? ` | ${stderr}` : ''}`;
618
+ const status = res.status ?? 'n/a';
619
+ return [
620
+ `exit=${status}`,
621
+ stderr ? `stderr=${stderr}` : '',
622
+ stdout && !stderr ? `stdout=${stdout}` : '',
623
+ ]
624
+ .filter((s) => s.length > 0)
625
+ .join(' ');
626
+ }
627
+ function safeStat(p) {
628
+ try {
629
+ return statSync(p);
630
+ }
631
+ catch {
632
+ return null;
633
+ }
634
+ }
635
+ /**
636
+ * Default readline-based prompt with a hard timeout. Resolves к the
637
+ * empty string on timeout, which the caller interprets as "operator
638
+ * declined". We deliberately do NOT use `process.stdin.once('data')`
639
+ * directly because that breaks Ctrl-C: readline gives us the standard
640
+ * signal handlers for free.
641
+ *
642
+ * Lifted out as a module-level helper so the spec can inject a stub
643
+ * via `RestoreCheckpointOptions.prompt` without touching real stdin.
644
+ */
645
+ function defaultPrompt(question, timeoutMs) {
646
+ return new Promise((resolveOuter) => {
647
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
648
+ let settled = false;
649
+ const finish = (value) => {
650
+ if (settled)
651
+ return;
652
+ settled = true;
653
+ try {
654
+ rl.close();
655
+ }
656
+ catch {
657
+ /* ignore */
658
+ }
659
+ resolveOuter(value);
660
+ };
661
+ const timer = setTimeout(() => finish(''), Math.max(100, timeoutMs));
662
+ if (typeof timer.unref === 'function')
663
+ timer.unref();
664
+ rl.question(question, (answer) => {
665
+ clearTimeout(timer);
666
+ finish(answer);
667
+ });
668
+ });
669
+ }
670
+ //# sourceMappingURL=shadow-git.js.map