@herbertgao/pi-extensions 2026.9.6 → 2026.9.7
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 +3 -8
- package/THIRD_PARTY_NOTICES.md +0 -4
- package/node_modules/@narumitw/pi-btw/README.md +2 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +52 -53
- package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
- package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
- package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
- package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
- package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
- package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
- package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
- package/node_modules/pi-lens/CHANGELOG.md +239 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
- package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
- package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
- package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
- package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
- package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
- package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
- package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
- package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
- package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
- package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
- package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
- package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
- package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
- package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
- package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
- package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
- package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
- package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
- package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
- package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
- package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
- package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
- package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
- package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
- package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
- package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
- package/node_modules/pi-lens/dist/index.js +27416 -24153
- package/node_modules/pi-lens/dist/mcp/server.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
- package/node_modules/pi-lens/docs/audit1.md +5 -5
- package/node_modules/pi-lens/docs/dependencies.md +11 -0
- package/node_modules/pi-lens/docs/dispositions.md +52 -0
- package/node_modules/pi-lens/docs/environment-variables.md +2 -2
- package/node_modules/pi-lens/docs/features.md +4 -4
- package/node_modules/pi-lens/docs/globalconfig.md +3 -1
- package/node_modules/pi-lens/docs/language-coverage.md +1 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
- package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
- package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
- package/node_modules/pi-lens/docs/settings.md +7 -4
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
- package/node_modules/pi-lens/docs/usage.md +2 -0
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
- package/package.json +5 -11
- package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
- package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
- package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
- package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
- package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
- package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
- package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
- package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
- package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
- package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
- package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
- package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
- package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
- package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
- package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
- package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
- package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
- package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
- package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
- package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
- package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
- package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
- package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
- package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
- package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
- package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
- package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
|
@@ -12,25 +12,29 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Same shape as `ledger-bounds.ts` (#2426) for the degradation ledger's bound.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Callers that need results use the result-returning overload; callers that
|
|
16
|
+
* only need completion use the void overload. Keeping both forms here prevents
|
|
17
|
+
* per-caller worker-pool copies from drifting.
|
|
17
18
|
*/
|
|
18
|
-
|
|
19
|
-
* Run `mapper` over `items` with at most `concurrency` in flight at once.
|
|
20
|
-
*/
|
|
21
|
-
export async function mapWithConcurrency(items, concurrency, mapper) {
|
|
19
|
+
export async function mapWithConcurrency(items, concurrency, mapper, signal) {
|
|
22
20
|
if (items.length === 0)
|
|
23
|
-
return;
|
|
21
|
+
return [];
|
|
24
22
|
let nextIndex = 0;
|
|
25
23
|
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
24
|
+
const results = new Map();
|
|
26
25
|
const worker = async () => {
|
|
27
26
|
while (true) {
|
|
27
|
+
if (signal?.aborted)
|
|
28
|
+
return;
|
|
28
29
|
const index = nextIndex++;
|
|
29
30
|
if (index >= items.length)
|
|
30
31
|
return;
|
|
31
|
-
await mapper(items[index]);
|
|
32
|
+
results.set(index, (await mapper(items[index])));
|
|
32
33
|
}
|
|
33
34
|
};
|
|
34
35
|
const workers = Array.from({ length: workerCount }, () => worker());
|
|
35
36
|
await Promise.all(workers);
|
|
37
|
+
return [...results.entries()]
|
|
38
|
+
.sort(([left], [right]) => left - right)
|
|
39
|
+
.map(([, result]) => result);
|
|
36
40
|
}
|
|
@@ -31,6 +31,8 @@ import { handleSessionStart } from "../runtime-session.js";
|
|
|
31
31
|
import { handleTurnEnd } from "../runtime-turn.js";
|
|
32
32
|
import { createMcpHost } from "./host-shim.js";
|
|
33
33
|
import { startSituationalToolTelemetrySession } from "../situational-tool-telemetry.js";
|
|
34
|
+
import { bounded } from "../deadline-utils.js";
|
|
35
|
+
import { HOOK_WALL_BUDGET_MS } from "../hook-budgets.js";
|
|
34
36
|
let contextPromise;
|
|
35
37
|
let resolvedRuntime;
|
|
36
38
|
/**
|
|
@@ -73,17 +75,20 @@ function joinMessages(consumed) {
|
|
|
73
75
|
return undefined;
|
|
74
76
|
return consumed.messages.map((message) => message.content).join("\n\n");
|
|
75
77
|
}
|
|
78
|
+
const degradedSessionStart = {
|
|
79
|
+
aliveLspClients: 0,
|
|
80
|
+
};
|
|
76
81
|
/**
|
|
77
82
|
* Run pi-lens's real session_start. Much of the work (scans, baseline, LSP warm)
|
|
78
83
|
* runs in the background, so the immediate return carries the synchronous
|
|
79
84
|
* guidance + whatever baseline/LSP state is ready; query `pilens_diagnostics` /
|
|
80
85
|
* `pilens_health` afterwards for the scan results as they land.
|
|
81
86
|
*/
|
|
82
|
-
|
|
87
|
+
async function runSessionStartImpl(cwd) {
|
|
83
88
|
startSituationalToolTelemetrySession("mcp");
|
|
84
89
|
const ctx = await getMcpSessionContext();
|
|
85
90
|
const host = createMcpHost(undefined, cwd);
|
|
86
|
-
await handleSessionStart({
|
|
91
|
+
await bounded(handleSessionStart({
|
|
87
92
|
ctxCwd: cwd,
|
|
88
93
|
// The MCP server has no TUI keystroke latency to protect, so the
|
|
89
94
|
// first-call-quick heuristic must not apply. Force "full" mode so
|
|
@@ -108,16 +113,33 @@ export async function runSessionStart(cwd) {
|
|
|
108
113
|
cleanStaleTsBuildInfo: () => [],
|
|
109
114
|
resetDispatchBaselines: (cwdArg) => resetDispatchBaselines(cwdArg ?? cwd),
|
|
110
115
|
resetLSPService,
|
|
116
|
+
}), {
|
|
117
|
+
ms: HOOK_WALL_BUDGET_MS.session_start,
|
|
118
|
+
signal: undefined,
|
|
119
|
+
hook: "session_start",
|
|
120
|
+
label: "handleSessionStart",
|
|
111
121
|
});
|
|
112
122
|
const baseline = ctx.runtime.errorDebtBaseline;
|
|
113
123
|
return {
|
|
114
|
-
guidance: joinMessages(consumeSessionStartGuidance(ctx.cacheManager, cwd)),
|
|
124
|
+
guidance: joinMessages(consumeSessionStartGuidance(ctx.cacheManager, cwd, "mcp")),
|
|
115
125
|
errorDebtBaseline: baseline
|
|
116
126
|
? { testsPassed: baseline.testsPassed, buildPassed: baseline.buildPassed }
|
|
117
127
|
: undefined,
|
|
118
128
|
aliveLspClients: getLSPService().getAliveClientCount(),
|
|
119
129
|
};
|
|
120
130
|
}
|
|
131
|
+
export function runSessionStart(cwd) {
|
|
132
|
+
return bounded(runSessionStartImpl(cwd), {
|
|
133
|
+
ms: HOOK_WALL_BUDGET_MS.session_start,
|
|
134
|
+
signal: undefined,
|
|
135
|
+
hook: "session_start",
|
|
136
|
+
label: "mcp-session-start",
|
|
137
|
+
}).then((outcome) => outcome ?? degradedSessionStart);
|
|
138
|
+
}
|
|
139
|
+
const degradedTurnEnd = {
|
|
140
|
+
outcome: { filesRegistered: 0 },
|
|
141
|
+
commit: () => { },
|
|
142
|
+
};
|
|
121
143
|
/**
|
|
122
144
|
* #1274: the delivery map used to be unbounded — every timed-out client and
|
|
123
145
|
* every distinct raw-cwd alias left an entry for the life of the process.
|
|
@@ -193,7 +215,7 @@ const inFlightIpcTurnEnds = new Map();
|
|
|
193
215
|
* reads edited files from turn-state, so we register the caller-supplied files
|
|
194
216
|
* first (a full-file range, importsChanged=true so dep/knip re-check broadly).
|
|
195
217
|
*/
|
|
196
|
-
async function
|
|
218
|
+
async function runTurnEndNowImpl(cwd, files = [], deferredDelivery = false) {
|
|
197
219
|
const ctx = await getMcpSessionContext();
|
|
198
220
|
const host = createMcpHost(undefined, cwd);
|
|
199
221
|
let registered = 0;
|
|
@@ -216,6 +238,7 @@ async function runTurnEndNow(cwd, files = [], deferredDelivery = false) {
|
|
|
216
238
|
lastSeen: new Date().toISOString(),
|
|
217
239
|
};
|
|
218
240
|
await handleTurnEnd({
|
|
241
|
+
signal: undefined,
|
|
219
242
|
ctxCwd: cwd,
|
|
220
243
|
getFlag: host.getFlag,
|
|
221
244
|
dbg: noop,
|
|
@@ -225,6 +248,7 @@ async function runTurnEndNow(cwd, files = [], deferredDelivery = false) {
|
|
|
225
248
|
deadCodeClients: ctx.clients.deadCodeClients,
|
|
226
249
|
depChecker: ctx.clients.depChecker,
|
|
227
250
|
testRunnerClient: ctx.clients.testRunnerClient,
|
|
251
|
+
host: "mcp",
|
|
228
252
|
owner,
|
|
229
253
|
resetLSPService,
|
|
230
254
|
resetFormatService,
|
|
@@ -250,6 +274,14 @@ async function runTurnEndNow(cwd, files = [], deferredDelivery = false) {
|
|
|
250
274
|
},
|
|
251
275
|
};
|
|
252
276
|
}
|
|
277
|
+
function runTurnEndNow(cwd, files = [], deferredDelivery = false) {
|
|
278
|
+
return bounded(runTurnEndNowImpl(cwd, files, deferredDelivery), {
|
|
279
|
+
ms: HOOK_WALL_BUDGET_MS.turn_end,
|
|
280
|
+
signal: undefined,
|
|
281
|
+
hook: "turn_end",
|
|
282
|
+
label: "mcp-turn-end",
|
|
283
|
+
}).then((transaction) => transaction ?? degradedTurnEnd);
|
|
284
|
+
}
|
|
253
285
|
/**
|
|
254
286
|
* Run a normal MCP turn_end. Its caller owns the tool response, so the
|
|
255
287
|
* historical consume-on-return behavior remains unchanged.
|
|
@@ -189,8 +189,17 @@ export function resetSinkRotations() {
|
|
|
189
189
|
state.rotationCount = 0;
|
|
190
190
|
state.rotationFailures = 0;
|
|
191
191
|
state.rotationRetryAfterMs = 0;
|
|
192
|
+
state.optionConflictRecorded = false;
|
|
192
193
|
}
|
|
193
194
|
}
|
|
195
|
+
/** Pure read-time view for the degradation ledger; never logs through this sink. */
|
|
196
|
+
export function getSinkOptionConflicts() {
|
|
197
|
+
if (!ndjsonGlobalState)
|
|
198
|
+
return [];
|
|
199
|
+
return [...ndjsonGlobalState.writers.values()]
|
|
200
|
+
.filter((state) => state.optionConflictRecorded)
|
|
201
|
+
.map((state) => ({ file: state.file }));
|
|
202
|
+
}
|
|
194
203
|
function requireCurrentGlobalState() {
|
|
195
204
|
if (ndjsonGlobalState)
|
|
196
205
|
return ndjsonGlobalState;
|
|
@@ -213,12 +222,6 @@ function registerWriter(state) {
|
|
|
213
222
|
function normalizeLogPath(file) {
|
|
214
223
|
return normalizeFilePath(path.resolve(file));
|
|
215
224
|
}
|
|
216
|
-
function assertCompatibleWriterOptions(existing, maxBytes, backupPath) {
|
|
217
|
-
if (existing.maxBytes === maxBytes && existing.backupPath === backupPath)
|
|
218
|
-
return;
|
|
219
|
-
throw new Error(`createNdjsonLogger: incompatible options for shared path ${existing.file}; ` +
|
|
220
|
-
`the first writer's maxBytes/backupPath must be reused`);
|
|
221
|
-
}
|
|
222
225
|
function writeQueueItemSync(state, item) {
|
|
223
226
|
if (item.kind === "truncate") {
|
|
224
227
|
fs.writeFileSync(state.file, "");
|
|
@@ -298,7 +301,6 @@ function createWriterState(file, maxBytes, backupPath) {
|
|
|
298
301
|
if (existing) {
|
|
299
302
|
// A partially initialized global state from another graph still needs to
|
|
300
303
|
// be enrolled, but it must never get a second queue or exit flusher.
|
|
301
|
-
assertCompatibleWriterOptions(existing, maxBytes, backupPath);
|
|
302
304
|
if (!exitFlushers.has(existing.exitFlusher))
|
|
303
305
|
registerWriter(existing);
|
|
304
306
|
// A state adopted from a pre-#1970 module graph predates this field.
|
|
@@ -318,6 +320,20 @@ function createWriterState(file, maxBytes, backupPath) {
|
|
|
318
320
|
existing.rotationFailures = 0;
|
|
319
321
|
if (typeof existing.rotationRetryAfterMs !== "number")
|
|
320
322
|
existing.rotationRetryAfterMs = 0;
|
|
323
|
+
if (typeof existing.optionConflictRecorded !== "boolean")
|
|
324
|
+
existing.optionConflictRecorded = false;
|
|
325
|
+
const optionsDiffer = existing.maxBytes !== maxBytes || existing.backupPath !== backupPath;
|
|
326
|
+
if (optionsDiffer) {
|
|
327
|
+
// A pre-#2505 graph can leave this writer unbounded. Adopt the incoming
|
|
328
|
+
// bound so reload restores rotation instead of preserving an unsafe gap.
|
|
329
|
+
if (existing.maxBytes === undefined && maxBytes !== undefined)
|
|
330
|
+
existing.maxBytes = maxBytes;
|
|
331
|
+
if (existing.backupPath === undefined && backupPath !== undefined)
|
|
332
|
+
existing.backupPath = backupPath;
|
|
333
|
+
// A rotation-policy disagreement must never abort extension loading.
|
|
334
|
+
// The ledger folds this bounded, per-path fact into health at read time.
|
|
335
|
+
existing.optionConflictRecorded = true;
|
|
336
|
+
}
|
|
321
337
|
return existing;
|
|
322
338
|
}
|
|
323
339
|
const state = {};
|
|
@@ -337,6 +353,7 @@ function createWriterState(file, maxBytes, backupPath) {
|
|
|
337
353
|
state.rotationCount = 0;
|
|
338
354
|
state.rotationFailures = 0;
|
|
339
355
|
state.rotationRetryAfterMs = 0;
|
|
356
|
+
state.optionConflictRecorded = false;
|
|
340
357
|
globalState.writers.set(file, state);
|
|
341
358
|
registerWriter(state);
|
|
342
359
|
return state;
|
|
@@ -717,6 +717,7 @@ export async function settleObservedMutation(args) {
|
|
|
717
717
|
return {
|
|
718
718
|
settled: false,
|
|
719
719
|
changedPaths: [],
|
|
720
|
+
unverifiablePaths: [],
|
|
720
721
|
replayed: 0,
|
|
721
722
|
scanned: 0,
|
|
722
723
|
stoppedEarly: false,
|
|
@@ -730,6 +731,7 @@ export async function settleObservedMutation(args) {
|
|
|
730
731
|
return {
|
|
731
732
|
settled: false,
|
|
732
733
|
changedPaths: [],
|
|
734
|
+
unverifiablePaths: [],
|
|
733
735
|
replayed: 0,
|
|
734
736
|
scanned: 0,
|
|
735
737
|
stoppedEarly: false,
|
|
@@ -744,6 +746,7 @@ export async function settleObservedMutation(args) {
|
|
|
744
746
|
return {
|
|
745
747
|
settled: false,
|
|
746
748
|
changedPaths: [],
|
|
749
|
+
unverifiablePaths: [],
|
|
747
750
|
replayed: 0,
|
|
748
751
|
scanned: 0,
|
|
749
752
|
stoppedEarly: false,
|
|
@@ -771,6 +774,7 @@ export async function settleObservedMutation(args) {
|
|
|
771
774
|
return {
|
|
772
775
|
settled: false,
|
|
773
776
|
changedPaths: [],
|
|
777
|
+
unverifiablePaths: [],
|
|
774
778
|
replayed: 0,
|
|
775
779
|
scanned: 0,
|
|
776
780
|
stoppedEarly: true,
|
|
@@ -779,7 +783,9 @@ export async function settleObservedMutation(args) {
|
|
|
779
783
|
}
|
|
780
784
|
const captured = capture.value;
|
|
781
785
|
seedLedger(captured.snapshot);
|
|
782
|
-
const
|
|
786
|
+
const diff = diffObservedStats(pending.stats, captured.snapshot);
|
|
787
|
+
const changed = diff.changed.filter((candidate) => args.isRecordable?.(candidate) !== false);
|
|
788
|
+
const unverifiablePaths = diff.unverifiable.filter((candidate) => args.isRecordable?.(candidate) !== false);
|
|
783
789
|
// Two different ways the observation can fall short of what the tool named:
|
|
784
790
|
// the ARM already truncated a wide directory (#2449 round 3, S3), or the
|
|
785
791
|
// re-capture's own deadline cut its tail. Both mean the same thing to the
|
|
@@ -790,6 +796,19 @@ export async function settleObservedMutation(args) {
|
|
|
790
796
|
: captured.stoppedEarly
|
|
791
797
|
? "capture-cut-short"
|
|
792
798
|
: undefined;
|
|
799
|
+
if (unverifiablePaths.length > 0) {
|
|
800
|
+
// Hashless evidence is a bounded coverage gap, not a mutation. Keep it
|
|
801
|
+
// visible through the existing degradation record without replaying it.
|
|
802
|
+
noteObservedUnverifiable(args.toolName);
|
|
803
|
+
logLatency({
|
|
804
|
+
type: "phase",
|
|
805
|
+
toolName: args.toolName,
|
|
806
|
+
phase: "observed_mutation_coverage_unknown",
|
|
807
|
+
filePath: unverifiablePaths.slice(0, 5).join(","),
|
|
808
|
+
durationMs: Date.now() - started,
|
|
809
|
+
result: `unverifiable:${unverifiablePaths.length}`,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
793
812
|
if (changed.length === 0) {
|
|
794
813
|
// An INCOMPLETE observation is not evidence of cleanliness — it is
|
|
795
814
|
// evidence we stopped looking. Advancing the clean latch on it would
|
|
@@ -797,11 +816,12 @@ export async function settleObservedMutation(args) {
|
|
|
797
816
|
// with a directory wider than the cap that is every single call.
|
|
798
817
|
if (truncated)
|
|
799
818
|
noteObservedUnverifiable(args.toolName);
|
|
800
|
-
else
|
|
819
|
+
else if (unverifiablePaths.length === 0)
|
|
801
820
|
noteObservedClean(args.toolName);
|
|
802
821
|
return {
|
|
803
822
|
settled: true,
|
|
804
823
|
changedPaths: [],
|
|
824
|
+
unverifiablePaths,
|
|
805
825
|
replayed: 0,
|
|
806
826
|
scanned: captured.snapshot.size,
|
|
807
827
|
stoppedEarly: truncated,
|
|
@@ -860,12 +880,30 @@ export async function settleObservedMutation(args) {
|
|
|
860
880
|
return {
|
|
861
881
|
settled: true,
|
|
862
882
|
changedPaths: changed,
|
|
883
|
+
unverifiablePaths,
|
|
863
884
|
replayed,
|
|
864
885
|
scanned: captured.snapshot.size,
|
|
865
886
|
stoppedEarly: truncated,
|
|
866
887
|
reason: cutReason,
|
|
867
888
|
};
|
|
868
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* The observational net asks whether to look harder. An absent content hash
|
|
892
|
+
* therefore remains unverifiable even when cheap stat fields report a change.
|
|
893
|
+
* #2984: stat-only evidence must not grant ownership or replay a mutation.
|
|
894
|
+
*/
|
|
895
|
+
function diffObservedStats(before, after) {
|
|
896
|
+
const changed = new Set(diffFileStats(before, after));
|
|
897
|
+
const unverifiable = new Set();
|
|
898
|
+
for (const [key, stat] of after) {
|
|
899
|
+
const previous = before.get(key);
|
|
900
|
+
if (previous && (previous.hash === undefined || stat.hash === undefined)) {
|
|
901
|
+
changed.delete(key);
|
|
902
|
+
unverifiable.add(key);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return { changed: [...changed], unverifiable: [...unverifiable] };
|
|
906
|
+
}
|
|
869
907
|
async function readBytesSafe(filePath) {
|
|
870
908
|
try {
|
|
871
909
|
return await fs.promises.readFile(filePath);
|
|
@@ -124,6 +124,9 @@ export async function captureFileStatsForPaths(files, options = {}) {
|
|
|
124
124
|
return { snapshot, stoppedEarly };
|
|
125
125
|
}
|
|
126
126
|
export async function captureFileStats(root, options = {}) {
|
|
127
|
+
if (options.forcedUnknownReason !== undefined) {
|
|
128
|
+
return { unknownReason: options.forcedUnknownReason, scannedCount: 0 };
|
|
129
|
+
}
|
|
127
130
|
const budgetMs = options.budgetMs ?? 50;
|
|
128
131
|
try {
|
|
129
132
|
const walk = await collectSourceFilesWithBudgetAsync(root, {
|
|
@@ -150,13 +153,12 @@ export async function captureFileStats(root, options = {}) {
|
|
|
150
153
|
}
|
|
151
154
|
/** Paths added or whose size/mtime changed. Deletions are NOT reported. */
|
|
152
155
|
export function diffFileStats(before, after) {
|
|
156
|
+
const contentChanged = new Set(diffFileContent(before, after));
|
|
153
157
|
const changed = [];
|
|
154
158
|
for (const [key, stat] of after) {
|
|
155
159
|
const prev = before.get(key);
|
|
156
160
|
// Content confirm: same mtime tick + same size but different bytes.
|
|
157
|
-
const contentConfirm =
|
|
158
|
-
stat.hash !== undefined &&
|
|
159
|
-
prev.hash !== stat.hash;
|
|
161
|
+
const contentConfirm = contentChanged.has(key);
|
|
160
162
|
if (!prev ||
|
|
161
163
|
prev.mtimeMs !== stat.mtimeMs ||
|
|
162
164
|
prev.size !== stat.size ||
|
|
@@ -166,6 +168,24 @@ export function diffFileStats(before, after) {
|
|
|
166
168
|
}
|
|
167
169
|
return changed;
|
|
168
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Return paths confirmed different by hashes, plus files absent before and
|
|
173
|
+
* present after. A missing baseline entry is evidence of creation; a missing
|
|
174
|
+
* hash on an existing entry remains unknown.
|
|
175
|
+
*/
|
|
176
|
+
export function diffFileContent(before, after) {
|
|
177
|
+
const changed = [];
|
|
178
|
+
for (const [key, stat] of after) {
|
|
179
|
+
const previous = before.get(key);
|
|
180
|
+
if (previous === undefined ||
|
|
181
|
+
(previous.hash !== undefined &&
|
|
182
|
+
stat.hash !== undefined &&
|
|
183
|
+
previous.hash !== stat.hash)) {
|
|
184
|
+
changed.push(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return changed;
|
|
188
|
+
}
|
|
169
189
|
export class OpaqueBaselineStore {
|
|
170
190
|
byCwd = new Map();
|
|
171
191
|
evictions = 0;
|
|
@@ -54,7 +54,9 @@ import * as os from "node:os";
|
|
|
54
54
|
import * as path from "node:path";
|
|
55
55
|
import { mkdtempSync } from "node:fs";
|
|
56
56
|
import { resolveOpengrepConfig } from "./opengrep-config.js";
|
|
57
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
57
58
|
import { getScratchTreeDirNames } from "./scratch-tree-policy.js";
|
|
59
|
+
import { realpathOrResolve } from "./path-utils.js";
|
|
58
60
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
59
61
|
import { SecurityScanClient } from "./security-scan-client.js";
|
|
60
62
|
const EMPTY_RESULT = {
|
|
@@ -92,12 +94,13 @@ export class OpengrepClient extends SecurityScanClient {
|
|
|
92
94
|
* opengrep process (mirrors `GitleaksClient`/`JscpdClient`).
|
|
93
95
|
*/
|
|
94
96
|
async scan(cwd) {
|
|
95
|
-
const targetDir =
|
|
97
|
+
const targetDir = realpathOrResolve(cwd);
|
|
96
98
|
const scannedAt = new Date().toISOString();
|
|
97
99
|
if (!(await this.ensureAvailable())) {
|
|
98
100
|
return {
|
|
99
101
|
...EMPTY_RESULT,
|
|
100
102
|
scannedAt,
|
|
103
|
+
reason: "not-installed",
|
|
101
104
|
summary: "opengrep not installed",
|
|
102
105
|
};
|
|
103
106
|
}
|
|
@@ -134,33 +137,76 @@ export class OpengrepClient extends SecurityScanClient {
|
|
|
134
137
|
], { cwd, timeout: SCAN_TIMEOUT_MS });
|
|
135
138
|
if (result.error) {
|
|
136
139
|
this.log(`Scan error: ${result.error.message}`);
|
|
140
|
+
const reason = `${result.failure ?? "spawn-failed"}: ${result.error.message}`;
|
|
141
|
+
this.recordRefusal(cwd, reason, result.status);
|
|
137
142
|
return {
|
|
138
143
|
...EMPTY_RESULT,
|
|
139
144
|
scannedAt,
|
|
140
|
-
|
|
145
|
+
reason: "spawn-failed",
|
|
146
|
+
summary: reason.slice(0, 200),
|
|
141
147
|
};
|
|
142
148
|
}
|
|
143
149
|
if (!fs.existsSync(reportPath)) {
|
|
150
|
+
const reason = (result.stderr ?? "").trim().split("\n")[0] || "no report produced";
|
|
151
|
+
this.recordRefusal(cwd, reason, result.status);
|
|
144
152
|
return {
|
|
145
153
|
...EMPTY_RESULT,
|
|
146
154
|
scannedAt,
|
|
147
|
-
|
|
155
|
+
reason: "no-report",
|
|
156
|
+
summary: reason,
|
|
148
157
|
};
|
|
149
158
|
}
|
|
150
|
-
const
|
|
159
|
+
const raw = fs.readFileSync(reportPath, "utf-8");
|
|
160
|
+
const report = readOpengrepReport(raw);
|
|
161
|
+
if (report.verdict === "refused" || result.status !== 0) {
|
|
162
|
+
const reason = report.verdict === "refused"
|
|
163
|
+
? report.reason
|
|
164
|
+
: `opengrep exited with status ${result.status}`;
|
|
165
|
+
this.recordRefusal(cwd, reason, result.status);
|
|
166
|
+
return {
|
|
167
|
+
...EMPTY_RESULT,
|
|
168
|
+
scannedAt,
|
|
169
|
+
reason: "refused",
|
|
170
|
+
summary: reason.slice(0, 200),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (report.partial) {
|
|
174
|
+
recordDegradationOnce({
|
|
175
|
+
kind: "opengrep-partial-scan",
|
|
176
|
+
subject: cwd,
|
|
177
|
+
reason: report.partial.reason,
|
|
178
|
+
metadata: { status: result.status },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
151
181
|
// #2154: the one opengrep site that parsed a scan of this root.
|
|
152
182
|
return {
|
|
153
183
|
success: true,
|
|
154
|
-
|
|
155
|
-
|
|
184
|
+
// A warning-level partial report with no scanned paths proves that
|
|
185
|
+
// the producer went cold. A complete empty report still proves a
|
|
186
|
+
// genuine scan, so it remains authoritative for the id-only fallback.
|
|
187
|
+
analyzed: !report.partial || report.scanned.length > 0,
|
|
188
|
+
...(report.scanned.length > 0
|
|
189
|
+
? {
|
|
190
|
+
analyzedFiles: report.scanned.map((file) => realpathOrResolve(path.resolve(cwd, file))),
|
|
191
|
+
}
|
|
192
|
+
: {}),
|
|
193
|
+
...(report.partial ? { partial: true } : {}),
|
|
194
|
+
...(report.partial ? { summary: report.partial.reason } : {}),
|
|
195
|
+
...(report.partial && report.scanned.length === 0
|
|
196
|
+
? { reason: "partial-no-paths" }
|
|
197
|
+
: {}),
|
|
198
|
+
findings: report.findings,
|
|
156
199
|
scannedAt,
|
|
157
200
|
};
|
|
158
201
|
}
|
|
159
202
|
catch (err) {
|
|
203
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
204
|
+
this.recordRefusal(cwd, reason, undefined);
|
|
160
205
|
return {
|
|
161
206
|
...EMPTY_RESULT,
|
|
162
207
|
scannedAt,
|
|
163
|
-
|
|
208
|
+
reason: "crashed",
|
|
209
|
+
summary: reason.slice(0, 200),
|
|
164
210
|
};
|
|
165
211
|
}
|
|
166
212
|
finally {
|
|
@@ -172,6 +218,53 @@ export class OpengrepClient extends SecurityScanClient {
|
|
|
172
218
|
}
|
|
173
219
|
}
|
|
174
220
|
}
|
|
221
|
+
recordRefusal(cwd, reason, status) {
|
|
222
|
+
recordDegradationOnce({
|
|
223
|
+
kind: "opengrep-scan-refused",
|
|
224
|
+
subject: cwd,
|
|
225
|
+
reason,
|
|
226
|
+
metadata: { status },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function readOpengrepReport(raw) {
|
|
231
|
+
let parsed;
|
|
232
|
+
try {
|
|
233
|
+
parsed = JSON.parse(raw);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return { verdict: "refused", reason: "unparseable opengrep report" };
|
|
237
|
+
}
|
|
238
|
+
if (!parsed || typeof parsed !== "object")
|
|
239
|
+
return { verdict: "refused", reason: "unparseable opengrep report" };
|
|
240
|
+
const report = parsed;
|
|
241
|
+
if (!Array.isArray(report.results) && !Array.isArray(report.errors))
|
|
242
|
+
return { verdict: "refused", reason: "unparseable opengrep report" };
|
|
243
|
+
const errors = (Array.isArray(report.errors) ? report.errors : []).map((error) => {
|
|
244
|
+
if (typeof error === "string")
|
|
245
|
+
return { message: error };
|
|
246
|
+
if (!error || typeof error !== "object")
|
|
247
|
+
return { message: "unrecognised opengrep error entry" };
|
|
248
|
+
const entry = error;
|
|
249
|
+
const level = typeof entry.level === "string" ? entry.level : undefined;
|
|
250
|
+
const message = typeof entry.message === "string" ? entry.message : undefined;
|
|
251
|
+
return {
|
|
252
|
+
...(level ? { level } : {}),
|
|
253
|
+
message: message ?? level ?? "unrecognised opengrep error entry",
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
const refusal = errors.find((error) => error.level !== "warn");
|
|
257
|
+
if (refusal)
|
|
258
|
+
return { verdict: "refused", reason: refusal.message };
|
|
259
|
+
const scanned = Array.isArray(report.paths?.scanned)
|
|
260
|
+
? report.paths.scanned.filter((file) => typeof file === "string")
|
|
261
|
+
: [];
|
|
262
|
+
return {
|
|
263
|
+
verdict: "usable",
|
|
264
|
+
...(errors[0] ? { partial: { reason: errors[0].message } } : {}),
|
|
265
|
+
findings: parseOpengrepReport(parsed),
|
|
266
|
+
scanned,
|
|
267
|
+
};
|
|
175
268
|
}
|
|
176
269
|
// --- Parser ---
|
|
177
270
|
/**
|
|
@@ -185,15 +278,17 @@ export class OpengrepClient extends SecurityScanClient {
|
|
|
185
278
|
* CLI surface has drifted in places, e.g. `--files-with-matches` requires
|
|
186
279
|
* `--experimental` where semgrep's doesn't).
|
|
187
280
|
*/
|
|
188
|
-
export function parseOpengrepReport(
|
|
189
|
-
if (!
|
|
190
|
-
return [];
|
|
191
|
-
let parsed;
|
|
192
|
-
try {
|
|
193
|
-
parsed = JSON.parse(raw);
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
281
|
+
export function parseOpengrepReport(rawOrParsed) {
|
|
282
|
+
if (typeof rawOrParsed === "string" && !rawOrParsed.trim())
|
|
196
283
|
return [];
|
|
284
|
+
let parsed = rawOrParsed;
|
|
285
|
+
if (typeof rawOrParsed === "string") {
|
|
286
|
+
try {
|
|
287
|
+
parsed = JSON.parse(rawOrParsed);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
197
292
|
}
|
|
198
293
|
if (!parsed || typeof parsed !== "object")
|
|
199
294
|
return [];
|