@pugi/cli 0.1.0-beta.7 → 0.1.0-beta.87

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 (402) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  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 +2 -2
  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 +12 -12
  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 +293 -7
  94. package/dist/core/edits/format-matrix.js +26 -0
  95. package/dist/core/edits/fuzzy-ladder.js +650 -0
  96. package/dist/core/edits/index.js +3 -1
  97. package/dist/core/edits/journal.js +199 -0
  98. package/dist/core/edits/layer-a-apply.js +15 -15
  99. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  100. package/dist/core/edits/layer-b-apply.js +9 -9
  101. package/dist/core/edits/layer-c-apply.js +6 -6
  102. package/dist/core/edits/layer-d-ast.js +557 -14
  103. package/dist/core/edits/marker-parser.js +12 -12
  104. package/dist/core/edits/security-gate.js +27 -27
  105. package/dist/core/edits/verify-hook.js +273 -0
  106. package/dist/core/edits/worktree.js +322 -0
  107. package/dist/core/engine/anvil-client.js +140 -26
  108. package/dist/core/engine/auto-compact.js +179 -0
  109. package/dist/core/engine/budgets.js +186 -0
  110. package/dist/core/engine/context-prefix.js +155 -0
  111. package/dist/core/engine/index.js +1 -1
  112. package/dist/core/engine/intensity.js +158 -0
  113. package/dist/core/engine/intent.js +260 -0
  114. package/dist/core/engine/native-pugi.js +1295 -227
  115. package/dist/core/engine/prompts.js +134 -16
  116. package/dist/core/engine/strip-internal-fields.js +124 -0
  117. package/dist/core/engine/tool-bridge.js +1295 -59
  118. package/dist/core/evaluation/golden-dataset.js +293 -0
  119. package/dist/core/feedback/queue.js +177 -0
  120. package/dist/core/feedback/submitter.js +145 -0
  121. package/dist/core/file-cache.js +113 -1
  122. package/dist/core/flatten/flatten-repo.js +439 -0
  123. package/dist/core/format/osc8-link.js +28 -0
  124. package/dist/core/hook-chains.js +392 -0
  125. package/dist/core/hooks/citation-verify-hook.js +138 -0
  126. package/dist/core/hooks/citation-verify.js +112 -0
  127. package/dist/core/hooks/events.js +44 -0
  128. package/dist/core/hooks/index.js +15 -0
  129. package/dist/core/hooks/registry.js +213 -0
  130. package/dist/core/hooks/runner.js +236 -0
  131. package/dist/core/hooks/v2/event-emitter.js +115 -0
  132. package/dist/core/hooks/v2/executor.js +282 -0
  133. package/dist/core/hooks/v2/index.js +25 -0
  134. package/dist/core/hooks/v2/lifecycle.js +104 -0
  135. package/dist/core/hooks/v2/loader.js +216 -0
  136. package/dist/core/hooks/v2/matcher.js +125 -0
  137. package/dist/core/hooks/v2/trust.js +143 -0
  138. package/dist/core/hooks/v2/types.js +86 -0
  139. package/dist/core/image/renderer.js +71 -0
  140. package/dist/core/init/detector.js +582 -0
  141. package/dist/core/init/template-renderer.js +242 -0
  142. package/dist/core/jobs/registry.js +18 -18
  143. package/dist/core/ledger/results-tsv.js +142 -0
  144. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  145. package/dist/core/lsp/cache.js +105 -0
  146. package/dist/core/lsp/client.js +776 -0
  147. package/dist/core/lsp/language-detect.js +66 -0
  148. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  149. package/dist/core/lsp/symbol-tools.js +372 -0
  150. package/dist/core/mcp/client.js +97 -28
  151. package/dist/core/mcp/http-server.js +553 -0
  152. package/dist/core/mcp/orchestrator-tools.js +662 -0
  153. package/dist/core/mcp/permission.js +190 -0
  154. package/dist/core/mcp/registry.js +39 -17
  155. package/dist/core/mcp/server-tools.js +219 -0
  156. package/dist/core/mcp/server.js +397 -0
  157. package/dist/core/mcp/trust.js +10 -10
  158. package/dist/core/memory/dual-write.js +416 -0
  159. package/dist/core/memory/passive-extract.js +130 -0
  160. package/dist/core/memory/phase1-kinds.js +20 -0
  161. package/dist/core/memory/secret-scanner.js +304 -0
  162. package/dist/core/memory-sync/queue.js +170 -0
  163. package/dist/core/metrics/extract.js +113 -0
  164. package/dist/core/modes/roo-modes.js +68 -0
  165. package/dist/core/onboarding/ensure-initialized.js +133 -0
  166. package/dist/core/onboarding/marker.js +111 -0
  167. package/dist/core/onboarding/telemetry-state.js +108 -0
  168. package/dist/core/output-style/presets.js +176 -0
  169. package/dist/core/output-style/state.js +185 -0
  170. package/dist/core/path-security.js +287 -5
  171. package/dist/core/permission.js +82 -22
  172. package/dist/core/permissions/auto-classifier.js +124 -0
  173. package/dist/core/permissions/bash-parser.js +371 -0
  174. package/dist/core/permissions/circuit-breaker.js +83 -0
  175. package/dist/core/permissions/constrained-edit.js +91 -0
  176. package/dist/core/permissions/gate.js +278 -0
  177. package/dist/core/permissions/index.js +20 -0
  178. package/dist/core/permissions/mode.js +174 -0
  179. package/dist/core/permissions/network-egress.js +137 -0
  180. package/dist/core/permissions/state.js +241 -0
  181. package/dist/core/permissions/tool-class.js +93 -0
  182. package/dist/core/plan-mode/ui-state.js +51 -0
  183. package/dist/core/plans/plan-artifact.js +721 -0
  184. package/dist/core/policy-limits/etag-store.js +122 -0
  185. package/dist/core/prd-check/parser.js +215 -0
  186. package/dist/core/prd-check/reporter.js +127 -0
  187. package/dist/core/prd-check/session-review.js +557 -0
  188. package/dist/core/prd-check/verifiers.js +223 -0
  189. package/dist/core/prompt-cache/client-cache.js +99 -0
  190. package/dist/core/prompts/assembly.js +29 -0
  191. package/dist/core/prompts/registry.js +364 -0
  192. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  193. package/dist/core/pugi-md/context-injector.js +76 -0
  194. package/dist/core/pugi-md/walk-up.js +207 -0
  195. package/dist/core/python/uv-installer.js +270 -0
  196. package/dist/core/python/uv-resolver.js +83 -0
  197. package/dist/core/rate-limit/narrator.js +146 -0
  198. package/dist/core/recipes/cli-types.js +20 -0
  199. package/dist/core/recipes/loader.js +103 -0
  200. package/dist/core/recipes/runner.js +345 -0
  201. package/dist/core/recipes/schema.js +587 -0
  202. package/dist/core/release-notes/parser.js +241 -0
  203. package/dist/core/release-notes/state.js +116 -0
  204. package/dist/core/repl/ask.js +37 -37
  205. package/dist/core/repl/cancellation.js +26 -26
  206. package/dist/core/repl/cap-warning.js +4 -4
  207. package/dist/core/repl/clipboard-read.js +11 -11
  208. package/dist/core/repl/dispatch-fsm.js +12 -12
  209. package/dist/core/repl/history-search.js +15 -15
  210. package/dist/core/repl/history.js +28 -18
  211. package/dist/core/repl/kill-ring.js +5 -5
  212. package/dist/core/repl/model-pricing.js +135 -0
  213. package/dist/core/repl/privacy-banner.js +22 -22
  214. package/dist/core/repl/session.js +2157 -214
  215. package/dist/core/repl/slash-commands.js +533 -40
  216. package/dist/core/repl/store/index.js +1 -1
  217. package/dist/core/repl/store/jsonl-log.js +22 -22
  218. package/dist/core/repl/store/lockfile.js +10 -10
  219. package/dist/core/repl/store/session-store.js +136 -107
  220. package/dist/core/repl/store/types.js +15 -15
  221. package/dist/core/repl/store/uuid-v7.js +12 -12
  222. package/dist/core/repl/workspace-context.js +43 -21
  223. package/dist/core/repo-map/build.js +125 -0
  224. package/dist/core/repo-map/cache.js +185 -0
  225. package/dist/core/repo-map/extractor.js +254 -0
  226. package/dist/core/repo-map/formatter.js +145 -0
  227. package/dist/core/repo-map/page-rank.js +105 -0
  228. package/dist/core/repo-map/scanner.js +211 -0
  229. package/dist/core/retry-budget/budget.js +284 -0
  230. package/dist/core/retry-budget/index.js +5 -0
  231. package/dist/core/retry-budget/retry-cap.js +74 -0
  232. package/dist/core/routing/lead-worker.js +43 -0
  233. package/dist/core/routing/pre-flight-estimator.js +108 -0
  234. package/dist/core/runs/run-tree.js +103 -0
  235. package/dist/core/security/injection-scanner.js +367 -0
  236. package/dist/core/security/output-filter.js +418 -0
  237. package/dist/core/session/env-file.js +105 -0
  238. package/dist/core/session/section-budgets.js +140 -0
  239. package/dist/core/session.js +92 -0
  240. package/dist/core/settings.js +286 -5
  241. package/dist/core/share/formatter.js +271 -0
  242. package/dist/core/share/redactor.js +221 -0
  243. package/dist/core/share/uploader.js +267 -0
  244. package/dist/core/skills/defaults.js +457 -0
  245. package/dist/core/skills/loader.js +22 -22
  246. package/dist/core/skills/sources.js +27 -27
  247. package/dist/core/smoke/headless-driver.js +174 -0
  248. package/dist/core/smoke/orchestrator.js +194 -0
  249. package/dist/core/smoke/runner.js +238 -0
  250. package/dist/core/smoke/scenario-parser.js +316 -0
  251. package/dist/core/statusline.js +99 -0
  252. package/dist/core/subagents/dispatcher-real.js +600 -0
  253. package/dist/core/subagents/dispatcher.js +132 -43
  254. package/dist/core/subagents/index.js +19 -6
  255. package/dist/core/subagents/isolation-matrix.js +213 -0
  256. package/dist/core/subagents/spawn.js +19 -4
  257. package/dist/core/telemetry/emitter.js +229 -0
  258. package/dist/core/telemetry/queue.js +251 -0
  259. package/dist/core/theme/context.js +91 -0
  260. package/dist/core/theme/presets.js +228 -0
  261. package/dist/core/theme/state.js +181 -0
  262. package/dist/core/todos/invariant.js +10 -0
  263. package/dist/core/todos/state.js +177 -0
  264. package/dist/core/tool-schema/compressor.js +89 -0
  265. package/dist/core/transport/version-interceptor.js +166 -0
  266. package/dist/core/trust.js +2 -2
  267. package/dist/core/tui/thinking-block.js +64 -0
  268. package/dist/core/vim/keymap.js +288 -0
  269. package/dist/core/vim/state.js +92 -0
  270. package/dist/core/watch-markers/marker-watcher.js +133 -0
  271. package/dist/core/worktree-manager/cleanup.js +123 -0
  272. package/dist/core/worktree-manager/manager.js +303 -0
  273. package/dist/index.js +28 -0
  274. package/dist/runtime/bootstrap.js +190 -0
  275. package/dist/runtime/cli.js +4162 -488
  276. package/dist/runtime/commands/agents.js +30 -30
  277. package/dist/runtime/commands/budget.js +5 -5
  278. package/dist/runtime/commands/cancel.js +231 -0
  279. package/dist/runtime/commands/chain.js +489 -0
  280. package/dist/runtime/commands/codegraph-status.js +227 -0
  281. package/dist/runtime/commands/compact.js +297 -0
  282. package/dist/runtime/commands/config.js +32 -32
  283. package/dist/runtime/commands/cost.js +199 -0
  284. package/dist/runtime/commands/delegate.js +244 -13
  285. package/dist/runtime/commands/dispatch.js +126 -0
  286. package/dist/runtime/commands/doctor.js +579 -0
  287. package/dist/runtime/commands/feedback.js +184 -0
  288. package/dist/runtime/commands/hooks.js +184 -0
  289. package/dist/runtime/commands/init.js +254 -0
  290. package/dist/runtime/commands/lsp.js +368 -0
  291. package/dist/runtime/commands/mcp.js +879 -0
  292. package/dist/runtime/commands/memory.js +582 -0
  293. package/dist/runtime/commands/model.js +237 -0
  294. package/dist/runtime/commands/onboarding.js +275 -0
  295. package/dist/runtime/commands/patch.js +128 -0
  296. package/dist/runtime/commands/permissions.js +112 -0
  297. package/dist/runtime/commands/plan.js +143 -0
  298. package/dist/runtime/commands/prd-check.js +285 -0
  299. package/dist/runtime/commands/privacy.js +17 -17
  300. package/dist/runtime/commands/recipe.js +325 -0
  301. package/dist/runtime/commands/redo-blob-store.js +92 -0
  302. package/dist/runtime/commands/redo.js +361 -0
  303. package/dist/runtime/commands/release-notes.js +229 -0
  304. package/dist/runtime/commands/repo-map.js +95 -0
  305. package/dist/runtime/commands/report.js +299 -0
  306. package/dist/runtime/commands/resume.js +118 -0
  307. package/dist/runtime/commands/review-consensus.js +68 -53
  308. package/dist/runtime/commands/rewind.js +333 -0
  309. package/dist/runtime/commands/roster.js +14 -14
  310. package/dist/runtime/commands/sessions.js +163 -0
  311. package/dist/runtime/commands/share.js +316 -0
  312. package/dist/runtime/commands/skills.js +31 -31
  313. package/dist/runtime/commands/status.js +186 -0
  314. package/dist/runtime/commands/stickers.js +82 -0
  315. package/dist/runtime/commands/style.js +194 -0
  316. package/dist/runtime/commands/theme.js +196 -0
  317. package/dist/runtime/commands/undo.js +54 -22
  318. package/dist/runtime/commands/update.js +289 -0
  319. package/dist/runtime/commands/vim.js +140 -0
  320. package/dist/runtime/commands/worktree.js +177 -0
  321. package/dist/runtime/commands/worktrees.js +155 -0
  322. package/dist/runtime/headless-repl.js +195 -0
  323. package/dist/runtime/headless.js +543 -0
  324. package/dist/runtime/load-hooks-or-exit.js +71 -0
  325. package/dist/runtime/plan-decompose.js +531 -0
  326. package/dist/runtime/update-check.js +28 -28
  327. package/dist/runtime/version.js +65 -0
  328. package/dist/skills/bundled/batch.js +617 -0
  329. package/dist/skills/bundled/index.js +45 -0
  330. package/dist/skills/bundled/loop.js +358 -0
  331. package/dist/skills/bundled/remember.js +383 -0
  332. package/dist/skills/bundled/simplify.js +289 -0
  333. package/dist/skills/bundled/skillify.js +373 -0
  334. package/dist/skills/bundled/stuck.js +558 -0
  335. package/dist/skills/bundled/verify.js +439 -0
  336. package/dist/testing/vcr.js +486 -0
  337. package/dist/tools/agent-tool.js +229 -0
  338. package/dist/tools/apply-patch.js +556 -0
  339. package/dist/tools/ask-user-question.js +222 -0
  340. package/dist/tools/ask-user.js +115 -0
  341. package/dist/tools/bash.js +623 -45
  342. package/dist/tools/brief.js +224 -0
  343. package/dist/tools/enter-worktree.js +250 -0
  344. package/dist/tools/exit-worktree.js +147 -0
  345. package/dist/tools/file-tools.js +161 -44
  346. package/dist/tools/lsp-tools.js +189 -0
  347. package/dist/tools/mcp-tool.js +260 -0
  348. package/dist/tools/multi-edit.js +361 -0
  349. package/dist/tools/powershell.js +268 -0
  350. package/dist/tools/registry.js +85 -0
  351. package/dist/tools/skill-tool.js +96 -0
  352. package/dist/tools/sleep.js +99 -0
  353. package/dist/tools/synthetic-output.js +133 -0
  354. package/dist/tools/tasks.js +208 -0
  355. package/dist/tools/todo-write.js +184 -0
  356. package/dist/tools/verify-plan-execution.js +295 -0
  357. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  358. package/dist/tools/web-fetch.js +195 -10
  359. package/dist/tools/web-search.js +458 -0
  360. package/dist/tui/agent-progress-card.js +111 -0
  361. package/dist/tui/agent-tree.js +11 -1
  362. package/dist/tui/ask-modal.js +14 -14
  363. package/dist/tui/ask-user-question-prompt.js +203 -0
  364. package/dist/tui/compact-banner.js +81 -0
  365. package/dist/tui/conversation-pane.js +85 -11
  366. package/dist/tui/cost-table.js +111 -0
  367. package/dist/tui/device-flow.js +2 -2
  368. package/dist/tui/doctor-table.js +46 -0
  369. package/dist/tui/feedback-prompt.js +156 -0
  370. package/dist/tui/input-box.js +247 -32
  371. package/dist/tui/login-picker.js +3 -3
  372. package/dist/tui/markdown-render.js +6 -6
  373. package/dist/tui/onboarding-wizard.js +240 -0
  374. package/dist/tui/permissions-picker.js +86 -0
  375. package/dist/tui/render.js +35 -0
  376. package/dist/tui/repl-render.js +332 -54
  377. package/dist/tui/repl-splash-art.js +16 -16
  378. package/dist/tui/repl-splash-mascot.js +48 -24
  379. package/dist/tui/repl-splash.js +22 -22
  380. package/dist/tui/repl.js +124 -44
  381. package/dist/tui/slash-palette.js +6 -6
  382. package/dist/tui/splash.js +2 -2
  383. package/dist/tui/status-bar.js +109 -31
  384. package/dist/tui/status-table.js +7 -0
  385. package/dist/tui/stickers-art.js +136 -0
  386. package/dist/tui/style-table.js +28 -0
  387. package/dist/tui/theme-table.js +29 -0
  388. package/dist/tui/thinking-spinner.js +123 -0
  389. package/dist/tui/tool-stream-pane.js +53 -4
  390. package/dist/tui/update-banner.js +27 -2
  391. package/dist/tui/vim-input.js +267 -0
  392. package/dist/tui/welcome-banner.js +107 -0
  393. package/dist/tui/welcome-data.js +293 -0
  394. package/dist/tui/workspace-context.js +2 -2
  395. package/docs/examples/codegraph.mcp.json +10 -0
  396. package/package.json +23 -6
  397. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  398. package/test/scenarios/compact-force.scenario.txt +11 -0
  399. package/test/scenarios/identity.scenario.txt +11 -0
  400. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  401. package/test/scenarios/walkback.scenario.txt +12 -0
  402. package/dist/core/engine/compaction-hook.js +0 -154
@@ -1,26 +1,26 @@
1
1
  /**
2
- * Bash command classifier — Sprint α5.2 (ADR-0056 PR-PUGI-CLI-M1-GAP-B).
2
+ * Bash command classifier — Sprint .
3
3
  *
4
4
  * Splits a shell command into a 7-class taxonomy so the permission
5
5
  * engine can apply class-aware policy instead of the prior bool gate
6
6
  * (`destructiveBashPatterns ? deny : ask`).
7
7
  *
8
8
  * Design notes:
9
- * - The classifier is a conservative pattern matcher, not a full
10
- * bash AST parser. M2 will replace it with a real parser (see
11
- * bash-security.md §4). For M1 the rules are explicit-substring +
12
- * simple tokenization, which is good enough to gate every command
13
- * the engine loop currently emits.
14
- * - Compound commands (`a && b`, `a || b`, `a ; b`, `a | b`) are
15
- * split on the four separators and every component is classified
16
- * individually. The overall class is the most dangerous component.
17
- * - The `destructive` patterns originally lived in
18
- * `permission.ts::destructiveBashPatterns`. They are now the
19
- * single source of truth here; `permission.ts` re-exports the
20
- * hard-deny check through `classifyBash`.
21
- * - The `unknown` class fires on parse failure (`eval`, deep
22
- * `$(...)` nesting, `curl | sh` install pipes) so the permission
23
- * engine can fail closed in interactive modes.
9
+ * - The classifier is a conservative pattern matcher, not a full
10
+ * bash AST parser. M2 will replace it with a real parser (see
11
+ * bash-security.md §4). For M1 the rules are explicit-substring +
12
+ * simple tokenization, which is good enough to gate every command
13
+ * the engine loop currently emits.
14
+ * - Compound commands (`a && b`, `a || b`, `a ; b`, `a | b`) are
15
+ * split on the four separators and every component is classified
16
+ * individually. The overall class is the most dangerous component.
17
+ * - The `destructive` patterns originally lived in
18
+ * `permission.ts::destructiveBashPatterns`. They are now the
19
+ * single source of truth here; `permission.ts` re-exports the
20
+ * hard-deny check through `classifyBash`.
21
+ * - The `unknown` class fires on parse failure (`eval`, deep
22
+ * `$(...)` nesting, `curl | sh` install pipes) so the permission
23
+ * engine can fail closed in interactive modes.
24
24
  */
25
25
  /**
26
26
  * Class rank for worst-component reduction in compound commands.
@@ -34,7 +34,7 @@
34
34
  * still letting genuine `write_workspace`, `network`, `write_protected`
35
35
  * and `destructive` components win when they appear.
36
36
  *
37
- * Code Reviewer P0 retro 2026-05-24: previously `unknown: 0` meant
37
+ * Code Reviewer P0 retro: previously `unknown: 0` meant
38
38
  * `read` (rank 1) won over `unknown` (rank 0) in the worst-component
39
39
  * reduction. That bypassed the file-level promise of fail-closed on
40
40
  * parse failure.
@@ -119,6 +119,63 @@ const DESTRUCTIVE_PATTERNS = [
119
119
  // History destruction
120
120
  { pattern: 'history -c' },
121
121
  { pattern: ' >/dev/null 2>&1; rm' },
122
+ // ---------------------------------------------------------------
123
+ // Patterns ported from external utility (Apache-2.0)
124
+ // and `safety-guard.sh` BLOCKED_PATTERNS array. Upstream source:
125
+ // external hooks/destructive-guard.sh (lines 7-13)
126
+ // external hooks/safety-guard.sh (lines 14-50)
127
+ //
128
+ //
129
+ // The patterns below need word-boundary matching because their
130
+ // tokens (kill, halt, reboot, ...) appear as substrings of common
131
+ // unrelated words (skills, default, chrooted-rebooter, etc.).
132
+ // Substring `.includes` cannot express that — `regex` is required.
133
+ // ---------------------------------------------------------------
134
+ // Process termination — `kill`, `pkill`, `killall` at command head
135
+ // or after `sudo`. Matches `kill 1234`, `kill -9 $$`, `sudo killall
136
+ // node`, but NOT `skill issue` (no leading boundary) or
137
+ // `git commit -m "skill kill story"` (the kill is inside a quoted
138
+ // string — quote-aware split handled upstream; here we still need
139
+ // the boundary). Anchored to start-of-component or `sudo ` prefix.
140
+ {
141
+ pattern: 'kill',
142
+ regex: /^(?:sudo\s+)?(?:pkill|killall|kill)\b/,
143
+ },
144
+ // System power state — reboot / shutdown / halt / poweroff / init 0
145
+ // / init 6. External tooling matches these anywhere in the command; we
146
+ // tighten to start-of-component or `sudo ` prefix to avoid FPs on
147
+ // file paths or variable names containing the substring.
148
+ {
149
+ pattern: 'reboot',
150
+ regex: /^(?:sudo\s+)?reboot\b/,
151
+ },
152
+ {
153
+ pattern: 'shutdown',
154
+ regex: /^(?:sudo\s+)?shutdown\b/,
155
+ },
156
+ {
157
+ pattern: 'halt',
158
+ regex: /^(?:sudo\s+)?halt\b/,
159
+ },
160
+ {
161
+ pattern: 'poweroff',
162
+ regex: /^(?:sudo\s+)?poweroff\b/,
163
+ },
164
+ {
165
+ pattern: 'init 0',
166
+ regex: /^(?:sudo\s+)?init\s+0\b/,
167
+ },
168
+ {
169
+ pattern: 'init 6',
170
+ regex: /^(?:sudo\s+)?init\s+6\b/,
171
+ },
172
+ // `git clean -f` (without -dx) — External tooling lists this as destructive
173
+ // because it still deletes untracked files. Pugi previously only
174
+ // gated `git clean -fdx`; broaden to any `-f` variant.
175
+ {
176
+ pattern: 'git clean -f',
177
+ regex: /\bgit\s+clean\s+-[A-Za-z]*f/,
178
+ },
122
179
  ];
123
180
  /**
124
181
  * Compound separators. We split on `&&`, `||`, `;`, `|` to classify
@@ -133,7 +190,7 @@ const COMPOUND_SEPARATORS = /\s*(?:&&|\|\||;|\|)\s*/;
133
190
  * that script bodies passed to `awk`, `sed`, `perl`, `python -c` are
134
191
  * not mis-split when they contain bare `;` or `|` glyphs.
135
192
  *
136
- * Code Reviewer P0 retro 2026-05-24: a naive regex split on
193
+ * Code Reviewer P0 retro: a naive regex split on
137
194
  * `awk 'BEGIN { for (i=0;i<5000;i++) ... }'` produces 3 components
138
195
  * (the awk script header + two for-loop fragments) that get
139
196
  * classified as `unknown` each and — with the unknown:3 rank above
@@ -289,6 +346,93 @@ const BUILD_TEST_PREFIXES = [
289
346
  'tsc -p',
290
347
  'eslint',
291
348
  'prettier --check',
349
+ // P0 fix (#37 CRITICAL): customer-blocking gap surfaced
350
+ // during dogfood. Engine emitted `chmod +x build.sh`, `node script.js`,
351
+ // `python3 -m pytest`, `git status`, `pnpm build`, `docker ps`, etc.
352
+ // and the classifier returned `unknown` → permission matrix denied
353
+ // в bypassPermissions mode (which the customer expected to auto-allow
354
+ // basic dev tools). Customers could not run ANY real build/test/git
355
+ // workflow through Pugi.
356
+ //
357
+ // The prefixes below cover three classes of developer tooling that
358
+ // are always allowed in `auto`/`dontAsk`/`bypassPermissions` modes,
359
+ // `ask` in interactive modes, and `deny` in `plan` (read-only) mode:
360
+ // - Language runtimes: `node`, `python`, `python3`, `ruby`, etc.
361
+ // - Native build chains: `gcc`, `clang`, `cmake`, `rustc`, etc.
362
+ // - Container/k8s read-class: `docker ps/inspect/logs`, `kubectl get`.
363
+ //
364
+ // Destructive variants are already gated upstream by DESTRUCTIVE_PATTERNS
365
+ // (e.g. `docker system prune`, `kubectl delete --all`). The first-token
366
+ // gate in classifyComponent runs THIS list before the unknown fallback.
367
+ //
368
+ // Language runtime invocations (first-token match, with or without args).
369
+ 'node',
370
+ 'python',
371
+ 'python3',
372
+ 'ruby',
373
+ 'perl',
374
+ 'php',
375
+ 'deno',
376
+ 'bun',
377
+ 'tsx',
378
+ 'ts-node',
379
+ // Native build chains.
380
+ 'gcc',
381
+ 'g++',
382
+ 'clang',
383
+ 'clang++',
384
+ 'cmake',
385
+ 'rustc',
386
+ 'javac',
387
+ 'java',
388
+ // Container/k8s read-class (the destructive subcommands are pre-empted
389
+ // by DESTRUCTIVE_PATTERNS: `docker system prune`, `kubectl delete --all`,
390
+ // `kubectl delete namespace`).
391
+ 'docker ps',
392
+ 'docker images',
393
+ 'docker inspect',
394
+ 'docker logs',
395
+ 'docker version',
396
+ 'docker info',
397
+ 'docker exec',
398
+ 'docker run',
399
+ 'docker stop',
400
+ 'docker start',
401
+ 'docker restart',
402
+ 'docker rm',
403
+ 'docker rmi',
404
+ 'docker build',
405
+ 'docker tag',
406
+ 'docker compose',
407
+ 'docker-compose',
408
+ 'kubectl get',
409
+ 'kubectl describe',
410
+ 'kubectl logs',
411
+ 'kubectl exec',
412
+ 'kubectl apply',
413
+ 'kubectl create',
414
+ 'kubectl rollout',
415
+ 'kubectl port-forward',
416
+ 'kubectl config',
417
+ // Git read+write surface (network ops already handled by NETWORK_PREFIXES;
418
+ // destructive ops `reset --hard`/`clean -fdx`/`push --force` blocked above).
419
+ // Note: WRITE_WORKSPACE_PREFIXES already covers `git commit/add/checkout/...`.
420
+ // These entries handle plain `git rev-list`, `git cherry-pick`, `git worktree`,
421
+ // `git submodule`, etc that customer scripts commonly invoke.
422
+ 'git rev-list',
423
+ 'git cherry-pick',
424
+ 'git worktree',
425
+ 'git submodule',
426
+ 'git blame',
427
+ 'git describe',
428
+ 'git tag --list',
429
+ 'git tag -l',
430
+ 'git for-each-ref',
431
+ 'git ls-remote',
432
+ // gh CLI (GitHub). `gh repo delete` / `gh release delete` reach into
433
+ // network operations but are non-destructive for the local workspace.
434
+ // Permission matrix asks before allowing in auto.
435
+ 'gh',
292
436
  ];
293
437
  /** Single-token read-only commands. Argument-free entries match exact. */
294
438
  const READ_TOKENS = new Set([
@@ -327,6 +471,16 @@ const READ_TOKENS = new Set([
327
471
  'cut',
328
472
  'sort',
329
473
  'uniq',
474
+ // P0 fix (#37 CRITICAL): structured-data inspection tools
475
+ // are pure stdin/stdout transformers (no FS write, no network) when
476
+ // не paired с `>` redirection (the redirection branch above promotes
477
+ // к write_workspace independently). Common в dev scripts for parsing
478
+ // package.json, tsconfig.json, Helm values.yaml, etc.
479
+ // `tee` is INTENTIONALLY excluded — it writes by definition, even
480
+ // в protected paths (`tee /etc/...` is already in DESTRUCTIVE_PATTERNS).
481
+ 'jq',
482
+ 'yq',
483
+ 'column',
330
484
  ]);
331
485
  const READ_PREFIXES = [
332
486
  'git status',
@@ -361,13 +515,23 @@ const WRITE_WORKSPACE_PREFIXES = [
361
515
  'git tag',
362
516
  'git rebase',
363
517
  'git merge',
518
+ // P0 fix (#37 CRITICAL): file-permission ops are common
519
+ // в build scripts (`chmod +x build.sh`, `chown $USER file`). The
520
+ // destructive variants (`chmod 777 /`, `chmod -R 777 /`, `chmod -R
521
+ // 777 ~`, `chown -R root /`, `chown -R / ...`) are pre-empted by
522
+ // DESTRUCTIVE_PATTERNS which runs BEFORE this list — safe to add
523
+ // here for the non-destructive path. detectProtectedWrite's `\bchmod\b`
524
+ // / `\bchown\b` regex also catches writes into protected paths
525
+ // regardless of this list.
526
+ 'chmod ',
527
+ 'chown ',
364
528
  ];
365
529
  /**
366
530
  * Protected-write triggers. If a command writes to any of these paths
367
531
  * the class is `write_protected` regardless of the operation type.
368
532
  *
369
533
  * Wildcards are handled as substring matches (e.g. `/.ssh/` matches
370
- * `~/.ssh/foo` and `/Users/x/.ssh/bar`).
534
+ * `~/.ssh/foo` and `[HOME]/USER/.ssh/bar`).
371
535
  */
372
536
  const PROTECTED_PATH_SUBSTRINGS = [
373
537
  '/.ssh/',
@@ -388,6 +552,40 @@ const PROTECTED_PATH_SUBSTRINGS = [
388
552
  '/usr/',
389
553
  '/var/',
390
554
  ];
555
+ /**
556
+ * Protected basename triggers — files whose CONTENT must never leak
557
+ * through the bash surface, even when the literal path is workspace-
558
+ * local. Mirrors `permission.ts::protectedBasenames` and `.env.*`
559
+ * pattern so the read-tool gate (which fires on `read .env`) and the
560
+ * bash gate (which fires on `cat .env`) stay symmetric.
561
+ *
562
+ * P0 fix (Codex audit): before this list existed, the
563
+ * engine model could circumvent the `read` tool's `protectedTargetReason`
564
+ * check by emitting `bash cat .env` — the classifier saw `cat` (read
565
+ * token) + `.env` (not in PROTECTED_PATH_SUBSTRINGS) and returned class
566
+ * `read`, which the permission matrix allows under every mode. The
567
+ * `local-first-invariants` spec proved the leak: `pugi explain .env`
568
+ * surfaced `SECRET=should_never_leak` in the engine summary.
569
+ *
570
+ * Match shape: the substring must touch a `.` boundary (`/.env`,
571
+ * ` .env`, `.env\b`) or appear as the full token so a path like
572
+ * `apps/codeforge/file.env-template` (no real secret) does not
573
+ * over-trigger.
574
+ */
575
+ const PROTECTED_BASENAME_PATTERNS = [
576
+ // `.env`, `.env.production`, `.env.local` — anywhere in the command.
577
+ // Boundary on the left is start/whitespace/quote/`/`, on the right
578
+ // start/whitespace/end/quote/`>`/`|`/`;`.
579
+ /(^|[\s'"\/=])\.env(\.[A-Za-z0-9_-]+)?($|[\s'"<>|;&])/,
580
+ // SSH key basenames (covers both `id_rsa` and `id_ed25519` even
581
+ // outside `~/.ssh/`). The `/.ssh/` substring above gates the
582
+ // directory case; this catches a key file copied to the workspace.
583
+ /(^|[\s'"\/])id_(rsa|ed25519|ecdsa|dsa)(\.pub)?($|[\s'"<>|;&])/,
584
+ // Other credential basenames mirrored from permission.ts.
585
+ /(^|[\s'"\/])\.npmrc($|[\s'"<>|;&])/,
586
+ /(^|[\s'"\/])\.pypirc($|[\s'"<>|;&])/,
587
+ /(^|[\s'"\/])\.gitconfig($|[\s'"<>|;&])/,
588
+ ];
391
589
  /**
392
590
  * Obfuscation triggers — any of these forces the `unknown` class so
393
591
  * the permission engine can fail closed.
@@ -400,16 +598,16 @@ const OBFUSCATION_TRIGGERS = [
400
598
  * Classify a single (non-compound) command component.
401
599
  *
402
600
  * Order of checks (most-specific first):
403
- * 1. destructive substring (hard deny path)
404
- * 2. obfuscation (curl|sh, deep $() nesting, raw eval)
405
- * 3. cd-escape (covered by classifyBash for the overall command;
406
- * single-component cd is handled here too)
407
- * 4. protected-write (redirection or write op into a protected path)
408
- * 5. write_workspace (mkdir/touch/cp/mv/git-write etc)
409
- * 6. network (curl/wget/ssh/installers)
410
- * 7. build_test (pnpm test, cargo build, ...)
411
- * 8. read (pwd, ls, cat, ...)
412
- * 9. unknown (default)
601
+ * 1. destructive substring (hard deny path)
602
+ * 2. obfuscation (curl|sh, deep $() nesting, raw eval)
603
+ * 3. cd-escape (covered by classifyBash for the overall command;
604
+ * single-component cd is handled here too)
605
+ * 4. protected-write (redirection or write op into a protected path)
606
+ * 5. write_workspace (mkdir/touch/cp/mv/git-write etc)
607
+ * 6. network (curl/wget/ssh/installers)
608
+ * 7. build_test (pnpm test, cargo build, ...)
609
+ * 8. read (pwd, ls, cat, ...)
610
+ * 9. unknown (default)
413
611
  */
414
612
  function classifyComponent(cmd, ctx) {
415
613
  const trimmed = cmd.trim();
@@ -459,7 +657,7 @@ function classifyComponent(cmd, ctx) {
459
657
  // `cat ~/.ssh/id_ed25519` still win when matched (they run before
460
658
  // this check).
461
659
  //
462
- // Code Reviewer P0 retro 2026-05-24: previously these reads fell
660
+ // Code Reviewer P0 retro: previously these reads fell
463
661
  // through to READ_TOKENS and were allowed in every mode.
464
662
  const protectedRead = detectProtectedRead(trimmed);
465
663
  if (protectedRead) {
@@ -469,6 +667,26 @@ function classifyComponent(cmd, ctx) {
469
667
  matched: protectedRead.matched,
470
668
  };
471
669
  }
670
+ // 4a-bis. Parent-traversal in read arguments. The file-tools layer
671
+ // refuses `..` segments via `resolveWorkspacePath`, but the bash
672
+ // surface had no equivalent gate — the engine could emit
673
+ // `cat ../README.md` or `ls ..` to enumerate / read outside the
674
+ // workspace, sidestepping the path-security check that the `read`
675
+ // and `glob` tools enforce.
676
+ //
677
+ // P0 fix (Codex audit): treat `..` as a path segment
678
+ // (`../`, ` ..`, `..\n`) in any read-class command as a workspace
679
+ // escape. We classify it as `write_protected` so the auto/dontAsk
680
+ // modes refuse, mirroring the `Path escapes workspace` semantics
681
+ // the file-tools layer already provides.
682
+ const traversal = detectParentTraversalRead(trimmed);
683
+ if (traversal) {
684
+ return {
685
+ class: 'write_protected',
686
+ reason: traversal.reason,
687
+ matched: traversal.matched,
688
+ };
689
+ }
472
690
  // 4b. .env writes are always protected, even inside the workspace
473
691
  // (CEO directive feedback_never_delete_untracked_env.md).
474
692
  const envWrite = detectEnvWrite(trimmed);
@@ -525,6 +743,25 @@ function classifyComponent(cmd, ctx) {
525
743
  if (trimmed === 'make' || trimmed.startsWith('make ')) {
526
744
  return { class: 'build_test', reason: 'make runner', matched: 'make' };
527
745
  }
746
+ // 7c. Operator-override safe tokens (P0 fix #37).
747
+ // `PUGI_CLASSIFIER_EXTRA_SAFE=tool1,tool2,...` extends the BUILD_TEST
748
+ // first-token list at runtime. This is a security-sensitive escape
749
+ // hatch — operators can add their custom build tools without a
750
+ // recompile. Destructive patterns ALREADY ran above (step 1) so this
751
+ // cannot whitelist `rm`, `mkfs`, `git push --force`, etc. The match
752
+ // is strict first-token equality — not substring — and the env var
753
+ // is read fresh on every classify call so tests can mutate it.
754
+ const extraSafe = readExtraSafeTokens();
755
+ if (extraSafe.size > 0) {
756
+ const firstTokenForExtraSafe = trimmed.split(/\s+/)[0] ?? '';
757
+ if (extraSafe.has(firstTokenForExtraSafe)) {
758
+ return {
759
+ class: 'build_test',
760
+ reason: `PUGI_CLASSIFIER_EXTRA_SAFE override: ${firstTokenForExtraSafe}`,
761
+ matched: firstTokenForExtraSafe,
762
+ };
763
+ }
764
+ }
528
765
  // 7c. Bare `cd <path>` (inside workspace — the cwd-escape detector
529
766
  // upgrades the class to write_protected when the target is
530
767
  // outside). Standalone `cd` (HOME) is escape, also handled by the
@@ -568,7 +805,15 @@ function classifyComponent(cmd, ctx) {
568
805
  }
569
806
  function findDestructiveMatch(cmd) {
570
807
  const upper = cmd.toUpperCase();
571
- for (const { pattern, caseInsensitive } of DESTRUCTIVE_PATTERNS) {
808
+ for (const { pattern, caseInsensitive, regex } of DESTRUCTIVE_PATTERNS) {
809
+ if (regex) {
810
+ // Word-boundary regex form (external-derived patterns). Match
811
+ // against the trimmed component so `^` anchors to command head,
812
+ // not surrounding whitespace from the compound split.
813
+ if (regex.test(cmd.trim()))
814
+ return pattern;
815
+ continue;
816
+ }
572
817
  if (caseInsensitive) {
573
818
  if (upper.includes(pattern))
574
819
  return pattern;
@@ -637,14 +882,57 @@ function nestingDepth(cmd, open, close) {
637
882
  function escapeRegex(s) {
638
883
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
639
884
  }
885
+ /**
886
+ * Operator-override safe tokens. Read from `PUGI_CLASSIFIER_EXTRA_SAFE`
887
+ * (comma-separated). Allows operators to extend the BUILD_TEST first-
888
+ * token list at runtime for site-specific tooling без recompile.
889
+ *
890
+ * Security note: destructive substring patterns run BEFORE this gate
891
+ * (step 1 in classifyComponent), so this cannot whitelist `rm`, `mkfs`,
892
+ * `git push --force`, etc. The env var only adds tools to the benign
893
+ * build_test class. Invalid entries (empty strings, тokens containing
894
+ * shell metas) are silently dropped to avoid surprising classifications.
895
+ *
896
+ * Read fresh on every call so per-test mutations work и so operators
897
+ * can update without restarting the agent loop. The cost (one env var
898
+ * read + Set construction per call) is negligible for the classifier's
899
+ * call frequency.
900
+ */
901
+ function readExtraSafeTokens() {
902
+ const raw = process.env.PUGI_CLASSIFIER_EXTRA_SAFE;
903
+ if (!raw || raw.trim() === '')
904
+ return new Set();
905
+ const tokens = new Set();
906
+ for (const candidate of raw.split(',')) {
907
+ const trimmed = candidate.trim();
908
+ if (trimmed === '')
909
+ continue;
910
+ // Reject anything containing shell metas or whitespace — only bare
911
+ // tool names allowed. Defends against accidental
912
+ // `PUGI_CLASSIFIER_EXTRA_SAFE='rm -rf /'` smuggling.
913
+ if (/[\s;|&<>$`(){}\[\]'"\\]/.test(trimmed))
914
+ continue;
915
+ tokens.add(trimmed);
916
+ }
917
+ return tokens;
918
+ }
640
919
  function detectProtectedWrite(cmd, ctx) {
641
920
  // Surface every write target this command produces so we can both
642
921
  // protected-path-check and outside-workspace-check them uniformly.
643
922
  // Captures `sort -o`, `uniq <in> <out>`, `sed -i` files, `awk '... > "file"'`,
644
923
  // and `>` / `>>` redirections without surrounding whitespace.
645
924
  const writeTargets = extractWriteTargets(cmd);
925
+ // Strip heredoc bodies before substring scan. Heredoc payloads are
926
+ // DATA (file contents the script writes), not commands the shell
927
+ // executes — a `package.json` body containing `/usr/local/bin/...`
928
+ // would FP as "Write into protected path: /usr/" under the broad
929
+ // includes() scan below. The per-target check at the bottom of this
930
+ // function still catches real `cat > /usr/file << EOF` attempts
931
+ // because extractWriteTargets reads the redirection target, not the
932
+ // heredoc body. CEO dogfood (#28 follow-up).
933
+ const cmdForScan = stripHeredocBodies(cmd);
646
934
  for (const needle of PROTECTED_PATH_SUBSTRINGS) {
647
- if (!cmd.includes(needle))
935
+ if (!cmdForScan.includes(needle))
648
936
  continue;
649
937
  // Reading from a protected path is allowed at the classifier
650
938
  // layer (the permission engine still gates `read`); writing is
@@ -695,17 +983,67 @@ function detectProtectedWrite(cmd, ctx) {
695
983
  }
696
984
  /**
697
985
  * Extract every write-target path the command produces. Covers:
698
- * - shell redirection `> file`, `>> file` (with optional whitespace,
699
- * skipping `>&1`, `>&2`, etc.)
700
- * - `sort -o file`
701
- * - `uniq <input> <output>` (the two-arg form)
702
- * - `sed -i <file>...` (in-place edit treats every trailing file as a
703
- * write target)
704
- * - `awk '... > "file"'` (quoted redirection inside an awk script)
986
+ * - shell redirection `> file`, `>> file` (with optional whitespace,
987
+ * skipping `>&1`, `>&2`, etc.)
988
+ * - `sort -o file`
989
+ * - `uniq <input> <output>` (the two-arg form)
990
+ * - `sed -i <file>...` (in-place edit treats every trailing file as a
991
+ * write target)
992
+ * - `awk '... > "file"'` (quoted redirection inside an awk script)
705
993
  *
706
994
  * Conservative — we do not try to resolve shell vars or globs; the
707
995
  * caller still gates absolute paths via `looksAbsoluteOutsideWorkspace`.
708
996
  */
997
+ /**
998
+ * Strip heredoc bodies so substring scans (e.g. `cmd.includes('/usr/')`)
999
+ * do not false-positive on file content the script is *writing*. A
1000
+ * heredoc starts с `<< 'WORD'` (or `<< WORD` / `<<-WORD`) and ends на a
1001
+ * line containing only WORD. The body between is DATA, not commands.
1002
+ *
1003
+ * Best-effort: handles single-heredoc-per-command (the common case)
1004
+ * AND multiple sequential heredocs. Nested heredocs (heredoc-inside-
1005
+ * heredoc) are rare and out of scope — the substring scan still gates
1006
+ * the outer command, just без stripping the nested body. Per-target
1007
+ * detection at detectProtectedWrite's tail loop catches real
1008
+ * `cat > /usr/file << EOF` attacks regardless of body content.
1009
+ *
1010
+ * CEO dogfood : `cat > package.json << 'EOF'\n{"bin":
1011
+ * "/usr/local/bin/foo"}\nEOF` was rejected as "Write into protected
1012
+ * path: /usr/" because the broad substring scan saw `/usr/` in the
1013
+ * JSON body. With heredoc-body stripping, the scan now sees only
1014
+ * `cat > package.json << 'EOF' EOF` which contains no protected path.
1015
+ */
1016
+ function stripHeredocBodies(cmd) {
1017
+ // Match `<< [-]'WORD'` or `<< [-]"WORD"` or `<< [-]WORD` (quoted form
1018
+ // disables variable expansion in real bash; we treat all three the
1019
+ // same for stripping). Capture the WORD so we can find the close
1020
+ // marker.
1021
+ const heredocStart = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/g;
1022
+ let out = cmd;
1023
+ let safetyLoops = 0;
1024
+ let match;
1025
+ while ((match = heredocStart.exec(out)) !== null) {
1026
+ if (++safetyLoops > 16)
1027
+ break;
1028
+ const word = match[2];
1029
+ if (!word)
1030
+ continue;
1031
+ const headEnd = match.index + match[0].length;
1032
+ // Find the close-marker line: `\n<optional indent>WORD<\n|$>`.
1033
+ const closeRegex = new RegExp(`\\n\\s*${word}(?:\\n|$)`);
1034
+ closeRegex.lastIndex = headEnd;
1035
+ const closeMatch = closeRegex.exec(out.slice(headEnd));
1036
+ if (!closeMatch)
1037
+ break;
1038
+ const closeStart = headEnd + closeMatch.index;
1039
+ const closeEnd = closeStart + closeMatch[0].length;
1040
+ // Replace heredoc body + close marker с single space so the regex
1041
+ // iterator's lastIndex stays meaningful.
1042
+ out = out.slice(0, headEnd) + ' ' + out.slice(closeEnd);
1043
+ heredocStart.lastIndex = headEnd + 1;
1044
+ }
1045
+ return out;
1046
+ }
709
1047
  function extractWriteTargets(cmd) {
710
1048
  const targets = [];
711
1049
  // Shell redirection (`>`, `>>`) with optional whitespace. Skip
@@ -777,14 +1115,72 @@ function detectProtectedRead(cmd) {
777
1115
  firstToken === 'find';
778
1116
  if (!isReadTool)
779
1117
  return null;
1118
+ // Strip heredoc bodies so `cat > config << 'EOF'\n... /etc/... \nEOF`
1119
+ // does not FP as "Read from protected path" when first-token=`cat` +
1120
+ // redirection writes к workspace-local file. Heredoc payload is data.
1121
+ // CEO dogfood .
1122
+ const cmdForScan = stripHeredocBodies(cmd);
780
1123
  for (const needle of PROTECTED_PATH_SUBSTRINGS) {
781
- if (cmd.includes(needle)) {
1124
+ if (cmdForScan.includes(needle)) {
782
1125
  return {
783
1126
  reason: `Read from protected path: ${needle}`,
784
1127
  matched: needle,
785
1128
  };
786
1129
  }
787
1130
  }
1131
+ // P0 fix: extend protected-read detection to credential
1132
+ // basenames (`cat .env`, `head id_rsa`, `grep TOKEN .env.production`).
1133
+ // Without this branch, the engine model can bypass the `read` tool's
1134
+ // `protectedTargetReason` gate by emitting a bash `cat` — the read
1135
+ // tool refuses, the model falls back to bash, and the classifier
1136
+ // (which only knew about full-path substrings) classified `cat .env`
1137
+ // as benign `read`. The `local-first-invariants` spec proved the leak.
1138
+ for (const pattern of PROTECTED_BASENAME_PATTERNS) {
1139
+ const match = cmd.match(pattern);
1140
+ if (match) {
1141
+ return {
1142
+ reason: `Read from protected basename: ${match[0].trim()}`,
1143
+ matched: match[0].trim(),
1144
+ };
1145
+ }
1146
+ }
1147
+ return null;
1148
+ }
1149
+ /**
1150
+ * Detect parent-traversal segments (`..`) inside read-class commands.
1151
+ * The file-tools layer (`resolveWorkspacePath`) refuses these for the
1152
+ * `read`/`glob`/`grep` tools, but bash had no equivalent gate. We
1153
+ * trigger on the SAME shape `path-security.ts` rejects: a `..` segment
1154
+ * separated by `/` or whitespace. Quoted/escaped variants get the same
1155
+ * treatment.
1156
+ *
1157
+ * Returns null on the safe path (no `..` segment) so the caller falls
1158
+ * through to the regular read classification.
1159
+ */
1160
+ function detectParentTraversalRead(cmd) {
1161
+ const firstToken = cmd.split(/\s+/)[0] ?? '';
1162
+ const isReadTool = READ_TOKENS.has(firstToken) ||
1163
+ READ_PREFIX_TOKENS.has(firstToken) ||
1164
+ firstToken === 'sed' ||
1165
+ firstToken === 'awk' ||
1166
+ firstToken === 'find';
1167
+ if (!isReadTool)
1168
+ return null;
1169
+ // Match `..` as a path segment: preceded by start/whitespace/quote/`/`
1170
+ // and followed by `/`, end-of-string, whitespace, or shell metas.
1171
+ // Avoids over-matching `v1..v2` (range syntax inside a single token)
1172
+ // and `1..5` (numeric ranges) because those lack the path boundary.
1173
+ const traversalPattern = /(^|[\s'"\/])\.\.(\/|$|[\s'"<>|;&])/;
1174
+ const m = cmd.match(traversalPattern);
1175
+ if (m) {
1176
+ return {
1177
+ reason: 'Read command escapes workspace via parent traversal',
1178
+ matched: '..',
1179
+ };
1180
+ }
1181
+ // Absolute path read of /etc, /usr, /var, etc is already covered by
1182
+ // PROTECTED_PATH_SUBSTRINGS in detectProtectedRead — no extra branch
1183
+ // needed here.
788
1184
  return null;
789
1185
  }
790
1186
  function detectEnvWrite(cmd) {