@herbertgao/pi-extensions 2026.8.4 → 2026.8.6
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/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
- package/node_modules/@narumitw/pi-btw/README.md +32 -24
- package/node_modules/@narumitw/pi-btw/package.json +6 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +2535 -954
- package/node_modules/pi-lens/README.md +13 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
- package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
- package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
- package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
- package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
- package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
- package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
- package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
- package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
- package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
- package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
- package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
- package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
- package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
- package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
- package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
- package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
- package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
- package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
- package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
- package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
- package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
- package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
- package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
- package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
- package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
- package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
- package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
- package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
- package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
- package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
- package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
- package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
- package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +51785 -36118
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +219 -46
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +40 -15
- package/node_modules/pi-lens/docs/agent-tools.md +21 -10
- package/node_modules/pi-lens/docs/analysisall.md +192 -0
- package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
- package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
- package/node_modules/pi-lens/docs/environment-variables.md +44 -0
- package/node_modules/pi-lens/docs/fable.md +27 -0
- package/node_modules/pi-lens/docs/features.md +45 -3
- package/node_modules/pi-lens/docs/globalconfig.md +56 -1
- package/node_modules/pi-lens/docs/language-coverage.md +4 -3
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
- package/node_modules/pi-lens/docs/mcp.md +37 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
- package/node_modules/pi-lens/docs/settings.md +18 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +41 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +10 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
- package/node_modules/pi-mcp-adapter/README.md +38 -4
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
- package/node_modules/pi-mcp-adapter/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/init.ts +29 -5
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +4 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +176 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +12 -13
- package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
- package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
- package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
- package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
- package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
- package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
- package/node_modules/pi-lens/dist/clients/types.js +0 -11
|
@@ -1,32 +1,699 @@
|
|
|
1
|
+
import * as nodeFs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isPathIgnoredByProject } from "./file-utils.js";
|
|
4
|
+
import { tokenizeShellCommand } from "./bash-file-access.js";
|
|
5
|
+
import { logLatency } from "./latency-logger.js";
|
|
6
|
+
import { advisoryFileHash, advisoryPathKey, MAX_ADVISORY_AFFECTED_FILES, snapshotAdvisoryProvenance, } from "./advisory-provenance.js";
|
|
7
|
+
function resolveGuardPath(filePath, cwd) {
|
|
8
|
+
return path.resolve(cwd, filePath);
|
|
9
|
+
}
|
|
10
|
+
function guardPathKey(filePath, cwd) {
|
|
11
|
+
return advisoryPathKey(filePath, cwd);
|
|
12
|
+
}
|
|
13
|
+
function fileFingerprint(filePath) {
|
|
14
|
+
return advisoryFileHash(filePath);
|
|
15
|
+
}
|
|
16
|
+
function currentFileFingerprint(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
nodeFs.statSync(filePath);
|
|
19
|
+
return fileFingerprint(filePath);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
return err.code === "ENOENT"
|
|
23
|
+
? "missing"
|
|
24
|
+
: `unreadable:${err.code ?? "unknown"}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function capAffectedFiles(files, cwd) {
|
|
28
|
+
const unique = [...new Set(files.map((file) => resolveGuardPath(file, cwd)))];
|
|
29
|
+
return {
|
|
30
|
+
files: unique.slice(0, MAX_ADVISORY_AFFECTED_FILES),
|
|
31
|
+
truncated: unique.length > MAX_ADVISORY_AFFECTED_FILES,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Recovery is safe only when blocker content and its file provenance agree. */
|
|
35
|
+
function hasCompleteBlockingProvenance(blockerContent, blockingFiles, cwd) {
|
|
36
|
+
if (typeof blockerContent !== "string" || blockerContent.length === 0)
|
|
37
|
+
return false;
|
|
38
|
+
if (!Array.isArray(blockingFiles) || blockingFiles.length === 0)
|
|
39
|
+
return false;
|
|
40
|
+
if (blockingFiles.some((file) => typeof file !== "string" || file.trim().length === 0)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const provenanceKeys = blockingFiles.map((file) => guardPathKey(file, cwd));
|
|
44
|
+
if (new Set(provenanceKeys).size !== provenanceKeys.length)
|
|
45
|
+
return false;
|
|
46
|
+
const blockerKeys = blockerContent.split("\n").map((line) => {
|
|
47
|
+
const separator = line.indexOf(": ");
|
|
48
|
+
if (separator <= 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
const file = line.slice(0, separator).trim();
|
|
51
|
+
return file.length > 0 ? guardPathKey(file, cwd) : undefined;
|
|
52
|
+
});
|
|
53
|
+
if (blockerKeys.some((key) => key === undefined))
|
|
54
|
+
return false;
|
|
55
|
+
const uniqueBlockerKeys = new Set(blockerKeys);
|
|
56
|
+
return (uniqueBlockerKeys.size === blockerKeys.length &&
|
|
57
|
+
uniqueBlockerKeys.size === provenanceKeys.length &&
|
|
58
|
+
[...uniqueBlockerKeys].every((key) => provenanceKeys.includes(key)));
|
|
59
|
+
}
|
|
1
60
|
function getShellCommand(input) {
|
|
2
61
|
if (!input || typeof input !== "object")
|
|
3
62
|
return "";
|
|
4
63
|
const raw = input;
|
|
5
|
-
if (typeof raw.command === "string")
|
|
64
|
+
if (typeof raw.command === "string" && raw.command.trim())
|
|
6
65
|
return raw.command;
|
|
7
|
-
if (typeof raw.cmd === "string")
|
|
66
|
+
if (typeof raw.cmd === "string" && raw.cmd.trim())
|
|
8
67
|
return raw.cmd;
|
|
68
|
+
if (typeof raw.command === "string")
|
|
69
|
+
return raw.command;
|
|
9
70
|
return "";
|
|
10
71
|
}
|
|
72
|
+
function executableName(value) {
|
|
73
|
+
const normalized = value.replace(/\\/g, "/");
|
|
74
|
+
let name = (normalized.slice(normalized.lastIndexOf("/") + 1) ?? "").toLowerCase();
|
|
75
|
+
// Shell launchers are commonly supplied as resolved Windows paths or as
|
|
76
|
+
// PATHEXT-qualified names. Guard classification must happen after the same
|
|
77
|
+
// basename/extension normalization for every wrapper family.
|
|
78
|
+
return name.replace(/\.(?:exe|com|bat|cmd)$/i, "");
|
|
79
|
+
}
|
|
80
|
+
function isShellWrapper(value) {
|
|
81
|
+
return new Set([
|
|
82
|
+
"sh",
|
|
83
|
+
"bash",
|
|
84
|
+
"dash",
|
|
85
|
+
"zsh",
|
|
86
|
+
"ash",
|
|
87
|
+
"cmd",
|
|
88
|
+
"pwsh",
|
|
89
|
+
"powershell",
|
|
90
|
+
]).has(executableName(value));
|
|
91
|
+
}
|
|
92
|
+
function isGitExecutable(value) {
|
|
93
|
+
return new Set(["git", "git.exe", "git.cmd", "git.bat"]).has(executableName(value));
|
|
94
|
+
}
|
|
95
|
+
const COMMAND_STRING_WRAPPERS = new Set(["busybox", "toybox", "nix-shell"]);
|
|
96
|
+
function isCommandStringWrapper(value) {
|
|
97
|
+
return COMMAND_STRING_WRAPPERS.has(executableName(value));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Canonicalize shell parameter separators before command classification. The
|
|
101
|
+
* quote-aware pass deliberately leaves literal arguments alone; the lexer
|
|
102
|
+
* then supplies the command-position boundaries used by the guard.
|
|
103
|
+
*/
|
|
104
|
+
function canonicalizeGuardCommand(command) {
|
|
105
|
+
let result = "";
|
|
106
|
+
let quote;
|
|
107
|
+
let escaped = false;
|
|
108
|
+
for (let i = 0; i < command.length; i++) {
|
|
109
|
+
const ch = command[i];
|
|
110
|
+
if (quote === "single") {
|
|
111
|
+
result += ch;
|
|
112
|
+
if (ch === "'")
|
|
113
|
+
quote = undefined;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (quote === "double") {
|
|
117
|
+
result += ch;
|
|
118
|
+
if (escaped)
|
|
119
|
+
escaped = false;
|
|
120
|
+
else if (ch === "\\")
|
|
121
|
+
escaped = true;
|
|
122
|
+
else if (ch === '"')
|
|
123
|
+
quote = undefined;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (ch === "'" || ch === '"') {
|
|
127
|
+
quote = ch === "'" ? "single" : "double";
|
|
128
|
+
result += ch;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const parameter = command.slice(i).match(/^\$\{IFS[^}]*\}/)?.[0];
|
|
132
|
+
const positional = command.slice(i).match(/^\$IFS(?:\$[0-9]+)?/)?.[0];
|
|
133
|
+
if (parameter || positional) {
|
|
134
|
+
result += " ";
|
|
135
|
+
i += (parameter ?? positional ?? "").length - 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
result += ch;
|
|
139
|
+
}
|
|
140
|
+
let collapsed = "";
|
|
141
|
+
let pendingSpace = false;
|
|
142
|
+
quote = undefined;
|
|
143
|
+
for (const ch of result) {
|
|
144
|
+
if (!quote && /\s/.test(ch)) {
|
|
145
|
+
pendingSpace = collapsed.length > 0;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (pendingSpace)
|
|
149
|
+
collapsed += " ";
|
|
150
|
+
pendingSpace = false;
|
|
151
|
+
collapsed += ch;
|
|
152
|
+
if (!quote && (ch === "'" || ch === '"')) {
|
|
153
|
+
quote = ch === "'" ? "single" : "double";
|
|
154
|
+
}
|
|
155
|
+
else if ((quote === "single" && ch === "'") || (quote === "double" && ch === '"')) {
|
|
156
|
+
quote = undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return collapsed.trim();
|
|
160
|
+
}
|
|
161
|
+
/** Normalize only a command-position token; never apply this to path args. */
|
|
162
|
+
function normalizeGuardVerbToken(value) {
|
|
163
|
+
return value
|
|
164
|
+
.replace(/\\(?=[A-Za-z0-9_])/g, "")
|
|
165
|
+
.replace(/`(?=.)/g, "")
|
|
166
|
+
.replace(/\^(?=.)/g, "");
|
|
167
|
+
}
|
|
168
|
+
function expandGuardVerbToken(value) {
|
|
169
|
+
return normalizeGuardVerbToken(value).trim().split(/\s+/).filter(Boolean);
|
|
170
|
+
}
|
|
171
|
+
function delimitedSubstitutionBody(command, start, opener, closer) {
|
|
172
|
+
let depth = 1;
|
|
173
|
+
let quote;
|
|
174
|
+
let escaped = false;
|
|
175
|
+
for (let i = start + opener.length; i < command.length; i++) {
|
|
176
|
+
const ch = command[i];
|
|
177
|
+
if (quote === "single") {
|
|
178
|
+
if (ch === "'")
|
|
179
|
+
quote = undefined;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (quote === "double" && ch === '"') {
|
|
183
|
+
quote = undefined;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (escaped) {
|
|
187
|
+
escaped = false;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === "\\") {
|
|
191
|
+
escaped = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (ch === "'" || ch === '"') {
|
|
195
|
+
quote = ch === "'" ? "single" : "double";
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (command.startsWith(opener, i)) {
|
|
199
|
+
depth += 1;
|
|
200
|
+
i += opener.length - 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (command.startsWith(closer, i) && --depth === 0) {
|
|
204
|
+
return { body: command.slice(start + opener.length, i), end: i };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
function containsGuardedSubstitution(command, depth) {
|
|
210
|
+
if (depth > 3)
|
|
211
|
+
return false;
|
|
212
|
+
let quote;
|
|
213
|
+
let escaped = false;
|
|
214
|
+
for (let i = 0; i < command.length; i++) {
|
|
215
|
+
const ch = command[i];
|
|
216
|
+
if (quote === "single") {
|
|
217
|
+
if (ch === "'")
|
|
218
|
+
quote = undefined;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (quote === "double" && ch === '"') {
|
|
222
|
+
quote = undefined;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (escaped) {
|
|
226
|
+
escaped = false;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (ch === "\\") {
|
|
230
|
+
escaped = true;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (ch === '"' || ch === "'") {
|
|
234
|
+
quote = ch === "'" ? "single" : "double";
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const substitution = command.startsWith("$(", i)
|
|
238
|
+
? delimitedSubstitutionBody(command, i, "$(", ")")
|
|
239
|
+
: command.startsWith("<(", i)
|
|
240
|
+
? delimitedSubstitutionBody(command, i, "<(", ")")
|
|
241
|
+
: command.startsWith(">(", i)
|
|
242
|
+
? delimitedSubstitutionBody(command, i, ">(", ")")
|
|
243
|
+
: ch === "`"
|
|
244
|
+
? { body: command.slice(i + 1, command.indexOf("`", i + 1)), end: command.indexOf("`", i + 1) }
|
|
245
|
+
: undefined;
|
|
246
|
+
if (substitution && substitution.end >= 0) {
|
|
247
|
+
const nested = canonicalizeGuardCommand(substitution.body);
|
|
248
|
+
if (containsGuardedSubstitution(nested, depth + 1) ||
|
|
249
|
+
tokenizeShellCommand(nested).some((segment) => containsCommitOrPush(segment.tokens, depth + 1))) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
i = substitution.end;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
function containsCommitOrPush(tokens, depth) {
|
|
258
|
+
if (depth > 3 || tokens.length === 0)
|
|
259
|
+
return false;
|
|
260
|
+
let commandTokens = tokens;
|
|
261
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(commandTokens[0] ?? "")) {
|
|
262
|
+
commandTokens = commandTokens.slice(1);
|
|
263
|
+
}
|
|
264
|
+
if (commandTokens.length === 0)
|
|
265
|
+
return false;
|
|
266
|
+
const expandedHead = expandGuardVerbToken(commandTokens[0] ?? "");
|
|
267
|
+
if (expandedHead.length > 1) {
|
|
268
|
+
commandTokens = [...expandedHead, ...commandTokens.slice(1)];
|
|
269
|
+
}
|
|
270
|
+
if (isCommandStringWrapper(commandTokens[0] ?? "")) {
|
|
271
|
+
const lower = commandTokens.slice(1).map((token) => token.toLowerCase());
|
|
272
|
+
const runIndex = lower.findIndex((token) => token === "--run");
|
|
273
|
+
if (runIndex >= 0 && runIndex + 1 < commandTokens.length) {
|
|
274
|
+
const nestedCommand = commandTokens.slice(runIndex + 2).join(" ");
|
|
275
|
+
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsCommitOrPush(segment.tokens, depth + 1));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// These commands consume their following words as text/patterns; a bare
|
|
279
|
+
// git token in their arguments is not an indirect executable invocation.
|
|
280
|
+
// `$(git push)` is execution and must block; `"git push"` as literal text
|
|
281
|
+
// is allowed. Substitutions are screened before this text-consumer escape.
|
|
282
|
+
if (["echo", "printf", "grep"].includes(executableName(commandTokens[0] ?? ""))) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const gitIndex = commandTokens.findIndex((token) => isGitExecutable(token));
|
|
286
|
+
if (gitIndex >= 0) {
|
|
287
|
+
// Any non-leading git invocation is indirect. Do not maintain a wrapper
|
|
288
|
+
// or flag allowlist: unknown launchers are the security boundary here.
|
|
289
|
+
if (gitIndex > 0)
|
|
290
|
+
return true;
|
|
291
|
+
const gitTokens = commandTokens.slice(gitIndex);
|
|
292
|
+
let i = 1;
|
|
293
|
+
const takesValue = new Set([
|
|
294
|
+
"-C",
|
|
295
|
+
"-c",
|
|
296
|
+
"--config-env",
|
|
297
|
+
"--git-dir",
|
|
298
|
+
"--work-tree",
|
|
299
|
+
"--exec-path",
|
|
300
|
+
"--namespace",
|
|
301
|
+
]);
|
|
302
|
+
while (i < gitTokens.length && gitTokens[i].startsWith("-")) {
|
|
303
|
+
const option = gitTokens[i];
|
|
304
|
+
if (["--help", "-h", "--version", "-v", "-V"].includes(option))
|
|
305
|
+
return false;
|
|
306
|
+
if (option === "--")
|
|
307
|
+
return gitTokens[i + 1] === "commit" || gitTokens[i + 1] === "push";
|
|
308
|
+
if (["-C", "-c"].some((prefix) => option.startsWith(prefix) && option.length > prefix.length)) {
|
|
309
|
+
i += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (["--config-env", "--git-dir", "--work-tree", "--exec-path", "--namespace"].some((prefix) => option.startsWith(`${prefix}=`))) {
|
|
313
|
+
i += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
i += takesValue.has(option) ? 2 : 1;
|
|
317
|
+
}
|
|
318
|
+
const verbs = expandGuardVerbToken(gitTokens[i] ?? "");
|
|
319
|
+
return verbs.length === 1 && (verbs[0] === "commit" || verbs[0] === "push");
|
|
320
|
+
}
|
|
321
|
+
const leadingExecutable = commandTokens[0] ?? "";
|
|
322
|
+
const knownCommandStringWrapper = isShellWrapper(leadingExecutable) || isCommandStringWrapper(leadingExecutable);
|
|
323
|
+
if (!knownCommandStringWrapper) {
|
|
324
|
+
// Unknown launchers are a fail-closed boundary only when they explicitly
|
|
325
|
+
// accept a command string. Re-tokenizing the value keeps literal mentions
|
|
326
|
+
// such as `myprog -c "echo git push"` out of the guarded-command path.
|
|
327
|
+
const unknownSwitchIndex = commandTokens.slice(1).findIndex((token) => (token.startsWith("-") || token.startsWith("/")) && token.length > 1);
|
|
328
|
+
if (unknownSwitchIndex < 0)
|
|
329
|
+
return false;
|
|
330
|
+
const commandIndex = unknownSwitchIndex + 2;
|
|
331
|
+
if (commandIndex >= commandTokens.length)
|
|
332
|
+
return false;
|
|
333
|
+
const nestedCommand = commandTokens.slice(commandIndex).join(" ");
|
|
334
|
+
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsCommitOrPush(segment.tokens, depth + 1));
|
|
335
|
+
}
|
|
336
|
+
const lower = commandTokens.slice(1).map((token) => token.toLowerCase());
|
|
337
|
+
const switchIndex = lower.findIndex((token) => token === "-c" ||
|
|
338
|
+
token === "--run" ||
|
|
339
|
+
token === "-lc" ||
|
|
340
|
+
(/^-[^-]*c$/.test(token) && !token.startsWith("--")) ||
|
|
341
|
+
token === "/c" ||
|
|
342
|
+
token === "-command" ||
|
|
343
|
+
token === "-command:" ||
|
|
344
|
+
token === "-encodedcommand");
|
|
345
|
+
if (switchIndex < 0 || switchIndex + 1 >= commandTokens.length)
|
|
346
|
+
return false;
|
|
347
|
+
// Encoded PowerShell is intentionally unsupported: decoding it here would
|
|
348
|
+
// be a second shell/parser and could turn an ambiguous command into a false
|
|
349
|
+
// allow. Plain -Command/-c is safely handed back to the shared lexer.
|
|
350
|
+
if (lower[switchIndex] === "-encodedcommand")
|
|
351
|
+
return false;
|
|
352
|
+
// switchIndex is relative to commandTokens.slice(1), so +2 addresses the
|
|
353
|
+
// command token after the switch for both cmd and PowerShell. This also
|
|
354
|
+
// handles cmd options preceding /C (for example `/S /C`).
|
|
355
|
+
let commandIndex = switchIndex + 2;
|
|
356
|
+
if (commandTokens[commandIndex] === "--")
|
|
357
|
+
commandIndex += 1;
|
|
358
|
+
const nestedCommand = commandTokens.slice(commandIndex).join(" ");
|
|
359
|
+
return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsCommitOrPush(segment.tokens, depth + 1));
|
|
360
|
+
}
|
|
361
|
+
/** Analyze actual executable invocations, not substrings in shell text. */
|
|
11
362
|
export function isGitCommitOrPushAttempt(toolName, input) {
|
|
12
363
|
if (toolName !== "bash")
|
|
13
364
|
return false;
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
365
|
+
const command = getShellCommand(input);
|
|
366
|
+
if (!command)
|
|
16
367
|
return false;
|
|
17
|
-
|
|
368
|
+
const canonical = canonicalizeGuardCommand(command);
|
|
369
|
+
if (containsGuardedSubstitution(canonical, 0))
|
|
370
|
+
return true;
|
|
371
|
+
return tokenizeShellCommand(canonical).some((segment) => containsCommitOrPush(segment.tokens, 0));
|
|
372
|
+
}
|
|
373
|
+
function isTurnEndFindingsCache(value) {
|
|
374
|
+
if (!value || typeof value !== "object")
|
|
375
|
+
return false;
|
|
376
|
+
const record = value;
|
|
377
|
+
return (typeof record.content === "string" &&
|
|
378
|
+
typeof record.hasBlockers === "boolean" &&
|
|
379
|
+
Array.isArray(record.affectedFiles) &&
|
|
380
|
+
record.affectedFiles.every((file) => typeof file === "string") &&
|
|
381
|
+
typeof record.sessionId === "string" &&
|
|
382
|
+
typeof record.projectSeqStart === "number" &&
|
|
383
|
+
typeof record.projectSeqEnd === "number" &&
|
|
384
|
+
Number.isFinite(record.projectSeqStart) &&
|
|
385
|
+
Number.isFinite(record.projectSeqEnd) &&
|
|
386
|
+
!!record.fileSeqByPath &&
|
|
387
|
+
typeof record.fileSeqByPath === "object" &&
|
|
388
|
+
Object.values(record.fileSeqByPath).every((seq) => typeof seq === "number" && Number.isFinite(seq)) &&
|
|
389
|
+
!!record.fileContentHashes &&
|
|
390
|
+
typeof record.fileContentHashes === "object" &&
|
|
391
|
+
Object.values(record.fileContentHashes).every((hash) => typeof hash === "string") &&
|
|
392
|
+
(record.affectedFilesTruncated === undefined || typeof record.affectedFilesTruncated === "boolean") &&
|
|
393
|
+
(record.blockingFiles === undefined || Array.isArray(record.blockingFiles)));
|
|
394
|
+
}
|
|
395
|
+
function cacheRecord(cacheManager, cwd) {
|
|
396
|
+
const entry = cacheManager.readCache("turn-end-findings", cwd);
|
|
397
|
+
return entry && isTurnEndFindingsCache(entry.data) ? entry.data : undefined;
|
|
398
|
+
}
|
|
399
|
+
function markCacheUnknown(runtime, reason) {
|
|
400
|
+
runtime.markGitGuardCacheUnknown(reason);
|
|
401
|
+
}
|
|
402
|
+
/** Persist a complete, bounded, content-bound guard record. */
|
|
403
|
+
export function writeGitGuardRecord(cacheManager, runtime, cwd, record) {
|
|
404
|
+
const capped = capAffectedFiles(Array.isArray(record.affectedFiles) ? record.affectedFiles : [], cwd);
|
|
405
|
+
const fileSeqByPath = { ...(record.fileSeqByPath ?? {}) };
|
|
406
|
+
for (const file of capped.files) {
|
|
407
|
+
const key = guardPathKey(file, cwd);
|
|
408
|
+
if (fileSeqByPath[key] === undefined) {
|
|
409
|
+
fileSeqByPath[key] = runtime.getFileSeq(resolveGuardPath(file, cwd));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const currentProvenance = snapshotAdvisoryProvenance({
|
|
413
|
+
cwd,
|
|
414
|
+
runtime,
|
|
415
|
+
generation: 0,
|
|
416
|
+
files: capped.files.map((file) => ({ path: file, role: "affected" })),
|
|
417
|
+
truncated: capped.truncated,
|
|
418
|
+
});
|
|
419
|
+
const fileContentHashes = Object.fromEntries(currentProvenance.files.map((file) => [
|
|
420
|
+
guardPathKey(file.path, cwd),
|
|
421
|
+
file.sha256,
|
|
422
|
+
]));
|
|
423
|
+
const data = {
|
|
424
|
+
...record,
|
|
425
|
+
affectedFiles: capped.files,
|
|
426
|
+
affectedFilesTruncated: capped.truncated,
|
|
427
|
+
fileSeqByPath,
|
|
428
|
+
blockingFiles: Array.isArray(record.blockingFiles)
|
|
429
|
+
? capAffectedFiles(record.blockingFiles, cwd).files
|
|
430
|
+
: undefined,
|
|
431
|
+
fileContentHashes,
|
|
432
|
+
provenance: record.provenance ?? currentProvenance,
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
cacheManager.writeCache("turn-end-findings", data, cwd);
|
|
436
|
+
runtime.clearGitGuardCacheUnknown();
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
markCacheUnknown(runtime, "cache_write_failed");
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function logDecision(cwd, decision, reasonCategory, metadata = {}) {
|
|
445
|
+
logLatency({
|
|
446
|
+
type: "phase",
|
|
447
|
+
toolName: "git-guard",
|
|
448
|
+
filePath: cwd,
|
|
449
|
+
phase: "decision",
|
|
450
|
+
durationMs: 0,
|
|
451
|
+
result: decision,
|
|
452
|
+
metadata: { decision, reasonCategory, ...metadata },
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function unknown(cwd, reasonCategory, metadata = {}) {
|
|
456
|
+
logDecision(cwd, "unknown", reasonCategory, metadata);
|
|
457
|
+
return {
|
|
458
|
+
block: true,
|
|
459
|
+
unknown: true,
|
|
460
|
+
reason: `🔴 COMMIT BLOCKED (--lens-guard): blocker state is unknown (${reasonCategory}). Re-run pi-lens checks or start a fresh session, then retry.`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Reconcile the one persisted record after a per-file dispatch. This is called
|
|
465
|
+
* on tool_result, never tool_call, and therefore does not add disk I/O to the
|
|
466
|
+
* edit preflight path.
|
|
467
|
+
*/
|
|
468
|
+
export function syncGitGuardRecord(runtime, cacheManager, cwd, editedFilePath) {
|
|
469
|
+
const entries = runtime.getInlineBlockersSnapshot?.() ?? [];
|
|
470
|
+
const inspection = cacheManager.inspectCache("turn-end-findings", cwd);
|
|
471
|
+
const existing = cacheRecord(cacheManager, cwd);
|
|
472
|
+
if (existing &&
|
|
473
|
+
existing.sessionId !== runtime.telemetrySessionId &&
|
|
474
|
+
entries.length === 0) {
|
|
475
|
+
markCacheUnknown(runtime, "session_mismatch");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (!existing && inspection !== "missing" && entries.length === 0) {
|
|
479
|
+
markCacheUnknown(runtime, `cache_${inspection}`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const fileSeqByPath = {};
|
|
483
|
+
for (const [filePath, seq] of runtime.getFileSeqEntries?.() ?? []) {
|
|
484
|
+
fileSeqByPath[guardPathKey(filePath, cwd)] = seq;
|
|
485
|
+
}
|
|
486
|
+
const inlineFiles = entries.map((entry) => resolveGuardPath(entry.filePath, cwd));
|
|
487
|
+
const existingBlockingFiles = existing?.blockingFiles ?? [];
|
|
488
|
+
const provenanceComplete = existing?.blockerContent
|
|
489
|
+
? hasCompleteBlockingProvenance(existing.blockerContent, existingBlockingFiles, cwd)
|
|
490
|
+
: true;
|
|
491
|
+
if (existing?.blockerContent && !provenanceComplete) {
|
|
492
|
+
markCacheUnknown(runtime, "blocking_provenance_untrusted");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const editedKey = editedFilePath ? guardPathKey(editedFilePath, cwd) : undefined;
|
|
496
|
+
const testFiles = existing?.testFailureFiles ?? [];
|
|
497
|
+
const remainingBlockingFiles = editedKey && existingBlockingFiles.length > 0
|
|
498
|
+
? existingBlockingFiles.filter((file) => guardPathKey(file, cwd) !== editedKey)
|
|
499
|
+
: existingBlockingFiles;
|
|
500
|
+
let affectedFiles = [...(existing?.affectedFiles ?? []), ...inlineFiles];
|
|
501
|
+
if (!entries.length && editedKey && existing) {
|
|
502
|
+
const isStillTestFailure = testFiles.some((file) => guardPathKey(file, cwd) === editedKey);
|
|
503
|
+
const isUnknownBlockingPath = existingBlockingFiles.length === 0 && !!existing.blockerContent;
|
|
504
|
+
if (!isStillTestFailure && !isUnknownBlockingPath) {
|
|
505
|
+
affectedFiles = affectedFiles.filter((file) => guardPathKey(file, cwd) !== editedKey);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// A clean per-file dispatch is authoritative for that file. When the
|
|
509
|
+
// persisted record has explicit blocking-file provenance and the last such
|
|
510
|
+
// file just reconciled clean, retaining blockerContent would resurrect a
|
|
511
|
+
// stale blocker on every later git-guard lookup. Records without that
|
|
512
|
+
// provenance remain fail-closed: they cannot be safely cleared here.
|
|
513
|
+
const clearedLastKnownBlocker = !entries.length &&
|
|
514
|
+
!!editedKey &&
|
|
515
|
+
provenanceComplete &&
|
|
516
|
+
existingBlockingFiles.length > 0 &&
|
|
517
|
+
remainingBlockingFiles.length === 0;
|
|
518
|
+
const blockerContent = entries.length > 0
|
|
519
|
+
? entries.map((entry) => `${entry.filePath}: ${entry.summary}`).join("\n")
|
|
520
|
+
: clearedLastKnownBlocker
|
|
521
|
+
? undefined
|
|
522
|
+
: existing?.blockerContent;
|
|
523
|
+
const hasTestFailures = existing?.testFailures === true;
|
|
524
|
+
const hasBlockers = !!blockerContent || hasTestFailures;
|
|
525
|
+
const content = [blockerContent, hasTestFailures ? existing?.testFailureContent : undefined]
|
|
526
|
+
.filter((value) => !!value)
|
|
527
|
+
.join("\n\n");
|
|
528
|
+
if (!hasBlockers && !content) {
|
|
529
|
+
cacheManager.clearCache("turn-end-findings", cwd);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
writeGitGuardRecord(cacheManager, runtime, cwd, {
|
|
533
|
+
content: content || existing?.content || "",
|
|
534
|
+
blockerContent,
|
|
535
|
+
blockingFiles: entries.length > 0 ? inlineFiles : remainingBlockingFiles,
|
|
536
|
+
hasBlockers,
|
|
537
|
+
affectedFiles,
|
|
538
|
+
sessionId: runtime.telemetrySessionId,
|
|
539
|
+
projectSeqStart: runtime.turnStartProjectSeq,
|
|
540
|
+
projectSeqEnd: runtime.projectSeq,
|
|
541
|
+
fileSeqByPath,
|
|
542
|
+
fileContentHashes: {},
|
|
543
|
+
consumed: false,
|
|
544
|
+
testFailures: existing?.testFailures,
|
|
545
|
+
testFailureContent: existing?.testFailureContent,
|
|
546
|
+
testFailureFiles: existing?.testFailureFiles,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/** Add blocking test failures to the same turn-end record. */
|
|
550
|
+
export function mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, files) {
|
|
551
|
+
const existing = cacheRecord(cacheManager, cwd);
|
|
552
|
+
const fileSeqByPath = {};
|
|
553
|
+
for (const [filePath, seq] of runtime.getFileSeqEntries?.() ?? []) {
|
|
554
|
+
fileSeqByPath[guardPathKey(filePath, cwd)] = seq;
|
|
555
|
+
}
|
|
556
|
+
const failedFiles = files.map((file) => resolveGuardPath(file, cwd));
|
|
557
|
+
const blockerContent = existing?.blockerContent;
|
|
558
|
+
const testFailureFiles = [
|
|
559
|
+
...(existing?.testFailureFiles ?? []),
|
|
560
|
+
...failedFiles,
|
|
561
|
+
].filter((file, index, all) => all.findIndex((candidate) => guardPathKey(candidate, cwd) === guardPathKey(file, cwd)) === index);
|
|
562
|
+
writeGitGuardRecord(cacheManager, runtime, cwd, {
|
|
563
|
+
content: [blockerContent, content].filter(Boolean).join("\n\n"),
|
|
564
|
+
blockerContent,
|
|
565
|
+
blockingFiles: existing?.blockingFiles ?? [],
|
|
566
|
+
testFailureContent: content,
|
|
567
|
+
testFailureFiles: testFailureFiles,
|
|
568
|
+
hasBlockers: true,
|
|
569
|
+
affectedFiles: [...(existing?.affectedFiles ?? []), ...failedFiles],
|
|
570
|
+
sessionId: runtime.telemetrySessionId,
|
|
571
|
+
projectSeqStart: runtime.turnStartProjectSeq,
|
|
572
|
+
projectSeqEnd: runtime.projectSeq,
|
|
573
|
+
fileSeqByPath,
|
|
574
|
+
fileContentHashes: {},
|
|
575
|
+
consumed: false,
|
|
576
|
+
testFailures: true,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/** A passing test run resolves only its own previous test failures. */
|
|
580
|
+
export function clearGitGuardTestFailure(cacheManager, cwd, runtime, passedFiles = []) {
|
|
581
|
+
const existing = cacheRecord(cacheManager, cwd);
|
|
582
|
+
if (!existing?.testFailures)
|
|
583
|
+
return;
|
|
584
|
+
const passedKeys = new Set((passedFiles.length > 0 ? passedFiles : existing.testFailureFiles ?? []).map((file) => guardPathKey(file, cwd)));
|
|
585
|
+
const remainingFiles = (existing.testFailureFiles ?? []).filter((file) => !passedKeys.has(guardPathKey(file, cwd)));
|
|
586
|
+
const blockerContent = existing.blockerContent ?? "";
|
|
587
|
+
if (!blockerContent && remainingFiles.length === 0) {
|
|
588
|
+
cacheManager.clearCache("turn-end-findings", cwd);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const blockingKeys = new Set((existing.blockingFiles ?? []).map((file) => guardPathKey(file, cwd)));
|
|
592
|
+
const affectedFiles = existing.affectedFiles.filter((file) => blockingKeys.has(guardPathKey(file, cwd)) ||
|
|
593
|
+
remainingFiles.some((testFile) => guardPathKey(testFile, cwd) === guardPathKey(file, cwd)));
|
|
594
|
+
writeGitGuardRecord(cacheManager, runtime, cwd, {
|
|
595
|
+
...existing,
|
|
596
|
+
content: blockerContent,
|
|
597
|
+
blockerContent,
|
|
598
|
+
affectedFiles,
|
|
599
|
+
testFailures: remainingFiles.length > 0,
|
|
600
|
+
testFailureContent: remainingFiles.length > 0 ? existing.testFailureContent : undefined,
|
|
601
|
+
testFailureFiles: remainingFiles.length > 0 ? remainingFiles : undefined,
|
|
602
|
+
hasBlockers: !!blockerContent || remainingFiles.length > 0,
|
|
603
|
+
sessionId: runtime.telemetrySessionId,
|
|
604
|
+
projectSeqStart: runtime.turnStartProjectSeq,
|
|
605
|
+
projectSeqEnd: runtime.projectSeq,
|
|
606
|
+
fileSeqByPath: Object.fromEntries(runtime.getFileSeqEntries().map(([filePath, seq]) => [guardPathKey(filePath, cwd), seq])),
|
|
607
|
+
fileContentHashes: {},
|
|
608
|
+
});
|
|
18
609
|
}
|
|
19
610
|
export function evaluateGitGuard(runtime, cacheManager, cwd) {
|
|
611
|
+
if (runtime.gitGuardHasBlockers) {
|
|
612
|
+
logDecision(cwd, "blocked", "runtime_blockers", {
|
|
613
|
+
projectSeq: runtime.projectSeq,
|
|
614
|
+
});
|
|
615
|
+
const detail = runtime.gitGuardSummary ? `\n${runtime.gitGuardSummary}` : "";
|
|
616
|
+
return {
|
|
617
|
+
block: true,
|
|
618
|
+
reason: `🔴 COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before commit/push.${detail}\nRun lens_diagnostics mode=all for full details, then commit again.`,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (runtime.gitGuardCacheUnknownReason) {
|
|
622
|
+
return unknown(cwd, runtime.gitGuardCacheUnknownReason);
|
|
623
|
+
}
|
|
624
|
+
const inspection = cacheManager.inspectCache("turn-end-findings", cwd);
|
|
625
|
+
if (inspection === "missing") {
|
|
626
|
+
logDecision(cwd, "allowed", "no_record");
|
|
627
|
+
return { block: false };
|
|
628
|
+
}
|
|
629
|
+
if (inspection !== "fresh")
|
|
630
|
+
return unknown(cwd, `cache_${inspection}`);
|
|
20
631
|
const pending = cacheManager.readCache("turn-end-findings", cwd);
|
|
21
|
-
|
|
22
|
-
|
|
632
|
+
if (!pending || !isTurnEndFindingsCache(pending.data)) {
|
|
633
|
+
return unknown(cwd, "cache_malformed");
|
|
634
|
+
}
|
|
635
|
+
const record = pending.data;
|
|
636
|
+
if (!record.hasBlockers) {
|
|
637
|
+
logDecision(cwd, "allowed", "advisory_only");
|
|
638
|
+
return { block: false };
|
|
639
|
+
}
|
|
640
|
+
if (record.affectedFilesTruncated) {
|
|
641
|
+
return unknown(cwd, "affected_files_truncated");
|
|
642
|
+
}
|
|
643
|
+
if (record.sessionId !== runtime.telemetrySessionId) {
|
|
644
|
+
return unknown(cwd, "session_mismatch");
|
|
645
|
+
}
|
|
646
|
+
if (record.projectSeqEnd !== runtime.projectSeq) {
|
|
647
|
+
return unknown(cwd, "project_sequence_mismatch", {
|
|
648
|
+
recordedProjectSeq: record.projectSeqEnd,
|
|
649
|
+
currentProjectSeq: runtime.projectSeq,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
const liveFiles = [];
|
|
653
|
+
for (const file of record.affectedFiles) {
|
|
654
|
+
const resolved = resolveGuardPath(file, cwd);
|
|
655
|
+
const key = guardPathKey(resolved, cwd);
|
|
656
|
+
const recordedSeq = record.fileSeqByPath[key];
|
|
657
|
+
if (recordedSeq === undefined ||
|
|
658
|
+
recordedSeq !== (runtime.getFileSeq?.(resolved) ?? 0)) {
|
|
659
|
+
return unknown(cwd, "file_sequence_mismatch", { file: resolved });
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
if (!isPathIgnoredByProject(resolved, runtime.projectRoot || cwd, false)) {
|
|
663
|
+
const currentHash = currentFileFingerprint(resolved);
|
|
664
|
+
if (currentHash !== "missing") {
|
|
665
|
+
liveFiles.push(resolved);
|
|
666
|
+
const expectedHash = record.fileContentHashes[key];
|
|
667
|
+
if (!expectedHash ||
|
|
668
|
+
expectedHash.startsWith("unreadable:") ||
|
|
669
|
+
currentHash.startsWith("unreadable:") ||
|
|
670
|
+
expectedHash !== currentHash) {
|
|
671
|
+
return unknown(cwd, "file_content_changed", { file: resolved });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
return unknown(cwd, "file_unreadable", { file: resolved });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (record.hasBlockers && record.affectedFiles.length === 0) {
|
|
681
|
+
return unknown(cwd, "blocker_without_affected_file");
|
|
682
|
+
}
|
|
683
|
+
// A deleted/ignored affected file has been resolved; no stale blocker is
|
|
684
|
+
// allowed to survive solely because its old path remains in the record.
|
|
685
|
+
if (record.hasBlockers && liveFiles.length === 0 && record.affectedFiles.length > 0) {
|
|
686
|
+
logDecision(cwd, "allowed", "affected_files_resolved", {
|
|
687
|
+
projectSeq: record.projectSeqEnd,
|
|
688
|
+
});
|
|
23
689
|
return { block: false };
|
|
24
690
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
:
|
|
691
|
+
logDecision(cwd, "blocked", "cache_blockers", {
|
|
692
|
+
projectSeq: record.projectSeqEnd,
|
|
693
|
+
affectedFileCount: record.affectedFiles.length,
|
|
694
|
+
});
|
|
28
695
|
return {
|
|
29
696
|
block: true,
|
|
30
|
-
reason:
|
|
697
|
+
reason: "🔴 COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before commit/push.\nRun lens_diagnostics mode=all for full details, then commit again.",
|
|
31
698
|
};
|
|
32
699
|
}
|