@pugi/cli 0.1.0-beta.99 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +11 -191
- package/bin/pugi +8 -0
- package/package.json +15 -71
- package/postinstall.mjs +31 -0
- package/CHANGELOG.md +0 -132
- package/THIRD_PARTY_NOTICES.md +0 -40
- package/assets/pugi-mascot.ansi +0 -16
- package/assets/pugi-prozr2-mascot.ansi +0 -9
- package/bin/run.js +0 -34
- package/dist/commands/deploy.js +0 -439
- package/dist/commands/flatten.js +0 -191
- package/dist/commands/jobs-watch.js +0 -201
- package/dist/commands/jobs.js +0 -260
- package/dist/commands/retro.js +0 -210
- package/dist/commands/smoke.js +0 -133
- package/dist/core/agent-progress/cleanup.js +0 -134
- package/dist/core/agent-progress/schema.js +0 -144
- package/dist/core/agent-progress/writer.js +0 -101
- package/dist/core/agents/adaptive-router.js +0 -330
- package/dist/core/agents/loader.js +0 -104
- package/dist/core/agents/query-decomposer.js +0 -297
- package/dist/core/agents/registry.js +0 -69
- package/dist/core/approvals/shortcut-resolver.js +0 -98
- package/dist/core/artifact-chain/dispatcher.js +0 -148
- package/dist/core/artifact-chain/exporter.js +0 -164
- package/dist/core/artifact-chain/state.js +0 -243
- package/dist/core/artifact-chain/steps.js +0 -169
- package/dist/core/ask-user/question.js +0 -92
- package/dist/core/audit/audit-trail.js +0 -275
- package/dist/core/auth/ensure-authenticated.js +0 -129
- package/dist/core/auth/env-provider.js +0 -238
- package/dist/core/auto-open-browser.js +0 -128
- package/dist/core/auto-update/channels.js +0 -122
- package/dist/core/auto-update/checker.js +0 -241
- package/dist/core/auto-update/state.js +0 -235
- package/dist/core/bare-mode/index.js +0 -107
- package/dist/core/bash/redirect.js +0 -281
- package/dist/core/bash-classifier.js +0 -1397
- package/dist/core/checkpoint/resumer.js +0 -149
- package/dist/core/checkpoint/rewinder.js +0 -291
- package/dist/core/checkpoints/shadow-git.js +0 -670
- package/dist/core/citations/parser.js +0 -109
- package/dist/core/classifier/yolo-classifier.js +0 -88
- package/dist/core/clipboard.js +0 -70
- package/dist/core/codegraph/decision-store.js +0 -248
- package/dist/core/codegraph/detect-repo.js +0 -459
- package/dist/core/codegraph/install.js +0 -134
- package/dist/core/codegraph/offer-hook.js +0 -220
- package/dist/core/compact/auto-trigger.js +0 -96
- package/dist/core/compact/buffer-rewriter.js +0 -115
- package/dist/core/compact/summarizer.js +0 -208
- package/dist/core/compact/token-counter.js +0 -108
- package/dist/core/consensus/anvil-fanout.js +0 -276
- package/dist/core/consensus/diff-capture.js +0 -491
- package/dist/core/consensus/rubric.js +0 -233
- package/dist/core/context/builder.js +0 -114
- package/dist/core/context/compaction-events.js +0 -99
- package/dist/core/context/compaction.js +0 -602
- package/dist/core/context/index.js +0 -28
- package/dist/core/context/invariants.js +0 -250
- package/dist/core/context/markdown-loader.js +0 -288
- package/dist/core/context/markdown-traverse.js +0 -255
- package/dist/core/context/pugiignore.js +0 -316
- package/dist/core/context/repo-skeleton.js +0 -533
- package/dist/core/context/tool-eviction.js +0 -55
- package/dist/core/context/watcher.js +0 -342
- package/dist/core/context/working-set.js +0 -165
- package/dist/core/coordinator/agent-tools.js +0 -77
- package/dist/core/coordinator/agent-toolset.js +0 -65
- package/dist/core/coordinator/fsm.js +0 -73
- package/dist/core/coordinator/mode-fsm.js +0 -70
- package/dist/core/cost/rate-card.js +0 -129
- package/dist/core/cost/tracker.js +0 -221
- package/dist/core/credentials.js +0 -355
- package/dist/core/cron/scheduler.js +0 -138
- package/dist/core/denial-tracking/index.js +0 -8
- package/dist/core/denial-tracking/state.js +0 -264
- package/dist/core/diagnostics/probe-runner.js +0 -93
- package/dist/core/diagnostics/probes/api.js +0 -46
- package/dist/core/diagnostics/probes/auth.js +0 -93
- package/dist/core/diagnostics/probes/bare-mode.js +0 -42
- package/dist/core/diagnostics/probes/cli-version.js +0 -127
- package/dist/core/diagnostics/probes/config.js +0 -72
- package/dist/core/diagnostics/probes/denial-tracking.js +0 -57
- package/dist/core/diagnostics/probes/disk.js +0 -81
- package/dist/core/diagnostics/probes/engine-live.js +0 -46
- package/dist/core/diagnostics/probes/git.js +0 -65
- package/dist/core/diagnostics/probes/hooks.js +0 -118
- package/dist/core/diagnostics/probes/mcp.js +0 -75
- package/dist/core/diagnostics/probes/node.js +0 -59
- package/dist/core/diagnostics/probes/pnpm.js +0 -36
- package/dist/core/diagnostics/probes/pugi-md.js +0 -89
- package/dist/core/diagnostics/probes/sandbox.js +0 -72
- package/dist/core/diagnostics/probes/session.js +0 -74
- package/dist/core/diagnostics/probes/status-snapshot.js +0 -488
- package/dist/core/diagnostics/probes/workspace.js +0 -63
- package/dist/core/diagnostics/types.js +0 -70
- package/dist/core/dispatch/cache-cleanup.js +0 -197
- package/dist/core/dispatch/cache-handoff.js +0 -295
- package/dist/core/edits/apply-patch-layer-e.js +0 -189
- package/dist/core/edits/dispatch.js +0 -511
- package/dist/core/edits/format-detector.js +0 -260
- package/dist/core/edits/format-matrix.js +0 -26
- package/dist/core/edits/fuzzy-ladder.js +0 -650
- package/dist/core/edits/index.js +0 -19
- package/dist/core/edits/journal.js +0 -199
- package/dist/core/edits/layer-a-apply.js +0 -217
- package/dist/core/edits/layer-a-fuzzy-apply.js +0 -198
- package/dist/core/edits/layer-b-apply.js +0 -211
- package/dist/core/edits/layer-c-apply.js +0 -160
- package/dist/core/edits/layer-d-ast.js +0 -572
- package/dist/core/edits/marker-parser.js +0 -401
- package/dist/core/edits/security-gate.js +0 -223
- package/dist/core/edits/verify-hook.js +0 -273
- package/dist/core/edits/worktree.js +0 -322
- package/dist/core/engine/adapter-runner.js +0 -8
- package/dist/core/engine/anvil-client.js +0 -344
- package/dist/core/engine/auto-compact.js +0 -179
- package/dist/core/engine/budgets.js +0 -195
- package/dist/core/engine/context-prefix.js +0 -155
- package/dist/core/engine/index.js +0 -12
- package/dist/core/engine/intensity.js +0 -163
- package/dist/core/engine/intent.js +0 -260
- package/dist/core/engine/native-pugi.js +0 -1616
- package/dist/core/engine/noop.js +0 -27
- package/dist/core/engine/prompts.js +0 -236
- package/dist/core/engine/strip-internal-fields.js +0 -124
- package/dist/core/engine/tool-bridge.js +0 -2173
- package/dist/core/engine/verification-patterns.js +0 -195
- package/dist/core/evaluation/golden-dataset.js +0 -293
- package/dist/core/feedback/queue.js +0 -177
- package/dist/core/feedback/submitter.js +0 -145
- package/dist/core/file-cache.js +0 -141
- package/dist/core/flatten/flatten-repo.js +0 -439
- package/dist/core/format/osc8-link.js +0 -28
- package/dist/core/hook-chains.js +0 -392
- package/dist/core/hooks/citation-verify-hook.js +0 -138
- package/dist/core/hooks/citation-verify.js +0 -112
- package/dist/core/hooks/events.js +0 -46
- package/dist/core/hooks/index.js +0 -15
- package/dist/core/hooks/registry.js +0 -216
- package/dist/core/hooks/runner.js +0 -236
- package/dist/core/hooks/v2/event-emitter.js +0 -115
- package/dist/core/hooks/v2/executor.js +0 -282
- package/dist/core/hooks/v2/index.js +0 -25
- package/dist/core/hooks/v2/lifecycle.js +0 -104
- package/dist/core/hooks/v2/loader.js +0 -216
- package/dist/core/hooks/v2/matcher.js +0 -125
- package/dist/core/hooks/v2/trust.js +0 -143
- package/dist/core/hooks/v2/types.js +0 -86
- package/dist/core/hooks/worktree-events.js +0 -158
- package/dist/core/hooks.js +0 -415
- package/dist/core/image/renderer.js +0 -71
- package/dist/core/index-store.js +0 -260
- package/dist/core/init/detector.js +0 -582
- package/dist/core/init/template-renderer.js +0 -242
- package/dist/core/jobs/registry.js +0 -462
- package/dist/core/ledger/results-tsv.js +0 -142
- package/dist/core/log-discipline/stdout-redirect.js +0 -51
- package/dist/core/lsp/cache.js +0 -105
- package/dist/core/lsp/client.js +0 -1229
- package/dist/core/lsp/language-detect.js +0 -66
- package/dist/core/lsp/post-edit-diagnostics.js +0 -171
- package/dist/core/lsp/server-detect.js +0 -173
- package/dist/core/lsp/symbol-cache.js +0 -162
- package/dist/core/lsp/symbol-tools.js +0 -664
- package/dist/core/mcp/client.js +0 -385
- package/dist/core/mcp/http-server.js +0 -553
- package/dist/core/mcp/orchestrator-config.js +0 -192
- package/dist/core/mcp/orchestrator-tools.js +0 -806
- package/dist/core/mcp/permission.js +0 -190
- package/dist/core/mcp/registry.js +0 -193
- package/dist/core/mcp/server-tools.js +0 -219
- package/dist/core/mcp/server.js +0 -397
- package/dist/core/mcp/trust.js +0 -91
- package/dist/core/memory/dual-write.js +0 -416
- package/dist/core/memory/passive-extract.js +0 -130
- package/dist/core/memory/phase1-kinds.js +0 -20
- package/dist/core/memory/secret-scanner.js +0 -304
- package/dist/core/memory-sync/queue.js +0 -170
- package/dist/core/metrics/extract.js +0 -113
- package/dist/core/modes/roo-modes.js +0 -68
- package/dist/core/onboarding/ensure-initialized.js +0 -133
- package/dist/core/onboarding/marker.js +0 -111
- package/dist/core/onboarding/telemetry-state.js +0 -108
- package/dist/core/output-style/presets.js +0 -176
- package/dist/core/output-style/state.js +0 -185
- package/dist/core/path-security.js +0 -345
- package/dist/core/permission.js +0 -369
- package/dist/core/permissions/auto-classifier.js +0 -124
- package/dist/core/permissions/bash-parser.js +0 -371
- package/dist/core/permissions/circuit-breaker.js +0 -83
- package/dist/core/permissions/constrained-edit.js +0 -91
- package/dist/core/permissions/gate.js +0 -278
- package/dist/core/permissions/index.js +0 -20
- package/dist/core/permissions/mode.js +0 -174
- package/dist/core/permissions/network-egress.js +0 -137
- package/dist/core/permissions/state.js +0 -241
- package/dist/core/permissions/tool-class.js +0 -107
- package/dist/core/plan-mode/ui-state.js +0 -51
- package/dist/core/plans/plan-artifact.js +0 -721
- package/dist/core/policy-limits/etag-store.js +0 -122
- package/dist/core/prd-check/parser.js +0 -215
- package/dist/core/prd-check/reporter.js +0 -127
- package/dist/core/prd-check/session-review.js +0 -557
- package/dist/core/prd-check/verifiers.js +0 -223
- package/dist/core/prompt-cache/client-cache.js +0 -99
- package/dist/core/prompts/assembly.js +0 -29
- package/dist/core/prompts/registry.js +0 -364
- package/dist/core/pugi-gitignore.js +0 -52
- package/dist/core/pugi-md/cc-compat-rules.js +0 -735
- package/dist/core/pugi-md/context-injector.js +0 -76
- package/dist/core/pugi-md/walk-up.js +0 -207
- package/dist/core/python/uv-installer.js +0 -270
- package/dist/core/python/uv-resolver.js +0 -83
- package/dist/core/rate-limit/narrator.js +0 -146
- package/dist/core/recipes/cli-types.js +0 -20
- package/dist/core/recipes/loader.js +0 -103
- package/dist/core/recipes/runner.js +0 -345
- package/dist/core/recipes/schema.js +0 -587
- package/dist/core/release-notes/parser.js +0 -241
- package/dist/core/release-notes/state.js +0 -116
- package/dist/core/repl/ask.js +0 -512
- package/dist/core/repl/cancellation.js +0 -98
- package/dist/core/repl/cap-warning.js +0 -91
- package/dist/core/repl/clipboard-read.js +0 -174
- package/dist/core/repl/dispatch-fsm.js +0 -220
- package/dist/core/repl/engine-bridge.js +0 -303
- package/dist/core/repl/history-search.js +0 -175
- package/dist/core/repl/history.js +0 -182
- package/dist/core/repl/kill-ring.js +0 -138
- package/dist/core/repl/model-pricing.js +0 -135
- package/dist/core/repl/privacy-banner.js +0 -71
- package/dist/core/repl/session.js +0 -4962
- package/dist/core/repl/slash-commands.js +0 -747
- package/dist/core/repl/store/index.js +0 -12
- package/dist/core/repl/store/jsonl-log.js +0 -321
- package/dist/core/repl/store/lockfile.js +0 -155
- package/dist/core/repl/store/session-store.js +0 -821
- package/dist/core/repl/store/types.js +0 -44
- package/dist/core/repl/store/uuid-v7.js +0 -68
- package/dist/core/repl/tool-route.js +0 -382
- package/dist/core/repl/workspace-context.js +0 -206
- package/dist/core/repo-map/build.js +0 -125
- package/dist/core/repo-map/cache.js +0 -185
- package/dist/core/repo-map/extractor.js +0 -254
- package/dist/core/repo-map/formatter.js +0 -145
- package/dist/core/repo-map/page-rank.js +0 -105
- package/dist/core/repo-map/scanner.js +0 -211
- package/dist/core/retro/git-collector.js +0 -251
- package/dist/core/retro/health-card.js +0 -25
- package/dist/core/retro/metrics.js +0 -342
- package/dist/core/retro/narrative.js +0 -249
- package/dist/core/retro/plane-collector.js +0 -274
- package/dist/core/retro/pr-issue-link.js +0 -65
- package/dist/core/retro/types.js +0 -16
- package/dist/core/retry-budget/budget.js +0 -284
- package/dist/core/retry-budget/index.js +0 -5
- package/dist/core/retry-budget/retry-cap.js +0 -74
- package/dist/core/routing/lead-worker.js +0 -43
- package/dist/core/routing/pre-flight-estimator.js +0 -108
- package/dist/core/runs/run-tree.js +0 -103
- package/dist/core/sandboxing/adapter.js +0 -29
- package/dist/core/sandboxing/index.js +0 -49
- package/dist/core/sandboxing/none.js +0 -19
- package/dist/core/sandboxing/seatbelt.js +0 -183
- package/dist/core/security/injection-scanner.js +0 -367
- package/dist/core/security/output-filter.js +0 -418
- package/dist/core/session/env-file.js +0 -105
- package/dist/core/session/section-budgets.js +0 -140
- package/dist/core/session.js +0 -377
- package/dist/core/settings.js +0 -400
- package/dist/core/share/formatter.js +0 -271
- package/dist/core/share/redactor.js +0 -221
- package/dist/core/share/uploader.js +0 -267
- package/dist/core/skills/defaults.js +0 -457
- package/dist/core/skills/loader.js +0 -454
- package/dist/core/skills/sources.js +0 -480
- package/dist/core/skills/trust.js +0 -172
- package/dist/core/smoke/headless-driver.js +0 -174
- package/dist/core/smoke/orchestrator.js +0 -194
- package/dist/core/smoke/runner.js +0 -238
- package/dist/core/smoke/scenario-parser.js +0 -316
- package/dist/core/statusline.js +0 -99
- package/dist/core/subagents/dispatcher-real.js +0 -600
- package/dist/core/subagents/dispatcher.js +0 -352
- package/dist/core/subagents/index.js +0 -39
- package/dist/core/subagents/isolation-matrix.js +0 -213
- package/dist/core/subagents/spawn.js +0 -101
- package/dist/core/telemetry/emitter.js +0 -229
- package/dist/core/telemetry/queue.js +0 -251
- package/dist/core/theme/context.js +0 -91
- package/dist/core/theme/presets.js +0 -228
- package/dist/core/theme/state.js +0 -181
- package/dist/core/todos/invariant.js +0 -10
- package/dist/core/todos/state.js +0 -177
- package/dist/core/tool-schema/compressor.js +0 -89
- package/dist/core/transport/version-interceptor.js +0 -166
- package/dist/core/trust.js +0 -109
- package/dist/core/tui/thinking-block.js +0 -64
- package/dist/core/vim/keymap.js +0 -288
- package/dist/core/vim/state.js +0 -92
- package/dist/core/watch-markers/marker-watcher.js +0 -133
- package/dist/core/worktree/include-parser.js +0 -249
- package/dist/core/worktree-manager/cleanup.js +0 -123
- package/dist/core/worktree-manager/manager.js +0 -303
- package/dist/index.js +0 -44
- package/dist/runtime/bootstrap.js +0 -190
- package/dist/runtime/cli.js +0 -8121
- package/dist/runtime/commands/agents.js +0 -385
- package/dist/runtime/commands/budget.js +0 -192
- package/dist/runtime/commands/cancel.js +0 -231
- package/dist/runtime/commands/chain.js +0 -489
- package/dist/runtime/commands/codegraph-status.js +0 -227
- package/dist/runtime/commands/compact.js +0 -297
- package/dist/runtime/commands/config.js +0 -595
- package/dist/runtime/commands/cost.js +0 -199
- package/dist/runtime/commands/delegate.js +0 -312
- package/dist/runtime/commands/dispatch.js +0 -126
- package/dist/runtime/commands/doctor.js +0 -579
- package/dist/runtime/commands/feedback.js +0 -184
- package/dist/runtime/commands/hooks.js +0 -187
- package/dist/runtime/commands/init.js +0 -254
- package/dist/runtime/commands/lsp.js +0 -368
- package/dist/runtime/commands/mcp.js +0 -935
- package/dist/runtime/commands/memory.js +0 -582
- package/dist/runtime/commands/model.js +0 -237
- package/dist/runtime/commands/onboarding.js +0 -275
- package/dist/runtime/commands/patch.js +0 -128
- package/dist/runtime/commands/permissions.js +0 -112
- package/dist/runtime/commands/plan.js +0 -143
- package/dist/runtime/commands/prd-check.js +0 -285
- package/dist/runtime/commands/privacy.js +0 -107
- package/dist/runtime/commands/recipe.js +0 -325
- package/dist/runtime/commands/redo-blob-store.js +0 -92
- package/dist/runtime/commands/redo.js +0 -361
- package/dist/runtime/commands/release-notes.js +0 -229
- package/dist/runtime/commands/repo-map.js +0 -95
- package/dist/runtime/commands/report.js +0 -299
- package/dist/runtime/commands/resume.js +0 -118
- package/dist/runtime/commands/review-consensus.js +0 -414
- package/dist/runtime/commands/rewind.js +0 -333
- package/dist/runtime/commands/roster.js +0 -117
- package/dist/runtime/commands/sessions.js +0 -163
- package/dist/runtime/commands/share.js +0 -316
- package/dist/runtime/commands/skills.js +0 -401
- package/dist/runtime/commands/status.js +0 -186
- package/dist/runtime/commands/stickers.js +0 -82
- package/dist/runtime/commands/style.js +0 -194
- package/dist/runtime/commands/theme.js +0 -196
- package/dist/runtime/commands/undo.js +0 -361
- package/dist/runtime/commands/update.js +0 -289
- package/dist/runtime/commands/vim.js +0 -140
- package/dist/runtime/commands/worktree.js +0 -177
- package/dist/runtime/commands/worktrees.js +0 -155
- package/dist/runtime/deprecation-warning.js +0 -69
- package/dist/runtime/engine-exit-code.js +0 -50
- package/dist/runtime/headless-repl.js +0 -195
- package/dist/runtime/headless.js +0 -548
- package/dist/runtime/load-hooks-or-exit.js +0 -71
- package/dist/runtime/plan-decompose.js +0 -531
- package/dist/runtime/sigint-guard.js +0 -272
- package/dist/runtime/stream-renderer.js +0 -195
- package/dist/runtime/update-check.js +0 -294
- package/dist/runtime/version.js +0 -65
- package/dist/runtime/worktree-bootstrap.js +0 -579
- package/dist/skills/bundled/batch.js +0 -617
- package/dist/skills/bundled/index.js +0 -45
- package/dist/skills/bundled/loop.js +0 -358
- package/dist/skills/bundled/remember.js +0 -383
- package/dist/skills/bundled/simplify.js +0 -289
- package/dist/skills/bundled/skillify.js +0 -373
- package/dist/skills/bundled/stuck.js +0 -558
- package/dist/skills/bundled/verify.js +0 -439
- package/dist/testing/vcr.js +0 -486
- package/dist/tools/agent-tool.js +0 -229
- package/dist/tools/apply-patch.js +0 -556
- package/dist/tools/ask-user-question.js +0 -337
- package/dist/tools/ask-user.js +0 -115
- package/dist/tools/bash.js +0 -1238
- package/dist/tools/brief.js +0 -224
- package/dist/tools/cron.js +0 -433
- package/dist/tools/enter-worktree.js +0 -250
- package/dist/tools/exit-worktree.js +0 -147
- package/dist/tools/file-tools.js +0 -553
- package/dist/tools/http-request.js +0 -336
- package/dist/tools/lsp-tools.js +0 -565
- package/dist/tools/mcp-tool.js +0 -260
- package/dist/tools/multi-edit.js +0 -361
- package/dist/tools/powershell.js +0 -268
- package/dist/tools/registry.js +0 -166
- package/dist/tools/server-tools.js +0 -892
- package/dist/tools/skill-tool.js +0 -96
- package/dist/tools/sleep.js +0 -99
- package/dist/tools/synthetic-output.js +0 -133
- package/dist/tools/tasks.js +0 -208
- package/dist/tools/todo-write.js +0 -184
- package/dist/tools/verify-plan-execution.js +0 -295
- package/dist/tools/web-fetch-injection-scanner.js +0 -207
- package/dist/tools/web-fetch.js +0 -720
- package/dist/tools/web-search.js +0 -458
- package/dist/tui/agent-progress-card.js +0 -111
- package/dist/tui/agent-tree-pane.js +0 -9
- package/dist/tui/agent-tree.js +0 -87
- package/dist/tui/ask-cli.js +0 -52
- package/dist/tui/ask-modal.js +0 -211
- package/dist/tui/ask-user-question-chips.js +0 -315
- package/dist/tui/ask-user-question-prompt.js +0 -203
- package/dist/tui/compact-banner.js +0 -81
- package/dist/tui/conversation-pane.js +0 -164
- package/dist/tui/cost-table.js +0 -111
- package/dist/tui/device-flow.js +0 -142
- package/dist/tui/doctor-table.js +0 -46
- package/dist/tui/feedback-prompt.js +0 -156
- package/dist/tui/input-box.js +0 -732
- package/dist/tui/login-picker.js +0 -69
- package/dist/tui/markdown-render.js +0 -266
- package/dist/tui/multi-file-diff-approval.js +0 -375
- package/dist/tui/onboarding-wizard.js +0 -240
- package/dist/tui/permissions-picker.js +0 -86
- package/dist/tui/render.js +0 -160
- package/dist/tui/repl-render.js +0 -770
- package/dist/tui/repl-splash-art.js +0 -64
- package/dist/tui/repl-splash-mascot.js +0 -154
- package/dist/tui/repl-splash.js +0 -117
- package/dist/tui/repl.js +0 -378
- package/dist/tui/slash-palette.js +0 -106
- package/dist/tui/splash-data.js +0 -61
- package/dist/tui/splash.js +0 -31
- package/dist/tui/status-bar.js +0 -209
- package/dist/tui/status-table.js +0 -7
- package/dist/tui/stickers-art.js +0 -136
- package/dist/tui/style-table.js +0 -28
- package/dist/tui/theme-table.js +0 -29
- package/dist/tui/thinking-spinner.js +0 -123
- package/dist/tui/tool-stream-pane.js +0 -140
- package/dist/tui/update-banner.js +0 -33
- package/dist/tui/vim-input.js +0 -267
- package/dist/tui/welcome-banner.js +0 -107
- package/dist/tui/welcome-data.js +0 -293
- package/dist/tui/workspace-context.js +0 -105
- package/docs/examples/codegraph.mcp.json +0 -10
- package/test/scenarios/codegen-create-file.scenario.txt +0 -13
- package/test/scenarios/compact-force.scenario.txt +0 -12
- package/test/scenarios/identity.scenario.txt +0 -11
- package/test/scenarios/persona-handoff.scenario.txt +0 -12
- package/test/scenarios/walkback.scenario.txt +0 -12
package/dist/tools/file-tools.js
DELETED
|
@@ -1,553 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* file-tools - Pugi CLI file/bash/glob/grep tool surface.
|
|
3
|
-
*
|
|
4
|
-
* Workspace-binding contract (CEO red-alert follow-up):
|
|
5
|
-
*
|
|
6
|
-
* Every tool dispatch path threads `ctx.root` from the operator's
|
|
7
|
-
* `process.cwd()` through `EngineTask.workspaceRoot` ->
|
|
8
|
-
* `native-pugi.run()` -> `toolCtx.root` -> here. Tools call
|
|
9
|
-
* `resolveWorkspacePath(ctx.root, path)` for every on-disk operation
|
|
10
|
-
* so a dispatched specialist (e.g. Hiroshi writing tic-tac-toe HTML)
|
|
11
|
-
* produces files in the OPERATOR'S cwd, never in a server-side temp
|
|
12
|
-
* space. The path-security gate refuses traversal (`../etc/passwd`,
|
|
13
|
-
* URL-encoded variants, symlink escapes at the target).
|
|
14
|
-
*
|
|
15
|
-
* Wiring chain:
|
|
16
|
-
* 1. runtime/cli.ts: workspaceRoot = process.cwd()
|
|
17
|
-
* 2. EngineTask.workspaceRoot threads through to native-pugi.run().
|
|
18
|
-
* 3. native-pugi: const root = task.workspaceRoot
|
|
19
|
-
* 4. tool-bridge: passes ctx.root to file-tools / bash.
|
|
20
|
-
* 5. file-tools: resolveWorkspacePath(ctx.root, path).
|
|
21
|
-
*
|
|
22
|
-
* The contract is locked by `test/tools-write-to-workspace.spec.ts`
|
|
23
|
-
* (6 cases covering relative + nested + absolute paths + traversal
|
|
24
|
-
* refusal). If any layer of the chain regressed silently, dispatched
|
|
25
|
-
* files would land in `/tmp` instead of the operator's repo, which
|
|
26
|
-
* is the same failure surface as the menu-mode anti-pattern the
|
|
27
|
-
* sibling commits close.
|
|
28
|
-
*/
|
|
29
|
-
import { spawnSync } from 'node:child_process';
|
|
30
|
-
import { existsSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
31
|
-
import { dirname, isAbsolute, relative } from 'node:path';
|
|
32
|
-
import { globSync } from 'node:fs';
|
|
33
|
-
import { decidePermission } from '../core/permission.js';
|
|
34
|
-
import { StaleReadError, createReadRecord, hashContent, } from '../core/file-cache.js';
|
|
35
|
-
import { resolveWorkspacePath } from '../core/path-security.js';
|
|
36
|
-
import { scanForInjection, summarizeFindings } from '../core/security/injection-scanner.js';
|
|
37
|
-
import { recordFileMutation, recordToolCall, recordToolResult } from '../core/session.js';
|
|
38
|
-
/**
|
|
39
|
-
* WriteGate marker — thrown by `gateOnCancellation` when the
|
|
40
|
-
* caller supplied a cancellation token that has already aborted. The
|
|
41
|
-
* tool dispatch loop in `tool-bridge.ts` recognises the name and folds
|
|
42
|
-
* the throw into a `status: 'aborted'` tool result rather than a hard
|
|
43
|
-
* error so the loop terminates cleanly.
|
|
44
|
-
*/
|
|
45
|
-
export class OperatorAbortedError extends Error {
|
|
46
|
-
constructor(toolName) {
|
|
47
|
-
super(`operator_aborted: ${toolName} refused — operator cancelled the dispatch.`);
|
|
48
|
-
this.name = 'OperatorAbortedError';
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Re-export StaleReadError so tool-bridge / test consumers can import
|
|
52
|
-
// the typed error from a single file-tools surface alongside
|
|
53
|
-
// OperatorAbortedError. Same shape as the existing OperatorAbortedError
|
|
54
|
-
// re-surface pattern.
|
|
55
|
-
export { StaleReadError } from '../core/file-cache.js';
|
|
56
|
-
/**
|
|
57
|
-
* WriteGate: refuse the tool dispatch when the active
|
|
58
|
-
* cancellation token has aborted. Idempotent (the token's `isAborted`
|
|
59
|
-
* is a getter, no side effects). Returns void on the happy path so the
|
|
60
|
-
* tool can proceed; throws `OperatorAbortedError` when cancelled.
|
|
61
|
-
*
|
|
62
|
-
* The audit trail still gets the call: `recordToolCall` already fired
|
|
63
|
-
* upstream of this guard so the abort + reason are persisted. The
|
|
64
|
-
* matching `recordToolResult` is fired by the caller in its catch
|
|
65
|
-
* block with `status: 'cancelled'` (see existing path for `error`).
|
|
66
|
-
*/
|
|
67
|
-
export function gateOnCancellation(ctx, toolName) {
|
|
68
|
-
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
69
|
-
throw new OperatorAbortedError(toolName);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Re-check the permission decision against the *resolved* real path so
|
|
74
|
-
* a workspace-local symlink (`alias -> .env`) cannot bypass the protected
|
|
75
|
-
* basename check. The first `decidePermission` call sees only the user
|
|
76
|
-
* input (`alias`); this second call sees the realpath relative to root
|
|
77
|
-
* (`.env`), which `protectedTargetReason` recognises.
|
|
78
|
-
*
|
|
79
|
-
* Returns the resolved absolute path. Throws when the resolved path is
|
|
80
|
-
* gated by anything other than `allow`.
|
|
81
|
-
*/
|
|
82
|
-
function permissionGatedResolve(ctx, inputPath, action, toolName) {
|
|
83
|
-
const resolved = resolveWorkspacePath(ctx.root, inputPath);
|
|
84
|
-
let realPath;
|
|
85
|
-
try {
|
|
86
|
-
realPath = realpathSync.native(resolved);
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
// For writes to a file that does not yet exist there is no symlink
|
|
90
|
-
// to follow; fall back to the resolved path so the workspace check
|
|
91
|
-
// already done in `resolveWorkspacePath` is the only gate.
|
|
92
|
-
const code = error.code;
|
|
93
|
-
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
94
|
-
return resolved;
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
if (realPath === resolved)
|
|
98
|
-
return resolved;
|
|
99
|
-
const realRelative = relative(ctx.root, realPath);
|
|
100
|
-
const realDecision = decidePermission({ tool: toolName, kind: action, target: realRelative }, ctx.settings, ctx.root);
|
|
101
|
-
if (realDecision.decision !== 'allow') {
|
|
102
|
-
throw new Error(`Permission ${realDecision.decision} for ${action} ${realRelative} (via symlink ${inputPath}): ${realDecision.reason}`);
|
|
103
|
-
}
|
|
104
|
-
return realPath;
|
|
105
|
-
}
|
|
106
|
-
export function readTool(ctx, path) {
|
|
107
|
-
const toolCallId = recordToolCall(ctx.session, 'read', path);
|
|
108
|
-
// WriteGate: fail fast on operator cancel BEFORE permission
|
|
109
|
-
// decision so a half-second post-cancel race never lands the read.
|
|
110
|
-
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
111
|
-
const reason = 'operator_aborted: read refused';
|
|
112
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
113
|
-
throw new OperatorAbortedError('read');
|
|
114
|
-
}
|
|
115
|
-
const decision = decidePermission({ tool: 'read', kind: 'read', target: path }, ctx.settings, ctx.root);
|
|
116
|
-
if (decision.decision !== 'allow') {
|
|
117
|
-
const reason = `Permission ${decision.decision} for read ${path}: ${decision.reason}`;
|
|
118
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
119
|
-
throw new Error(reason);
|
|
120
|
-
}
|
|
121
|
-
let resolved;
|
|
122
|
-
try {
|
|
123
|
-
resolved = permissionGatedResolve(ctx, path, 'read', 'read');
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
const reason = error.message;
|
|
127
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
const content = readFileSync(resolved, 'utf8');
|
|
131
|
-
ctx.readCache.set(createReadRecord(ctx.root, path, content, 'read_tool'));
|
|
132
|
-
recordToolResult(ctx.session, toolCallId, 'success', `Read ${path}`);
|
|
133
|
-
return content;
|
|
134
|
-
}
|
|
135
|
-
export function writeTool(ctx, path, content) {
|
|
136
|
-
const toolCallId = recordToolCall(ctx.session, 'write', path);
|
|
137
|
-
// WriteGate: refuse the write when the operator has cancelled
|
|
138
|
-
// the dispatch. The audit log captures the cancellation reason so a
|
|
139
|
-
// post-mortem can distinguish operator_aborted from settings-deny.
|
|
140
|
-
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
141
|
-
const reason = 'operator_aborted: write refused';
|
|
142
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
143
|
-
throw new OperatorAbortedError('write');
|
|
144
|
-
}
|
|
145
|
-
const decision = decidePermission({ tool: 'write', kind: 'edit', target: path }, ctx.settings, ctx.root);
|
|
146
|
-
if (decision.decision !== 'allow') {
|
|
147
|
-
const reason = `Permission ${decision.decision} for write ${path}: ${decision.reason}`;
|
|
148
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
149
|
-
throw new Error(reason);
|
|
150
|
-
}
|
|
151
|
-
let resolved;
|
|
152
|
-
try {
|
|
153
|
-
resolved = permissionGatedResolve(ctx, path, 'edit', 'write');
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
const reason = error.message;
|
|
157
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
158
|
-
throw error;
|
|
159
|
-
}
|
|
160
|
-
const existed = existsSync(resolved);
|
|
161
|
-
// stale-read gate for writeTool's update-existing path. The
|
|
162
|
-
// model uses writeTool for two distinct intents:
|
|
163
|
-
//
|
|
164
|
-
// - create-new: path does not exist on disk. There is no prior
|
|
165
|
-
// read to validate against; skip the gate. This is the
|
|
166
|
-
// intentional escape hatch the leak spec also calls out.
|
|
167
|
-
// - overwrite-existing: path exists. Without the gate the model
|
|
168
|
-
// could blind-clobber an externally-modified file, losing the
|
|
169
|
-
// concurrent change silently. Force the model to re-read first.
|
|
170
|
-
//
|
|
171
|
-
// We deliberately apply the SAME stale-validation primitive editTool
|
|
172
|
-
// uses so the two write surfaces stay symmetric and a future fix to
|
|
173
|
-
// either one cannot accidentally weaken the other.
|
|
174
|
-
let before;
|
|
175
|
-
if (existed) {
|
|
176
|
-
before = readFileSync(resolved, 'utf8');
|
|
177
|
-
const currentStat = statSync(resolved);
|
|
178
|
-
const validation = ctx.readCache.validate(ctx.root, path, currentStat.mtimeMs, before);
|
|
179
|
-
if (validation.stale) {
|
|
180
|
-
const reason = `stale_read: write ${path} refused — ${validation.detail}`;
|
|
181
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
182
|
-
throw new StaleReadError(path, validation.reason, validation.detail);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
|
|
186
|
-
writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o600 });
|
|
187
|
-
renameSync(tmp, resolved);
|
|
188
|
-
// Injection scan (ported an external utility,
|
|
189
|
-
// Apache-2.0). Scan the BODY (never the path — path security is
|
|
190
|
-
// owned by `path-security.ts`). Findings are SURFACED as an extra
|
|
191
|
-
// line on the session tool-result, never block the write. Hard-
|
|
192
|
-
// block requires a separate CEO-signed PR. Failure here must NOT
|
|
193
|
-
// throw: a buggy scanner cannot rugpull the write that already
|
|
194
|
-
// landed on disk above.
|
|
195
|
-
surfaceInjectionWarning(ctx, toolCallId, 'write', path, content);
|
|
196
|
-
// Refresh the cache with the post-write content so the model can
|
|
197
|
-
// chain a follow-up read+edit on the same file without an extra
|
|
198
|
-
// round-trip. Same pattern editTool uses below.
|
|
199
|
-
ctx.readCache.set(createReadRecord(ctx.root, path, content, 'read_tool'));
|
|
200
|
-
recordFileMutation(ctx.session, {
|
|
201
|
-
toolCallId,
|
|
202
|
-
path,
|
|
203
|
-
operation: existed ? 'update' : 'create',
|
|
204
|
-
beforeHash: before ? hashContent(before) : undefined,
|
|
205
|
-
afterHash: hashContent(content),
|
|
206
|
-
});
|
|
207
|
-
recordToolResult(ctx.session, toolCallId, 'success', `${existed ? 'Updated' : 'Created'} ${path}`);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Surface an injection-scan warning on a file write/edit BODY. The
|
|
211
|
-
* scan never blocks — it folds findings into the session as a
|
|
212
|
-
* `tool_result` with status `warn` so an operator (or SOC pipeline
|
|
213
|
-
* tailing `<workspace>/.pugi/events.jsonl`) sees the signal without a
|
|
214
|
-
* mid-dispatch rollback.
|
|
215
|
-
*
|
|
216
|
-
* Wrapped in try/catch so a malformed scanner never crashes the tool
|
|
217
|
-
* loop — the write itself has already landed on disk by the time we
|
|
218
|
-
* call this.
|
|
219
|
-
*/
|
|
220
|
-
function surfaceInjectionWarning(ctx, triggeringToolCallId, tool, path, body) {
|
|
221
|
-
try {
|
|
222
|
-
const findings = scanForInjection(body);
|
|
223
|
-
if (findings.length === 0)
|
|
224
|
-
return;
|
|
225
|
-
const summary = summarizeFindings(findings);
|
|
226
|
-
const warnCallId = recordToolCall(ctx.session, 'injection_warning', path);
|
|
227
|
-
const message = `injection_warning: ${tool} ${path} — ${summary.total} pattern(s) ` +
|
|
228
|
-
`(score=${summary.score}, kinds=${summary.kinds.join('|')}). ` +
|
|
229
|
-
`Triggering call: ${triggeringToolCallId}. ` +
|
|
230
|
-
`Detector: external-injection-patterns. Write was NOT blocked.`;
|
|
231
|
-
recordToolResult(ctx.session, warnCallId, 'success', message);
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
// Scanner failure must NEVER throw — the write has already
|
|
235
|
-
// landed and the tool loop has to continue. Silent no-op
|
|
236
|
-
// mirrors the audit-trail contract.
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
export function editTool(ctx, path, oldString, newString) {
|
|
240
|
-
const toolCallId = recordToolCall(ctx.session, 'edit', path);
|
|
241
|
-
// WriteGate: refuse the edit when the operator has cancelled
|
|
242
|
-
// the dispatch. Edits are higher-risk than reads — surface the abort
|
|
243
|
-
// BEFORE we even consult permissions so a cancel-during-tool-loop
|
|
244
|
-
// never partially mutates the workspace.
|
|
245
|
-
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
246
|
-
const reason = 'operator_aborted: edit refused';
|
|
247
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
248
|
-
throw new OperatorAbortedError('edit');
|
|
249
|
-
}
|
|
250
|
-
const decision = decidePermission({ tool: 'edit', kind: 'edit', target: path }, ctx.settings, ctx.root);
|
|
251
|
-
if (decision.decision !== 'allow') {
|
|
252
|
-
const reason = `Permission ${decision.decision} for edit ${path}: ${decision.reason}`;
|
|
253
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
254
|
-
throw new Error(reason);
|
|
255
|
-
}
|
|
256
|
-
let resolved;
|
|
257
|
-
try {
|
|
258
|
-
resolved = permissionGatedResolve(ctx, path, 'edit', 'edit');
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
const reason = error.message;
|
|
262
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
// stale-read gate. Validate the model's read-time view of
|
|
266
|
-
// the file against the on-disk state BEFORE applying the mutation.
|
|
267
|
-
// We read disk content once and feed it to the validator so a single
|
|
268
|
-
// syscall covers both the gate decision AND the oldString/newString
|
|
269
|
-
// replacement below.
|
|
270
|
-
const before = readFileSync(resolved, 'utf8');
|
|
271
|
-
const currentStat = statSync(resolved);
|
|
272
|
-
const validation = ctx.readCache.validate(ctx.root, path, currentStat.mtimeMs, before);
|
|
273
|
-
if (validation.stale) {
|
|
274
|
-
const reason = `stale_read: edit ${path} refused — ${validation.detail}`;
|
|
275
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
276
|
-
throw new StaleReadError(path, validation.reason, validation.detail);
|
|
277
|
-
}
|
|
278
|
-
const currentHash = hashContent(before);
|
|
279
|
-
const matches = before.split(oldString).length - 1;
|
|
280
|
-
if (matches === 0) {
|
|
281
|
-
const reason = `Cannot edit ${path}: oldString not found`;
|
|
282
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
283
|
-
throw new Error(reason);
|
|
284
|
-
}
|
|
285
|
-
if (matches > 1) {
|
|
286
|
-
const reason = `Cannot edit ${path}: oldString is not unique`;
|
|
287
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
288
|
-
throw new Error(reason);
|
|
289
|
-
}
|
|
290
|
-
const after = before.replace(oldString, newString);
|
|
291
|
-
const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
|
|
292
|
-
writeFileSync(tmp, after, { encoding: 'utf8', mode: 0o600 });
|
|
293
|
-
renameSync(tmp, resolved);
|
|
294
|
-
// Injection scan (ported an external utility,
|
|
295
|
-
// Apache-2.0). We scan the NEW SUBSTRING the model is inserting,
|
|
296
|
-
// not the full post-edit file — the rest of the file is operator-
|
|
297
|
-
// owned content that pre-dates this dispatch. False-positive on
|
|
298
|
-
// legitimate prose that mentions banned phrases is the worst
|
|
299
|
-
// outcome and the warn-only contract bounds the cost.
|
|
300
|
-
surfaceInjectionWarning(ctx, toolCallId, 'edit', path, newString);
|
|
301
|
-
ctx.readCache.set(createReadRecord(ctx.root, path, after, 'read_tool'));
|
|
302
|
-
recordFileMutation(ctx.session, {
|
|
303
|
-
toolCallId,
|
|
304
|
-
path,
|
|
305
|
-
operation: 'update',
|
|
306
|
-
beforeHash: currentHash,
|
|
307
|
-
afterHash: hashContent(after),
|
|
308
|
-
});
|
|
309
|
-
recordToolResult(ctx.session, toolCallId, 'success', `Edited ${path}`);
|
|
310
|
-
}
|
|
311
|
-
export function globTool(ctx, pattern) {
|
|
312
|
-
const toolCallId = recordToolCall(ctx.session, 'glob', pattern);
|
|
313
|
-
// WriteGate: cancel-aware short-circuit. Glob is read-only but
|
|
314
|
-
// can be expensive on large trees; respecting the abort here keeps
|
|
315
|
-
// the tool loop responsive when the operator hits Ctrl+C mid-scan.
|
|
316
|
-
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
317
|
-
const reason = 'operator_aborted: glob refused';
|
|
318
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
319
|
-
throw new OperatorAbortedError('glob');
|
|
320
|
-
}
|
|
321
|
-
// Pugi globs are workspace-scoped. Reject any pattern that could enumerate
|
|
322
|
-
// outside the workspace:
|
|
323
|
-
// 1. absolute paths (`/etc/**/*`) — globSync resolves these against `/`
|
|
324
|
-
// regardless of `cwd`, so they fan out outside the repo.
|
|
325
|
-
// 2. `..` as a path SEGMENT (`../*`, `src/../etc`) — parent traversal.
|
|
326
|
-
// A substring check would over-reject legitimate names like
|
|
327
|
-
// `src/v1..v2/*` so we split on `/` instead.
|
|
328
|
-
if (isAbsolute(pattern)) {
|
|
329
|
-
const reason = `Absolute glob patterns are not allowed: ${pattern}`;
|
|
330
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
331
|
-
throw new Error(reason);
|
|
332
|
-
}
|
|
333
|
-
if (pattern.split('/').some((segment) => segment === '..')) {
|
|
334
|
-
const reason = `Glob pattern escapes workspace via '..' segment: ${pattern}`;
|
|
335
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
336
|
-
throw new Error(reason);
|
|
337
|
-
}
|
|
338
|
-
const results = globSync(pattern, {
|
|
339
|
-
cwd: ctx.root,
|
|
340
|
-
withFileTypes: false,
|
|
341
|
-
exclude: ['node_modules/**', 'dist/**', '.git/**', '.pugi/**'],
|
|
342
|
-
})
|
|
343
|
-
.map((entry) => String(entry))
|
|
344
|
-
.slice(0, 500);
|
|
345
|
-
recordToolResult(ctx.session, toolCallId, 'success', `Glob matched ${results.length} paths`);
|
|
346
|
-
return results;
|
|
347
|
-
}
|
|
348
|
-
export function grepTool(ctx, query) {
|
|
349
|
-
const toolCallId = recordToolCall(ctx.session, 'grep', query);
|
|
350
|
-
// WriteGate: refuse before scanning. Grep walks the whole
|
|
351
|
-
// workspace and can take seconds on a large repo; check abort first
|
|
352
|
-
// so a cancel mid-scan returns immediately rather than after the
|
|
353
|
-
// full walk completes.
|
|
354
|
-
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
355
|
-
const reason = 'operator_aborted: grep refused';
|
|
356
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
357
|
-
throw new OperatorAbortedError('grep');
|
|
358
|
-
}
|
|
359
|
-
const files = globTool(ctx, '**/*').filter((path) => !path.endsWith('/'));
|
|
360
|
-
const matches = [];
|
|
361
|
-
for (const path of files) {
|
|
362
|
-
if (matches.length >= 200)
|
|
363
|
-
break;
|
|
364
|
-
// WriteGate: poll abort inside the file loop so a cancel
|
|
365
|
-
// arriving mid-scan terminates early. The per-file branch keeps
|
|
366
|
-
// the responsiveness bounded by the slowest single-file read.
|
|
367
|
-
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
368
|
-
const reason = `operator_aborted: grep stopped mid-scan after ${matches.length} matches`;
|
|
369
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
370
|
-
throw new OperatorAbortedError('grep');
|
|
371
|
-
}
|
|
372
|
-
// Permission gate every file read individually — grep used to bypass
|
|
373
|
-
// `decidePermission` and could surface lines from protected files
|
|
374
|
-
// (.env, *.sql, *.pem, ~/.ssh/**) when invoked from a directory walk.
|
|
375
|
-
const decision = decidePermission({ tool: 'grep', kind: 'read', target: path }, ctx.settings, ctx.root);
|
|
376
|
-
if (decision.decision !== 'allow')
|
|
377
|
-
continue;
|
|
378
|
-
let fullPath;
|
|
379
|
-
try {
|
|
380
|
-
// `permissionGatedResolve` follows symlinks and re-checks the
|
|
381
|
-
// realpath against the permission rules. Without this an attacker
|
|
382
|
-
// could plant `alias -> .env` inside the workspace and recover
|
|
383
|
-
// secrets through `pugi explain .` because the initial decision
|
|
384
|
-
// only saw the unprotected basename `alias`.
|
|
385
|
-
fullPath = permissionGatedResolve(ctx, path, 'read', 'grep');
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
if (dirname(fullPath).includes('node_modules'))
|
|
391
|
-
continue;
|
|
392
|
-
try {
|
|
393
|
-
const lines = readFileSync(fullPath, 'utf8').split('\n');
|
|
394
|
-
lines.forEach((text, index) => {
|
|
395
|
-
if (matches.length < 200 && text.includes(query)) {
|
|
396
|
-
matches.push({ path: relative(ctx.root, fullPath), line: index + 1, text });
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
// Binary or unreadable files are ignored by the scaffolded grep tool.
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
recordToolResult(ctx.session, toolCallId, 'success', `Grep matched ${matches.length} lines`);
|
|
405
|
-
return matches;
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Workspace-scoped bash tool. Sized for the M1 engine adapter:
|
|
409
|
-
* - Runs through `/bin/sh -c <command>` so the model can use pipes,
|
|
410
|
-
* redirection, and shell builtins (`ls | wc -l`, `git status`).
|
|
411
|
-
* - `cwd` is pinned to the workspace root so a stray `cd /` cannot
|
|
412
|
-
* leak commands outside the repo (the child process inherits root
|
|
413
|
-
* filesystem visibility — destructive patterns are blocked by
|
|
414
|
-
* `decidePermission`, not by chroot).
|
|
415
|
-
* - Output capped at 64KB combined stdout/stderr to keep the
|
|
416
|
-
* transcript bounded; the model gets the head + a `(...truncated)`
|
|
417
|
-
* marker if the cap fires.
|
|
418
|
-
* - 30s wall-clock timeout. The engine loop's per-tool error path
|
|
419
|
-
* surfaces the timeout to the model so it can retry with a narrower
|
|
420
|
-
* command or give up.
|
|
421
|
-
*
|
|
422
|
-
* Permission gating: `kind: 'bash'`. The CLI's permission module already
|
|
423
|
-
* hard-denies the destructive-patterns list (rm -rf /, DROP DATABASE,
|
|
424
|
-
* etc) regardless of mode. Plan-mode callers MUST gate the bash tool
|
|
425
|
-
* out before it reaches the registry — `engine-tools.ts` does this.
|
|
426
|
-
*/
|
|
427
|
-
export const BASH_OUTPUT_CAP = 64 * 1024;
|
|
428
|
-
export const BASH_DEFAULT_TIMEOUT_MS = 30_000;
|
|
429
|
-
// Child-process stdio buffer — large enough that the model-facing
|
|
430
|
-
// truncation cap (`BASH_OUTPUT_CAP`) is always the gate, never the
|
|
431
|
-
// child's internal buffer. Code Reviewer P2 retro flagged
|
|
432
|
-
// `BASH_OUTPUT_CAP * 2` as too tight: real builds (`pnpm build`,
|
|
433
|
-
// `tsc --noEmit`) routinely exceed 128 KB combined and the model
|
|
434
|
-
// then saw a fatal `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` instead of a
|
|
435
|
-
// graceful `(...truncated at N bytes)` tail.
|
|
436
|
-
export const BASH_CHILD_MAXBUFFER = 10 * 1024 * 1024;
|
|
437
|
-
export function bashTool(ctx, command, options = {}) {
|
|
438
|
-
const toolCallId = recordToolCall(ctx.session, 'bash', command);
|
|
439
|
-
// WriteGate: bash is the highest-risk tool surface. Refuse
|
|
440
|
-
// before the destructive-pattern classifier even runs so a
|
|
441
|
-
// cancelled dispatch never spawns a child process. Note: this is
|
|
442
|
-
// pre-spawn cancellation only; once the /bin/sh -c process is
|
|
443
|
-
// running, the synchronous spawnSync wait blocks until it exits or
|
|
444
|
-
// the 30s timeout fires. Phase 2 will wire SIGTERM forwarding via
|
|
445
|
-
// an async wrapper.
|
|
446
|
-
if (ctx.cancellation && ctx.cancellation.isAborted) {
|
|
447
|
-
const reason = 'operator_aborted: bash refused';
|
|
448
|
-
recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
|
|
449
|
-
throw new OperatorAbortedError('bash');
|
|
450
|
-
}
|
|
451
|
-
const decision = decidePermission({ tool: 'bash', kind: 'bash', target: command }, ctx.settings, ctx.root);
|
|
452
|
-
if (decision.decision !== 'allow') {
|
|
453
|
-
const reason = `Permission ${decision.decision} for bash: ${decision.reason}`;
|
|
454
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
455
|
-
throw new Error(reason);
|
|
456
|
-
}
|
|
457
|
-
// `/bin/sh -c` is portable enough for the M1 alpha; downstream
|
|
458
|
-
// operators on hosts without /bin/sh can override via $SHELL, but
|
|
459
|
-
// that is an explicit opt-in for now.
|
|
460
|
-
//
|
|
461
|
-
// Env sanitisation strategy: build the child env from an explicit
|
|
462
|
-
// allow-list rather than inheriting `process.env` and trying to
|
|
463
|
-
// strip secrets after the fact. Code Reviewer P1 flagged
|
|
464
|
-
// that the deny-list approach missed ANTHROPIC_API_KEY / GH_TOKEN
|
|
465
|
-
// / AWS_SECRET_ACCESS_KEY / DATABASE_URL / arbitrary *_TOKEN /
|
|
466
|
-
// *_SECRET / *_KEY variables — every CI agent rotation would risk
|
|
467
|
-
// leaking a new secret name. Allow-listed PATH / HOME / USER /
|
|
468
|
-
// SHELL / LANG / LC_* / TERM / TZ + Pugi-internal PUGI_ROOT for
|
|
469
|
-
// tools that need it. Any other env variable is invisible to the
|
|
470
|
-
// child process.
|
|
471
|
-
const childEnv = {};
|
|
472
|
-
const SAFE_ENV_ALLOW = new Set([
|
|
473
|
-
'PATH',
|
|
474
|
-
'HOME',
|
|
475
|
-
'USER',
|
|
476
|
-
'LOGNAME',
|
|
477
|
-
'SHELL',
|
|
478
|
-
'LANG',
|
|
479
|
-
'TZ',
|
|
480
|
-
'TERM',
|
|
481
|
-
'PWD',
|
|
482
|
-
]);
|
|
483
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
484
|
-
if (value === undefined)
|
|
485
|
-
continue;
|
|
486
|
-
if (SAFE_ENV_ALLOW.has(key) || key.startsWith('LC_')) {
|
|
487
|
-
childEnv[key] = value;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
const timeoutMs = options.timeoutMs ?? BASH_DEFAULT_TIMEOUT_MS;
|
|
491
|
-
// `spawnSync` (vs `execFileSync`) captures stdout AND stderr on
|
|
492
|
-
// BOTH success and failure paths. Code Reviewer P1:
|
|
493
|
-
// `execFileSync` returns only stdout on exit 0, silently dropping
|
|
494
|
-
// stderr output from `tsc`, `eslint`, `pytest`, etc. — the model
|
|
495
|
-
// would see `(no output)` for successful runs that emitted real
|
|
496
|
-
// warnings.
|
|
497
|
-
//
|
|
498
|
-
// maxBuffer is generous (10 MB) so the child process is never the
|
|
499
|
-
// truncation gate — the post-hoc `.slice(0, BASH_OUTPUT_CAP)` below
|
|
500
|
-
// is the single source of truth for what the model sees. Code
|
|
501
|
-
// Reviewer P2 retro: the previous `BASH_OUTPUT_CAP * 2`
|
|
502
|
-
// (128 KB) would hard-throw `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`
|
|
503
|
-
// on noisy commands (`pnpm build`, `tsc --noEmit` on the whole
|
|
504
|
-
// monorepo) instead of returning the truncated head.
|
|
505
|
-
const result = spawnSync('/bin/sh', ['-c', command], {
|
|
506
|
-
cwd: ctx.root,
|
|
507
|
-
env: childEnv,
|
|
508
|
-
encoding: 'utf8',
|
|
509
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
510
|
-
timeout: timeoutMs,
|
|
511
|
-
maxBuffer: BASH_CHILD_MAXBUFFER,
|
|
512
|
-
});
|
|
513
|
-
if (result.error) {
|
|
514
|
-
const err = result.error;
|
|
515
|
-
if (err.code === 'ETIMEDOUT' || result.signal === 'SIGTERM') {
|
|
516
|
-
const reason = `bash command timed out after ${timeoutMs}ms`;
|
|
517
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
518
|
-
throw new Error(reason);
|
|
519
|
-
}
|
|
520
|
-
if (err.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
521
|
-
// maxBuffer overflow — surface as truncated rather than an
|
|
522
|
-
// opaque Node internal code so the model sees the same
|
|
523
|
-
// truncation marker it gets on stdout/stderr cap hits. With the
|
|
524
|
-
// post-Code-Reviewer-P2-retro-2026-05-23 maxBuffer at 10 MB
|
|
525
|
-
// this branch only fires on truly pathological output (>10 MB
|
|
526
|
-
// single command).
|
|
527
|
-
const reason = `bash output exceeded ${BASH_CHILD_MAXBUFFER} byte child-process buffer`;
|
|
528
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
529
|
-
throw new Error(reason);
|
|
530
|
-
}
|
|
531
|
-
const reason = `bash invocation failed: ${err.message ?? String(err)}`;
|
|
532
|
-
recordToolResult(ctx.session, toolCallId, 'error', reason);
|
|
533
|
-
throw new Error(reason);
|
|
534
|
-
}
|
|
535
|
-
const stdout = (result.stdout ?? '').toString();
|
|
536
|
-
const stderr = (result.stderr ?? '').toString();
|
|
537
|
-
const truncatedOut = stdout.length > BASH_OUTPUT_CAP;
|
|
538
|
-
const truncatedErr = stderr.length > BASH_OUTPUT_CAP;
|
|
539
|
-
const truncated = truncatedOut || truncatedErr;
|
|
540
|
-
const out = truncatedOut
|
|
541
|
-
? `${stdout.slice(0, BASH_OUTPUT_CAP)}\n(...truncated at ${BASH_OUTPUT_CAP} bytes)`
|
|
542
|
-
: stdout;
|
|
543
|
-
const err = truncatedErr
|
|
544
|
-
? `${stderr.slice(0, BASH_OUTPUT_CAP)}\n(...truncated at ${BASH_OUTPUT_CAP} bytes)`
|
|
545
|
-
: stderr;
|
|
546
|
-
const exitCode = result.status ?? 1;
|
|
547
|
-
// Non-zero exit is a normal outcome (e.g. `grep` finding no match,
|
|
548
|
-
// `test -f` returning 1). Surface it as a success at the audit
|
|
549
|
-
// layer; the engine loop feeds the exit code back to the model.
|
|
550
|
-
recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${exitCode} stdout=${stdout.length} stderr=${stderr.length}`);
|
|
551
|
-
return { stdout: out, stderr: err, exitCode, truncated };
|
|
552
|
-
}
|
|
553
|
-
//# sourceMappingURL=file-tools.js.map
|