@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.88

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 (405) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  4. package/assets/pugi-prozr2-mascot.ansi +9 -0
  5. package/bin/run.js +33 -1
  6. package/dist/commands/deploy.js +40 -40
  7. package/dist/commands/flatten.js +191 -0
  8. package/dist/commands/jobs-watch.js +201 -0
  9. package/dist/commands/jobs.js +42 -27
  10. package/dist/commands/smoke.js +133 -0
  11. package/dist/core/agent-progress/cleanup.js +134 -0
  12. package/dist/core/agent-progress/schema.js +144 -0
  13. package/dist/core/agent-progress/writer.js +101 -0
  14. package/dist/core/agents/adaptive-router.js +330 -0
  15. package/dist/core/agents/query-decomposer.js +297 -0
  16. package/dist/core/agents/registry.js +3 -3
  17. package/dist/core/approvals/shortcut-resolver.js +98 -0
  18. package/dist/core/artifact-chain/dispatcher.js +148 -0
  19. package/dist/core/artifact-chain/exporter.js +164 -0
  20. package/dist/core/artifact-chain/state.js +243 -0
  21. package/dist/core/artifact-chain/steps.js +169 -0
  22. package/dist/core/ask-user/question.js +92 -0
  23. package/dist/core/audit/audit-trail.js +275 -0
  24. package/dist/core/auth/ensure-authenticated.js +129 -0
  25. package/dist/core/auth/env-provider.js +238 -0
  26. package/dist/core/auto-open-browser.js +4 -4
  27. package/dist/core/auto-update/channels.js +122 -0
  28. package/dist/core/auto-update/checker.js +241 -0
  29. package/dist/core/auto-update/state.js +235 -0
  30. package/dist/core/bare-mode/index.js +107 -0
  31. package/dist/core/bash/redirect.js +281 -0
  32. package/dist/core/bash-classifier.js +436 -40
  33. package/dist/core/checkpoint/resumer.js +149 -0
  34. package/dist/core/checkpoint/rewinder.js +291 -0
  35. package/dist/core/checkpoints/shadow-git.js +670 -0
  36. package/dist/core/citations/parser.js +109 -0
  37. package/dist/core/classifier/yolo-classifier.js +88 -0
  38. package/dist/core/codegraph/decision-store.js +248 -0
  39. package/dist/core/codegraph/detect-repo.js +459 -0
  40. package/dist/core/codegraph/install.js +134 -0
  41. package/dist/core/codegraph/offer-hook.js +220 -0
  42. package/dist/core/compact/auto-trigger.js +96 -0
  43. package/dist/core/compact/buffer-rewriter.js +115 -0
  44. package/dist/core/compact/summarizer.js +208 -0
  45. package/dist/core/compact/token-counter.js +108 -0
  46. package/dist/core/consensus/anvil-fanout.js +25 -25
  47. package/dist/core/consensus/diff-capture.js +121 -12
  48. package/dist/core/consensus/rubric.js +21 -21
  49. package/dist/core/context/builder.js +6 -6
  50. package/dist/core/context/compaction-events.js +8 -8
  51. package/dist/core/context/compaction.js +31 -31
  52. package/dist/core/context/index.js +15 -8
  53. package/dist/core/context/invariants.js +51 -51
  54. package/dist/core/context/markdown-loader.js +28 -10
  55. package/dist/core/context/markdown-traverse.js +255 -0
  56. package/dist/core/context/pugiignore.js +41 -41
  57. package/dist/core/context/repo-skeleton.js +37 -37
  58. package/dist/core/context/tool-eviction.js +55 -0
  59. package/dist/core/context/watcher.js +32 -32
  60. package/dist/core/context/working-set.js +23 -23
  61. package/dist/core/coordinator/agent-tools.js +77 -0
  62. package/dist/core/coordinator/agent-toolset.js +65 -0
  63. package/dist/core/coordinator/fsm.js +73 -0
  64. package/dist/core/coordinator/mode-fsm.js +70 -0
  65. package/dist/core/cost/rate-card.js +129 -0
  66. package/dist/core/cost/tracker.js +221 -0
  67. package/dist/core/credentials.js +12 -12
  68. package/dist/core/cron/scheduler.js +138 -0
  69. package/dist/core/denial-tracking/index.js +8 -0
  70. package/dist/core/denial-tracking/state.js +264 -0
  71. package/dist/core/diagnostics/probe-runner.js +93 -0
  72. package/dist/core/diagnostics/probes/api.js +46 -0
  73. package/dist/core/diagnostics/probes/auth.js +93 -0
  74. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  75. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  76. package/dist/core/diagnostics/probes/config.js +72 -0
  77. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  78. package/dist/core/diagnostics/probes/disk.js +81 -0
  79. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  80. package/dist/core/diagnostics/probes/git.js +65 -0
  81. package/dist/core/diagnostics/probes/hooks.js +118 -0
  82. package/dist/core/diagnostics/probes/mcp.js +75 -0
  83. package/dist/core/diagnostics/probes/node.js +59 -0
  84. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  85. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  86. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  87. package/dist/core/diagnostics/probes/session.js +74 -0
  88. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  89. package/dist/core/diagnostics/probes/workspace.js +63 -0
  90. package/dist/core/diagnostics/types.js +70 -0
  91. package/dist/core/dispatch/cache-cleanup.js +197 -0
  92. package/dist/core/dispatch/cache-handoff.js +295 -0
  93. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  94. package/dist/core/edits/dispatch.js +293 -7
  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 +3 -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 +322 -0
  108. package/dist/core/engine/anvil-client.js +151 -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 +134 -16
  117. package/dist/core/engine/strip-internal-fields.js +124 -0
  118. package/dist/core/engine/tool-bridge.js +1295 -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 +44 -0
  129. package/dist/core/hooks/index.js +15 -0
  130. package/dist/core/hooks/registry.js +213 -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/image/renderer.js +71 -0
  141. package/dist/core/init/detector.js +582 -0
  142. package/dist/core/init/template-renderer.js +242 -0
  143. package/dist/core/jobs/registry.js +18 -18
  144. package/dist/core/ledger/results-tsv.js +142 -0
  145. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  146. package/dist/core/lsp/cache.js +105 -0
  147. package/dist/core/lsp/client.js +776 -0
  148. package/dist/core/lsp/language-detect.js +66 -0
  149. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  150. package/dist/core/lsp/symbol-tools.js +372 -0
  151. package/dist/core/mcp/client.js +97 -28
  152. package/dist/core/mcp/http-server.js +553 -0
  153. package/dist/core/mcp/orchestrator-tools.js +662 -0
  154. package/dist/core/mcp/permission.js +190 -0
  155. package/dist/core/mcp/registry.js +39 -17
  156. package/dist/core/mcp/server-tools.js +219 -0
  157. package/dist/core/mcp/server.js +397 -0
  158. package/dist/core/mcp/trust.js +10 -10
  159. package/dist/core/memory/dual-write.js +416 -0
  160. package/dist/core/memory/passive-extract.js +130 -0
  161. package/dist/core/memory/phase1-kinds.js +20 -0
  162. package/dist/core/memory/secret-scanner.js +304 -0
  163. package/dist/core/memory-sync/queue.js +170 -0
  164. package/dist/core/metrics/extract.js +113 -0
  165. package/dist/core/modes/roo-modes.js +68 -0
  166. package/dist/core/onboarding/ensure-initialized.js +133 -0
  167. package/dist/core/onboarding/marker.js +111 -0
  168. package/dist/core/onboarding/telemetry-state.js +108 -0
  169. package/dist/core/output-style/presets.js +176 -0
  170. package/dist/core/output-style/state.js +185 -0
  171. package/dist/core/path-security.js +287 -5
  172. package/dist/core/permission.js +82 -22
  173. package/dist/core/permissions/auto-classifier.js +124 -0
  174. package/dist/core/permissions/bash-parser.js +371 -0
  175. package/dist/core/permissions/circuit-breaker.js +83 -0
  176. package/dist/core/permissions/constrained-edit.js +91 -0
  177. package/dist/core/permissions/gate.js +278 -0
  178. package/dist/core/permissions/index.js +20 -0
  179. package/dist/core/permissions/mode.js +174 -0
  180. package/dist/core/permissions/network-egress.js +137 -0
  181. package/dist/core/permissions/state.js +241 -0
  182. package/dist/core/permissions/tool-class.js +93 -0
  183. package/dist/core/plan-mode/ui-state.js +51 -0
  184. package/dist/core/plans/plan-artifact.js +721 -0
  185. package/dist/core/policy-limits/etag-store.js +122 -0
  186. package/dist/core/prd-check/parser.js +215 -0
  187. package/dist/core/prd-check/reporter.js +127 -0
  188. package/dist/core/prd-check/session-review.js +557 -0
  189. package/dist/core/prd-check/verifiers.js +223 -0
  190. package/dist/core/prompt-cache/client-cache.js +99 -0
  191. package/dist/core/prompts/assembly.js +29 -0
  192. package/dist/core/prompts/registry.js +364 -0
  193. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  194. package/dist/core/pugi-md/context-injector.js +76 -0
  195. package/dist/core/pugi-md/walk-up.js +207 -0
  196. package/dist/core/python/uv-installer.js +270 -0
  197. package/dist/core/python/uv-resolver.js +83 -0
  198. package/dist/core/rate-limit/narrator.js +146 -0
  199. package/dist/core/recipes/cli-types.js +20 -0
  200. package/dist/core/recipes/loader.js +103 -0
  201. package/dist/core/recipes/runner.js +345 -0
  202. package/dist/core/recipes/schema.js +587 -0
  203. package/dist/core/release-notes/parser.js +241 -0
  204. package/dist/core/release-notes/state.js +116 -0
  205. package/dist/core/repl/ask.js +37 -37
  206. package/dist/core/repl/cancellation.js +26 -26
  207. package/dist/core/repl/cap-warning.js +4 -4
  208. package/dist/core/repl/clipboard-read.js +11 -11
  209. package/dist/core/repl/dispatch-fsm.js +12 -12
  210. package/dist/core/repl/history-search.js +15 -15
  211. package/dist/core/repl/history.js +28 -18
  212. package/dist/core/repl/kill-ring.js +5 -5
  213. package/dist/core/repl/model-pricing.js +135 -0
  214. package/dist/core/repl/privacy-banner.js +22 -22
  215. package/dist/core/repl/session.js +2157 -214
  216. package/dist/core/repl/slash-commands.js +533 -40
  217. package/dist/core/repl/store/index.js +1 -1
  218. package/dist/core/repl/store/jsonl-log.js +22 -22
  219. package/dist/core/repl/store/lockfile.js +10 -10
  220. package/dist/core/repl/store/session-store.js +136 -107
  221. package/dist/core/repl/store/types.js +15 -15
  222. package/dist/core/repl/store/uuid-v7.js +12 -12
  223. package/dist/core/repl/workspace-context.js +43 -21
  224. package/dist/core/repo-map/build.js +125 -0
  225. package/dist/core/repo-map/cache.js +185 -0
  226. package/dist/core/repo-map/extractor.js +254 -0
  227. package/dist/core/repo-map/formatter.js +145 -0
  228. package/dist/core/repo-map/page-rank.js +105 -0
  229. package/dist/core/repo-map/scanner.js +211 -0
  230. package/dist/core/retry-budget/budget.js +284 -0
  231. package/dist/core/retry-budget/index.js +5 -0
  232. package/dist/core/retry-budget/retry-cap.js +74 -0
  233. package/dist/core/routing/lead-worker.js +43 -0
  234. package/dist/core/routing/pre-flight-estimator.js +108 -0
  235. package/dist/core/runs/run-tree.js +103 -0
  236. package/dist/core/security/injection-scanner.js +367 -0
  237. package/dist/core/security/output-filter.js +418 -0
  238. package/dist/core/session/env-file.js +105 -0
  239. package/dist/core/session/section-budgets.js +140 -0
  240. package/dist/core/session.js +92 -0
  241. package/dist/core/settings.js +298 -5
  242. package/dist/core/share/formatter.js +271 -0
  243. package/dist/core/share/redactor.js +221 -0
  244. package/dist/core/share/uploader.js +267 -0
  245. package/dist/core/skills/defaults.js +457 -0
  246. package/dist/core/skills/loader.js +22 -22
  247. package/dist/core/skills/sources.js +27 -27
  248. package/dist/core/smoke/headless-driver.js +174 -0
  249. package/dist/core/smoke/orchestrator.js +194 -0
  250. package/dist/core/smoke/runner.js +238 -0
  251. package/dist/core/smoke/scenario-parser.js +316 -0
  252. package/dist/core/statusline.js +99 -0
  253. package/dist/core/subagents/dispatcher-real.js +600 -0
  254. package/dist/core/subagents/dispatcher.js +132 -43
  255. package/dist/core/subagents/index.js +19 -6
  256. package/dist/core/subagents/isolation-matrix.js +213 -0
  257. package/dist/core/subagents/spawn.js +19 -4
  258. package/dist/core/telemetry/emitter.js +229 -0
  259. package/dist/core/telemetry/queue.js +251 -0
  260. package/dist/core/theme/context.js +91 -0
  261. package/dist/core/theme/presets.js +228 -0
  262. package/dist/core/theme/state.js +181 -0
  263. package/dist/core/todos/invariant.js +10 -0
  264. package/dist/core/todos/state.js +177 -0
  265. package/dist/core/tool-schema/compressor.js +89 -0
  266. package/dist/core/transport/version-interceptor.js +166 -0
  267. package/dist/core/trust.js +2 -2
  268. package/dist/core/tui/thinking-block.js +64 -0
  269. package/dist/core/vim/keymap.js +288 -0
  270. package/dist/core/vim/state.js +92 -0
  271. package/dist/core/watch-markers/marker-watcher.js +133 -0
  272. package/dist/core/worktree-manager/cleanup.js +123 -0
  273. package/dist/core/worktree-manager/manager.js +303 -0
  274. package/dist/index.js +36 -0
  275. package/dist/runtime/bootstrap.js +190 -0
  276. package/dist/runtime/cli.js +4203 -493
  277. package/dist/runtime/commands/agents.js +30 -30
  278. package/dist/runtime/commands/budget.js +5 -5
  279. package/dist/runtime/commands/cancel.js +231 -0
  280. package/dist/runtime/commands/chain.js +489 -0
  281. package/dist/runtime/commands/codegraph-status.js +227 -0
  282. package/dist/runtime/commands/compact.js +297 -0
  283. package/dist/runtime/commands/config.js +73 -39
  284. package/dist/runtime/commands/cost.js +199 -0
  285. package/dist/runtime/commands/delegate.js +244 -13
  286. package/dist/runtime/commands/dispatch.js +126 -0
  287. package/dist/runtime/commands/doctor.js +579 -0
  288. package/dist/runtime/commands/feedback.js +184 -0
  289. package/dist/runtime/commands/hooks.js +184 -0
  290. package/dist/runtime/commands/init.js +254 -0
  291. package/dist/runtime/commands/lsp.js +368 -0
  292. package/dist/runtime/commands/mcp.js +879 -0
  293. package/dist/runtime/commands/memory.js +582 -0
  294. package/dist/runtime/commands/model.js +237 -0
  295. package/dist/runtime/commands/onboarding.js +275 -0
  296. package/dist/runtime/commands/patch.js +128 -0
  297. package/dist/runtime/commands/permissions.js +112 -0
  298. package/dist/runtime/commands/plan.js +143 -0
  299. package/dist/runtime/commands/prd-check.js +285 -0
  300. package/dist/runtime/commands/privacy.js +17 -17
  301. package/dist/runtime/commands/recipe.js +325 -0
  302. package/dist/runtime/commands/redo-blob-store.js +92 -0
  303. package/dist/runtime/commands/redo.js +361 -0
  304. package/dist/runtime/commands/release-notes.js +229 -0
  305. package/dist/runtime/commands/repo-map.js +95 -0
  306. package/dist/runtime/commands/report.js +299 -0
  307. package/dist/runtime/commands/resume.js +118 -0
  308. package/dist/runtime/commands/review-consensus.js +68 -53
  309. package/dist/runtime/commands/rewind.js +333 -0
  310. package/dist/runtime/commands/roster.js +14 -14
  311. package/dist/runtime/commands/sessions.js +163 -0
  312. package/dist/runtime/commands/share.js +316 -0
  313. package/dist/runtime/commands/skills.js +31 -31
  314. package/dist/runtime/commands/status.js +186 -0
  315. package/dist/runtime/commands/stickers.js +82 -0
  316. package/dist/runtime/commands/style.js +194 -0
  317. package/dist/runtime/commands/theme.js +196 -0
  318. package/dist/runtime/commands/undo.js +54 -22
  319. package/dist/runtime/commands/update.js +289 -0
  320. package/dist/runtime/commands/vim.js +140 -0
  321. package/dist/runtime/commands/worktree.js +177 -0
  322. package/dist/runtime/commands/worktrees.js +155 -0
  323. package/dist/runtime/headless-repl.js +195 -0
  324. package/dist/runtime/headless.js +543 -0
  325. package/dist/runtime/load-hooks-or-exit.js +71 -0
  326. package/dist/runtime/plan-decompose.js +531 -0
  327. package/dist/runtime/sigint-guard.js +272 -0
  328. package/dist/runtime/update-check.js +28 -28
  329. package/dist/runtime/version.js +65 -0
  330. package/dist/skills/bundled/batch.js +617 -0
  331. package/dist/skills/bundled/index.js +45 -0
  332. package/dist/skills/bundled/loop.js +358 -0
  333. package/dist/skills/bundled/remember.js +383 -0
  334. package/dist/skills/bundled/simplify.js +289 -0
  335. package/dist/skills/bundled/skillify.js +373 -0
  336. package/dist/skills/bundled/stuck.js +558 -0
  337. package/dist/skills/bundled/verify.js +439 -0
  338. package/dist/testing/vcr.js +486 -0
  339. package/dist/tools/agent-tool.js +229 -0
  340. package/dist/tools/apply-patch.js +556 -0
  341. package/dist/tools/ask-user-question.js +288 -0
  342. package/dist/tools/ask-user.js +115 -0
  343. package/dist/tools/bash.js +624 -46
  344. package/dist/tools/brief.js +224 -0
  345. package/dist/tools/enter-worktree.js +250 -0
  346. package/dist/tools/exit-worktree.js +147 -0
  347. package/dist/tools/file-tools.js +161 -44
  348. package/dist/tools/lsp-tools.js +189 -0
  349. package/dist/tools/mcp-tool.js +260 -0
  350. package/dist/tools/multi-edit.js +361 -0
  351. package/dist/tools/powershell.js +268 -0
  352. package/dist/tools/registry.js +85 -0
  353. package/dist/tools/skill-tool.js +96 -0
  354. package/dist/tools/sleep.js +99 -0
  355. package/dist/tools/synthetic-output.js +133 -0
  356. package/dist/tools/tasks.js +208 -0
  357. package/dist/tools/todo-write.js +184 -0
  358. package/dist/tools/verify-plan-execution.js +295 -0
  359. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  360. package/dist/tools/web-fetch.js +195 -10
  361. package/dist/tools/web-search.js +458 -0
  362. package/dist/tui/agent-progress-card.js +111 -0
  363. package/dist/tui/agent-tree.js +11 -1
  364. package/dist/tui/ask-modal.js +14 -14
  365. package/dist/tui/ask-user-question-chips.js +257 -0
  366. package/dist/tui/ask-user-question-prompt.js +203 -0
  367. package/dist/tui/compact-banner.js +81 -0
  368. package/dist/tui/conversation-pane.js +85 -11
  369. package/dist/tui/cost-table.js +111 -0
  370. package/dist/tui/device-flow.js +2 -2
  371. package/dist/tui/doctor-table.js +46 -0
  372. package/dist/tui/feedback-prompt.js +156 -0
  373. package/dist/tui/input-box.js +247 -32
  374. package/dist/tui/login-picker.js +3 -3
  375. package/dist/tui/markdown-render.js +6 -6
  376. package/dist/tui/onboarding-wizard.js +240 -0
  377. package/dist/tui/permissions-picker.js +86 -0
  378. package/dist/tui/render.js +35 -0
  379. package/dist/tui/repl-render.js +332 -54
  380. package/dist/tui/repl-splash-art.js +16 -16
  381. package/dist/tui/repl-splash-mascot.js +48 -24
  382. package/dist/tui/repl-splash.js +22 -22
  383. package/dist/tui/repl.js +124 -44
  384. package/dist/tui/slash-palette.js +6 -6
  385. package/dist/tui/splash.js +2 -2
  386. package/dist/tui/status-bar.js +109 -31
  387. package/dist/tui/status-table.js +7 -0
  388. package/dist/tui/stickers-art.js +136 -0
  389. package/dist/tui/style-table.js +28 -0
  390. package/dist/tui/theme-table.js +29 -0
  391. package/dist/tui/thinking-spinner.js +123 -0
  392. package/dist/tui/tool-stream-pane.js +53 -4
  393. package/dist/tui/update-banner.js +27 -2
  394. package/dist/tui/vim-input.js +267 -0
  395. package/dist/tui/welcome-banner.js +107 -0
  396. package/dist/tui/welcome-data.js +293 -0
  397. package/dist/tui/workspace-context.js +2 -2
  398. package/docs/examples/codegraph.mcp.json +10 -0
  399. package/package.json +25 -7
  400. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  401. package/test/scenarios/compact-force.scenario.txt +11 -0
  402. package/test/scenarios/identity.scenario.txt +11 -0
  403. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  404. package/test/scenarios/walkback.scenario.txt +12 -0
  405. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,201 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `pugi jobs --watch` Ink TUI — live progress .
4
+ *
5
+ * Tails `~/.pugi/agent-progress/*.json` via chokidar and re-renders a
6
+ * grid of <AgentProgressCard> components. Mirrors the the upstream tool
7
+ * `/compact` layout pattern — one card per agent, sorted by status
8
+ * (running → completed → failed) then by lastUpdate descending so the
9
+ * freshest activity floats к the top.
10
+ *
11
+ * Lifecycle:
12
+ * 1. Resolve dir via resolveProgressDir() (env-aware).
13
+ * 2. Initial scan with readdirSync — populate the map.
14
+ * 3. chokidar.watch(dir, { ignoreInitial: true }) for add/change/unlink.
15
+ * 4. setInterval(1000) tick re-renders to keep elapsed labels live
16
+ * WITHOUT re-reading files.
17
+ * 5. SIGINT (Ctrl+C) triggers `app.unmount()` + process.exit(0).
18
+ *
19
+ * Layout: single column under 100 cols, two columns at 100+ cols
20
+ * (Ink's flexbox computes the responsive split automatically — we just
21
+ * set `flexDirection="row" flexWrap="wrap"` and bound each card к 50%
22
+ * width when stdout is wide enough).
23
+ *
24
+ * The TUI is opt-in (operator runs `pugi jobs --watch`); the bare
25
+ * `pugi jobs` continues to print the legacy job-registry table.
26
+ */
27
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import chokidar from 'chokidar';
30
+ import { useEffect, useMemo, useState } from 'react';
31
+ import { Box, Text, render, useApp, useInput, useStdout } from 'ink';
32
+ import { resolveProgressDir } from '../core/agent-progress/writer.js';
33
+ import { runCleanup } from '../core/agent-progress/cleanup.js';
34
+ import { validateAgentProgress, } from '../core/agent-progress/schema.js';
35
+ import { AgentProgressCard } from '../tui/agent-progress-card.js';
36
+ /** Cleanup pass cadence inside the watcher. Cheap (readdir on a small
37
+ * dir); 60s is the operator-tested sweet spot — fast enough to feel
38
+ * live, slow enough к never burn CPU. */
39
+ const CLEANUP_INTERVAL_MS = 60_000;
40
+ const STATUS_RANK = {
41
+ running: 0,
42
+ completed: 1,
43
+ failed: 2,
44
+ };
45
+ /**
46
+ * Read + validate one progress file. Returns undefined on any failure
47
+ * — the watcher prefers к skip a malformed entry rather than crash.
48
+ * Exported for the spec.
49
+ */
50
+ export function readProgressFile(path) {
51
+ if (!existsSync(path))
52
+ return undefined;
53
+ try {
54
+ const body = readFileSync(path, 'utf8');
55
+ const parsed = JSON.parse(body);
56
+ const result = validateAgentProgress(parsed);
57
+ return result.ok ? result.value : undefined;
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ /**
64
+ * Sort progress entries for stable, operator-friendly display:
65
+ * 1. Running first.
66
+ * 2. Then by lastUpdate (most recent first).
67
+ * 3. Tiebreak on agentId for snapshot stability.
68
+ */
69
+ export function sortProgressEntries(entries) {
70
+ return [...entries].sort((a, b) => {
71
+ const rank = STATUS_RANK[a.status] - STATUS_RANK[b.status];
72
+ if (rank !== 0)
73
+ return rank;
74
+ const tsDiff = Date.parse(b.lastUpdate) - Date.parse(a.lastUpdate);
75
+ if (tsDiff !== 0)
76
+ return tsDiff;
77
+ return a.agentId.localeCompare(b.agentId);
78
+ });
79
+ }
80
+ export function JobsWatch(props) {
81
+ const { dir, nowEpochMs, staticMode = false } = props;
82
+ const resolvedDir = useMemo(() => resolveProgressDir(dir), [dir]);
83
+ const [agents, setAgents] = useState(() => initialScan(resolvedDir));
84
+ const [tick, setTick] = useState(0);
85
+ const { exit } = useApp();
86
+ const { stdout } = useStdout();
87
+ const cols = stdout?.columns ?? 80;
88
+ const twoColumns = cols >= 100;
89
+ useInput((_input, key) => {
90
+ if (key.escape || key.ctrl) {
91
+ // Ink's useInput passes ctrl=true for Ctrl+<x>; we exit on Ctrl+C
92
+ // (key.ctrl alone is too broad — actual key char arrives in `input`).
93
+ // Practically, raw mode passes Ctrl+C as SIGINT to the process so
94
+ // this is a belt-and-suspenders catch.
95
+ if (key.escape)
96
+ exit();
97
+ }
98
+ });
99
+ useEffect(() => {
100
+ if (staticMode)
101
+ return;
102
+ let watcher;
103
+ try {
104
+ watcher = chokidar.watch(resolvedDir, {
105
+ ignoreInitial: true,
106
+ depth: 0,
107
+ // The agent writer uses atomic rename — chokidar reports `add`
108
+ // when the final filename appears, never the *.tmp-* intermediate.
109
+ ignored: /\.tmp-/,
110
+ });
111
+ const onChange = (path) => {
112
+ if (!/\.json$/.test(path))
113
+ return;
114
+ const progress = readProgressFile(path);
115
+ if (!progress)
116
+ return;
117
+ setAgents((prev) => ({ ...prev, [progress.agentId]: progress }));
118
+ };
119
+ const onRemove = (path) => {
120
+ const match = /([^/\\]+)\.json$/.exec(path);
121
+ if (!match)
122
+ return;
123
+ const id = match[1];
124
+ if (!id)
125
+ return;
126
+ setAgents((prev) => {
127
+ if (!(id in prev))
128
+ return prev;
129
+ const next = { ...prev };
130
+ delete next[id];
131
+ return next;
132
+ });
133
+ };
134
+ watcher.on('add', onChange);
135
+ watcher.on('change', onChange);
136
+ watcher.on('unlink', onRemove);
137
+ }
138
+ catch {
139
+ // Watcher failures degrade gracefully — initial scan still rendered.
140
+ }
141
+ const interval = setInterval(() => setTick((t) => t + 1), 1000);
142
+ // Auto-cleanup completed/failed entries older than the TTL. The
143
+ // sweep moves them к `<dir>/archive/` so operators can still
144
+ // forensic; the watcher's chokidar `unlink` handler drops them
145
+ // from the live grid as the rename fires.
146
+ const cleanupInterval = setInterval(() => {
147
+ try {
148
+ runCleanup({ dir: resolvedDir });
149
+ }
150
+ catch {
151
+ // best-effort housekeeping; never crash the TUI
152
+ }
153
+ }, CLEANUP_INTERVAL_MS);
154
+ return () => {
155
+ clearInterval(interval);
156
+ clearInterval(cleanupInterval);
157
+ if (watcher) {
158
+ void watcher.close();
159
+ }
160
+ };
161
+ }, [resolvedDir, staticMode]);
162
+ const sorted = useMemo(() => sortProgressEntries(Object.values(agents)), [agents]);
163
+ const effectiveNow = nowEpochMs ?? Date.now() + tick * 0; // tick triggers re-render only
164
+ if (sorted.length === 0) {
165
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { dimColor: true, children: ["No agents broadcasting progress yet. Watching", ' ', _jsx(Text, { bold: true, children: resolvedDir }), " for `<id>.json` files\u2026"] }), _jsx(Text, { dimColor: true, children: "(Press Esc / Ctrl+C \u043A exit.)" })] }));
166
+ }
167
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "pugi jobs --watch" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", sorted.length, " ", sorted.length === 1 ? 'agent' : 'agents', " \u00B7", ' ', resolvedDir] })] }), _jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: sorted.map((progress) => (_jsx(Box, { width: twoColumns ? '50%' : '100%', flexDirection: "column", children: _jsx(AgentProgressCard, { progress: progress, nowEpochMs: effectiveNow }) }, progress.agentId))) }), _jsx(Text, { dimColor: true, children: "(Esc / Ctrl+C \u043A exit.)" })] }));
168
+ }
169
+ function initialScan(dir) {
170
+ const map = {};
171
+ if (!existsSync(dir))
172
+ return map;
173
+ let entries;
174
+ try {
175
+ entries = readdirSync(dir);
176
+ }
177
+ catch {
178
+ return map;
179
+ }
180
+ for (const name of entries) {
181
+ if (!name.endsWith('.json'))
182
+ continue;
183
+ if (/\.tmp-/.test(name))
184
+ continue;
185
+ const progress = readProgressFile(join(dir, name));
186
+ if (progress)
187
+ map[progress.agentId] = progress;
188
+ }
189
+ return map;
190
+ }
191
+ /**
192
+ * Entry-point for `pugi jobs --watch`. Mounts the Ink app and returns
193
+ * a Promise that resolves when the user exits (Ctrl+C / Esc).
194
+ */
195
+ export async function runJobsWatch(options = {}) {
196
+ return new Promise((resolve) => {
197
+ const app = render(_jsx(JobsWatch, { dir: options.dir }), { exitOnCtrlC: true });
198
+ void app.waitUntilExit().then(() => resolve(0));
199
+ });
200
+ }
201
+ //# sourceMappingURL=jobs-watch.js.map
@@ -1,21 +1,22 @@
1
1
  /**
2
- * `pugi jobs` command surface — Sprint α5.9 (ADR-0056 PR-PUGI-CLI-M1-GAP-J).
2
+ * `pugi jobs` command surface — Sprint .
3
3
  *
4
4
  * Subcommands:
5
- * pugi jobs list — table of all tracked jobs (or JSON envelope)
6
- * pugi jobs status <id> — full record + tail of overflow artifact
7
- * pugi jobs tail <id> — stream the overflow artifact
8
- * pugi jobs kill <id> — SIGTERM then SIGKILL after a grace
9
- * pugi jobs kill --all — kill every running job in this session
5
+ * pugi jobs list — table of all tracked jobs (or JSON envelope)
6
+ * pugi jobs status <id> — full record + tail of overflow artifact
7
+ * pugi jobs tail <id> — stream the overflow artifact
8
+ * pugi jobs kill <id> — SIGTERM then SIGKILL after a grace
9
+ * pugi jobs kill --all — kill every running job in this session
10
10
  *
11
11
  * Power-word voice rules (per Pugi brand guide):
12
- * - status names render as "on watch" (running), "shipped" (finished),
13
- * "stood down" (killed), "blocked" (failed), "lost" (abandoned).
14
- * - JSON envelopes keep the machine-friendly enum so consumers do
15
- * not have to map back.
12
+ * - status names render as "on watch" (running), "shipped" (finished),
13
+ * "stood down" (killed), "blocked" (failed), "lost" (abandoned).
14
+ * - JSON envelopes keep the machine-friendly enum so consumers do
15
+ * not have to map back.
16
16
  */
17
17
  import { existsSync, readFileSync } from 'node:fs';
18
18
  import { formatDuration, getJobRegistry, relativeAge, } from '../core/jobs/registry.js';
19
+ import { runJobsWatch } from './jobs-watch.js';
19
20
  const HUMAN_STATUS = {
20
21
  running: 'on watch',
21
22
  finished: 'shipped',
@@ -24,6 +25,19 @@ const HUMAN_STATUS = {
24
25
  abandoned: 'lost',
25
26
  };
26
27
  export async function runJobsCommand(args, flags, io, sessionId) {
28
+ // live progress : `pugi jobs --watch` mounts the live
29
+ // agent-progress TUI. The flag may sit anywhere in the arg list
30
+ // (`pugi jobs --watch` or `pugi jobs watch`) — both forms route к
31
+ // the watcher. JSON mode is incompatible (it would never return); we
32
+ // bail with a helpful error instead of silently swallowing the flag.
33
+ const wantsWatch = args.includes('--watch') || args[0] === 'watch';
34
+ if (wantsWatch) {
35
+ if (flags.json) {
36
+ io.writeError('pugi jobs --watch is interactive; --json is not supported.');
37
+ return 2;
38
+ }
39
+ return runJobsWatch();
40
+ }
27
41
  const sub = args[0] ?? 'list';
28
42
  switch (sub) {
29
43
  case 'list':
@@ -43,11 +57,12 @@ export async function runJobsCommand(args, flags, io, sessionId) {
43
57
  function usage() {
44
58
  return [
45
59
  'Usage:',
46
- ' pugi jobs list [--json] Table of background jobs.',
47
- ' pugi jobs status <id> [--json] Full record + tail of artifact.',
48
- ' pugi jobs tail <id> Stream the captured artifact.',
49
- ' pugi jobs kill <id> [--json] SIGTERM, escalate to SIGKILL.',
50
- ' pugi jobs kill --all [--json] Stand down every running job.',
60
+ ' pugi jobs list [--json] Table of background jobs.',
61
+ ' pugi jobs status <id> [--json] Full record + tail of artifact.',
62
+ ' pugi jobs tail <id> Stream the captured artifact.',
63
+ ' pugi jobs kill <id> [--json] SIGTERM, escalate to SIGKILL.',
64
+ ' pugi jobs kill --all [--json] Stand down every running job.',
65
+ ' pugi jobs --watch Live Ink TUI of agent progress.',
51
66
  ].join('\n');
52
67
  }
53
68
  async function runList(flags, io) {
@@ -91,20 +106,20 @@ async function runStatus(id, flags, io) {
91
106
  }
92
107
  const lines = [
93
108
  `Job ${entry.id}`,
94
- ` PID: ${entry.pid}`,
95
- ` Command: ${entry.command}`,
96
- ` Class: ${entry.bashClass}`,
97
- ` Status: ${HUMAN_STATUS[entry.status]} (${entry.status})`,
98
- ` CWD: ${entry.cwd}`,
99
- ` Started: ${entry.startedAt} (${relativeAge(entry.startedAt)} ago)`,
100
- ` Duration: ${formatDuration(entry.startedAt, entry.finishedAt)}`,
109
+ ` PID: ${entry.pid}`,
110
+ ` Command: ${entry.command}`,
111
+ ` Class: ${entry.bashClass}`,
112
+ ` Status: ${HUMAN_STATUS[entry.status]} (${entry.status})`,
113
+ ` CWD: ${entry.cwd}`,
114
+ ` Started: ${entry.startedAt} (${relativeAge(entry.startedAt)} ago)`,
115
+ ` Duration: ${formatDuration(entry.startedAt, entry.finishedAt)}`,
101
116
  ];
102
117
  if (entry.finishedAt)
103
- lines.push(` Finished: ${entry.finishedAt}`);
118
+ lines.push(` Finished: ${entry.finishedAt}`);
104
119
  if (entry.exitCode !== undefined)
105
- lines.push(` Exit code: ${entry.exitCode}`);
120
+ lines.push(` Exit code: ${entry.exitCode}`);
106
121
  if (entry.outputArtifactRef)
107
- lines.push(` Artifact: ${entry.outputArtifactRef}`);
122
+ lines.push(` Artifact: ${entry.outputArtifactRef}`);
108
123
  if (tail) {
109
124
  lines.push('', '--- output tail ---', tail);
110
125
  }
@@ -159,7 +174,7 @@ async function runKill(args, flags, io, sessionId) {
159
174
  }
160
175
  else {
161
176
  for (const result of results) {
162
- io.write(` ${result.id} ${result.killed ? 'stood down' : 'noop'} (${result.method})\n`);
177
+ io.write(` ${result.id} ${result.killed ? 'stood down' : 'noop'} (${result.method})\n`);
163
178
  }
164
179
  }
165
180
  return 0;
@@ -233,7 +248,7 @@ function renderTable(entries) {
233
248
  return rows
234
249
  .map((row) => row
235
250
  .map((cell, i) => cell.padEnd(widths[i] ?? cell.length))
236
- .join(' ')
251
+ .join(' ')
237
252
  .trimEnd())
238
253
  .join('\n');
239
254
  }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `pugi smoke` — runs the bundled scenario corpus through the headless
3
+ * harness and reports pass/fail per scenario (
4
+ *).
5
+ *
6
+ * The CLI surface lives here rather than in `runtime/cli.ts` so the
7
+ * dispatch surface stays focused on argv routing. This module owns:
8
+ *
9
+ * - Resolving the scenarios directory (default
10
+ * `<cli-root>/test/scenarios/` when bundled, configurable via
11
+ * `--scenarios-dir`).
12
+ * - Selecting the `pugi` binary the headless driver should spawn.
13
+ * Local development: `node <cli-root>/bin/run.js`. CI / published
14
+ * usage: the `pugi` on PATH.
15
+ * - Forwarding the orchestrator output to the unified
16
+ * `writeOutput` writer so `--json` mode works without a second
17
+ * code path.
18
+ *
19
+ * Phase 1 deliberately ships ONE flag (`--filter`) and one option
20
+ * (`--scenarios-dir`); the rest of the surface comes in Phase 2 once
21
+ * the engine path is wired and we know which scenarios actually need
22
+ * per-run plumbing (timeouts, fixture credentials, hermetic stubs).
23
+ */
24
+ import { existsSync } from 'node:fs';
25
+ import { dirname, resolve } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+ import { runSmoke, renderReportText, } from '../core/smoke/orchestrator.js';
28
+ import { runHeadlessScenario } from '../core/smoke/headless-driver.js';
29
+ /**
30
+ * Entry point invoked by `runtime/cli.ts::dispatchSmoke`. Returns the
31
+ * desired process exit code so the dispatcher can set
32
+ * `process.exitCode` without a second round-trip.
33
+ */
34
+ export async function runSmokeCommand(ctx) {
35
+ // Parse the (small) command-local argv. We only honor flags this
36
+ // command actually consumes; unknown args produce a usage error so
37
+ // typos surface immediately.
38
+ let scenariosDirOverride = ctx.scenariosDir ?? undefined;
39
+ let filter = ctx.filter ?? '';
40
+ for (let i = 0; i < ctx.args.length; i += 1) {
41
+ const arg = ctx.args[i] ?? '';
42
+ if (arg === '--filter') {
43
+ const next = ctx.args[i + 1];
44
+ if (!next || next.startsWith('--')) {
45
+ ctx.writeOutput({ ok: false, error: '--filter requires a pattern' }, 'pugi smoke: --filter requires a pattern (substring or *-glob)');
46
+ return 2;
47
+ }
48
+ filter = next;
49
+ i += 1;
50
+ }
51
+ else if (arg.startsWith('--filter=')) {
52
+ filter = arg.slice('--filter='.length);
53
+ }
54
+ else if (arg === '--scenarios-dir') {
55
+ const next = ctx.args[i + 1];
56
+ if (!next || next.startsWith('--')) {
57
+ ctx.writeOutput({ ok: false, error: '--scenarios-dir requires a path' }, 'pugi smoke: --scenarios-dir requires a path');
58
+ return 2;
59
+ }
60
+ scenariosDirOverride = next;
61
+ i += 1;
62
+ }
63
+ else if (arg.startsWith('--scenarios-dir=')) {
64
+ scenariosDirOverride = arg.slice('--scenarios-dir='.length);
65
+ }
66
+ else if (arg === '--help' || arg === '-h') {
67
+ ctx.writeOutput({
68
+ ok: true,
69
+ usage: 'pugi smoke [--filter <pattern>] [--scenarios-dir <path>]',
70
+ }, [
71
+ 'pugi smoke — run the bundled scenario corpus headlessly.',
72
+ '',
73
+ 'Flags:',
74
+ ' --filter <pattern> Run a subset (substring or *-glob match on scenario id).',
75
+ ' --scenarios-dir <path> Override the scenarios directory (default: bundled corpus).',
76
+ ].join('\n'));
77
+ return 0;
78
+ }
79
+ else {
80
+ ctx.writeOutput({ ok: false, error: `unknown arg: ${arg}` }, `pugi smoke: unknown arg ${arg}`);
81
+ return 2;
82
+ }
83
+ }
84
+ const scenariosDir = scenariosDirOverride ?? resolveBundledScenariosDir();
85
+ if (!existsSync(scenariosDir)) {
86
+ ctx.writeOutput({ ok: false, error: `scenarios dir not found: ${scenariosDir}` }, `pugi smoke: scenarios dir not found: ${scenariosDir}`);
87
+ return 2;
88
+ }
89
+ const pugiBin = ctx.pugiBin ?? process.env.PUGI_SMOKE_BIN ?? 'pugi';
90
+ const log = ctx.log ?? ((line) => process.stderr.write(`${line}\n`));
91
+ const report = await runSmoke({
92
+ scenariosDir,
93
+ filter,
94
+ executor: (scenario) => runHeadlessScenario(scenario, { pugiBin }),
95
+ log,
96
+ });
97
+ if (ctx.json) {
98
+ ctx.writeOutput(report, renderReportText(report));
99
+ }
100
+ else {
101
+ process.stdout.write(`${renderReportText(report)}\n`);
102
+ }
103
+ return report.exitCode;
104
+ }
105
+ /**
106
+ * Resolve the scenarios directory that ships alongside the CLI.
107
+ *
108
+ * Both the dev source layout (`<cli-root>/src/commands/smoke.ts`) and
109
+ * the built output layout (`<cli-root>/dist/commands/smoke.js`) land
110
+ * on the same `../../test/scenarios` path relative to this file.
111
+ * The bundled `npm i -g @pugi/cli` install replicates that structure
112
+ * by shipping `test/scenarios` (glob `**\/*.scenario.txt`) via the
113
+ * `package.json` `files` field — so the single resolved path works in
114
+ * dev, in `tsx` runs, and in published installs without a config knob.
115
+ *
116
+ * If a future restructure ever bundles scenarios at
117
+ * `<cli-root>/dist/scenarios/` instead, add that to the fallback chain
118
+ * here AND mirror the path in `package.json` `files`. Until then we
119
+ * keep the resolver intentionally one-line.
120
+ */
121
+ export function resolveBundledScenariosDir() {
122
+ const here = dirname(fileURLToPath(import.meta.url));
123
+ // Works for both src/commands/smoke.ts and dist/commands/smoke.js —
124
+ // both live two directories below the cli root.
125
+ const bundled = resolve(here, '..', '..', 'test', 'scenarios');
126
+ if (existsSync(bundled))
127
+ return bundled;
128
+ // Last resort: return the expected path so the orchestrator surfaces
129
+ // a clean "scenarios dir not found" diagnostic at the call site
130
+ // rather than the resolver swallowing it silently.
131
+ return bundled;
132
+ }
133
+ //# sourceMappingURL=smoke.js.map
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Agent-progress auto-cleanup — live progress .
3
+ *
4
+ * Rule: a progress file whose status === 'completed' OR 'failed' AND
5
+ * whose `lastUpdate` is older than COMPLETION_TTL_MS (default 5 min)
6
+ * gets MOVED into `<dir>/archive/<id>-<ts>.json` rather than deleted.
7
+ * Operators sometimes want to inspect a finished agent's last state;
8
+ * the archive keeps that affordance while preventing the live watcher
9
+ * from cluttering up with stale rows.
10
+ *
11
+ * The function is pure-ish:
12
+ * - All clock reads go through the injected `now` (defaults Date.now).
13
+ * - Returns a structured report so the caller (the watcher or a CLI
14
+ * cron) can log what got swept.
15
+ * - File-system errors degrade silently — the cleanup is best-effort
16
+ * housekeeping, never a hot path.
17
+ *
18
+ * Hook-in point: `pugi jobs --watch` calls `runCleanup()` once per
19
+ * 60-second tick (cheap — readdir on a small directory). A separate
20
+ * cron entry can call it standalone via the (future) `pugi jobs
21
+ * gc` subcommand if the operator never runs --watch.
22
+ */
23
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import { validateAgentProgress, } from './schema.js';
26
+ import { resolveProgressDir } from './writer.js';
27
+ /** Default time a completed/failed entry sits before getting swept. */
28
+ export const COMPLETION_TTL_MS = 5 * 60 * 1000;
29
+ /** Archive subdirectory under the resolved progress dir. */
30
+ export const ARCHIVE_SUBDIR = 'archive';
31
+ /**
32
+ * Run a single cleanup pass. Returns the report for telemetry / tests.
33
+ */
34
+ export function runCleanup(options = {}) {
35
+ const dir = resolveProgressDir(options.dir);
36
+ const ttl = options.ttlMs ?? COMPLETION_TTL_MS;
37
+ const now = options.now ?? Date.now;
38
+ const report = { dir, archived: [], skipped: [] };
39
+ if (!existsSync(dir))
40
+ return report;
41
+ const archiveDir = join(dir, ARCHIVE_SUBDIR);
42
+ let entries;
43
+ try {
44
+ entries = readdirSync(dir);
45
+ }
46
+ catch (err) {
47
+ report.skipped.push({ path: dir, reason: `readdir failed: ${err.message}` });
48
+ return report;
49
+ }
50
+ for (const name of entries) {
51
+ if (!name.endsWith('.json'))
52
+ continue;
53
+ if (/\.tmp-/.test(name))
54
+ continue;
55
+ const path = join(dir, name);
56
+ let body;
57
+ try {
58
+ body = readFileSync(path, 'utf8');
59
+ }
60
+ catch {
61
+ report.skipped.push({ path, reason: 'read failed' });
62
+ continue;
63
+ }
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(body);
67
+ }
68
+ catch {
69
+ report.skipped.push({ path, reason: 'malformed JSON' });
70
+ continue;
71
+ }
72
+ const validation = validateAgentProgress(parsed);
73
+ if (!validation.ok) {
74
+ report.skipped.push({ path, reason: `invalid: ${validation.error}` });
75
+ continue;
76
+ }
77
+ const progress = validation.value;
78
+ if (!isExpired(progress, now(), ttl)) {
79
+ continue;
80
+ }
81
+ if (!existsSync(archiveDir)) {
82
+ try {
83
+ mkdirSync(archiveDir, { recursive: true });
84
+ }
85
+ catch (err) {
86
+ report.skipped.push({
87
+ path,
88
+ reason: `archive mkdir failed: ${err.message}`,
89
+ });
90
+ continue;
91
+ }
92
+ }
93
+ const target = join(archiveDir, `${progress.agentId}-${safeStamp(progress.lastUpdate)}.json`);
94
+ try {
95
+ renameSync(path, target);
96
+ report.archived.push({ agentId: progress.agentId, from: path, to: target });
97
+ }
98
+ catch (err) {
99
+ report.skipped.push({ path, reason: `rename failed: ${err.message}` });
100
+ }
101
+ }
102
+ return report;
103
+ }
104
+ /**
105
+ * Decide whether a single progress doc has aged out. Exported for the
106
+ * spec — kept pure so tests can probe edges (running entries never
107
+ * expire, completed entries before TTL stay, etc).
108
+ */
109
+ export function isExpired(progress, nowEpochMs, ttlMs = COMPLETION_TTL_MS) {
110
+ if (progress.status === 'running')
111
+ return false;
112
+ const lastTs = Date.parse(progress.lastUpdate);
113
+ if (Number.isNaN(lastTs))
114
+ return false;
115
+ return nowEpochMs - lastTs >= ttlMs;
116
+ }
117
+ // Claude review followup: `pruneArchive` was removed.
118
+ // Rationale (P1 in the Claude review batch on PR):
119
+ // - hardcoded `/tmp` path was POSIX-only (Windows would break),
120
+ // - `renameSync` across volumes throws EXDEV,
121
+ // - collision risk between concurrent hosts/sessions sharing `/tmp`,
122
+ // - and crucially, the function had ZERO call-sites in the codebase.
123
+ // Deleting it is strictly safer than leaving a broken-on-Windows
124
+ // dead helper that future refactors might accidentally wire up.
125
+ // If/when an archive GC is needed, the right shape is:
126
+ // - `os.tmpdir()` (not '/tmp'),
127
+ // - `rmSync(path, {force: true})` for cross-volume safe delete,
128
+ // - exposed as `pugi jobs gc` so we have an explicit caller.
129
+ function safeStamp(iso) {
130
+ // Build a filename-safe slug — strip colons/dots which are friendly
131
+ // в ISO timestamps but hostile к some filesystems.
132
+ return iso.replace(/[:.]/g, '-');
133
+ }
134
+ //# sourceMappingURL=cleanup.js.map