@herbertgao/pi-extensions 2026.8.5 → 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 +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +5 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- 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/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +24 -17
- package/node_modules/@narumitw/pi-btw/package.json +5 -5
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +33 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +95 -0
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/biome-client.js +121 -13
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +135 -30
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +19 -7
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +6 -4
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +114 -10
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +101 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -4
- 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/runner-helpers.js +230 -50
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +296 -3
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
- package/node_modules/pi-lens/dist/clients/git-guard.js +18 -21
- package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +116 -7
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +4 -9
- package/node_modules/pi-lens/dist/clients/knip-client.js +51 -14
- package/node_modules/pi-lens/dist/clients/latency-logger.js +50 -1
- package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +58 -13
- package/node_modules/pi-lens/dist/clients/lsp/client.js +169 -8
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +29 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +264 -66
- package/node_modules/pi-lens/dist/clients/lsp/server.js +247 -60
- 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/mcp/analyze.js +4 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +30 -17
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +29 -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/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +153 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +104 -11
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +170 -22
- package/node_modules/pi-lens/dist/clients/runtime-session.js +28 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +124 -10
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +388 -35
- package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +88 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +141 -23
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +431 -24
- package/node_modules/pi-lens/dist/clients/tool-policy.js +2 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
- package/node_modules/pi-lens/dist/index.js +4827 -1633
- package/node_modules/pi-lens/dist/mcp/server.js +8 -2
- 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/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +3 -2
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +62 -7
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +38 -13
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +11 -3
- package/node_modules/pi-lens/docs/features.md +14 -1
- package/node_modules/pi-lens/docs/globalconfig.md +11 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
- package/node_modules/pi-lens/docs/settings.md +6 -0
- package/node_modules/pi-lens/docs/usage.md +23 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +1 -1
- 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-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/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-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/scripts/analyze-pi-lens-logs.mjs +79 -0
- 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 +13 -0
- package/node_modules/pi-mcp-adapter/README.md +4 -1
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +18 -15
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +18 -8
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +5 -0
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
- package/node_modules/pi-mcp-adapter/types.ts +93 -10
- 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 +9 -9
|
@@ -14,9 +14,32 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
16
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
17
|
+
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
17
18
|
export class SecurityScanClient {
|
|
18
19
|
toolName;
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Availability memo, backed by the shared transient-aware latch (#1467).
|
|
22
|
+
*
|
|
23
|
+
* Assigning `false` still means "durable: this machine does not have the
|
|
24
|
+
* tool" — every existing subclass write keeps its meaning. A probe that
|
|
25
|
+
* merely timed out must go through `markTransientlyUnavailable` instead, so
|
|
26
|
+
* it expires and the tool can come back without a host restart.
|
|
27
|
+
*/
|
|
28
|
+
availabilityLatch = createAvailabilityLatch();
|
|
29
|
+
get available() {
|
|
30
|
+
return this.availabilityLatch.read();
|
|
31
|
+
}
|
|
32
|
+
set available(value) {
|
|
33
|
+
if (value === true)
|
|
34
|
+
this.availabilityLatch.noteAvailable();
|
|
35
|
+
else if (value === false)
|
|
36
|
+
this.availabilityLatch.noteUnavailable("missing", "not-found");
|
|
37
|
+
else
|
|
38
|
+
this.availabilityLatch.reset();
|
|
39
|
+
}
|
|
40
|
+
/** Outcome/cause of the most recent `probeVersion` call. */
|
|
41
|
+
lastProbeOutcome = null;
|
|
42
|
+
lastProbeCause = null;
|
|
20
43
|
ensureInFlight = null;
|
|
21
44
|
inFlight = new Map();
|
|
22
45
|
binaryPath = null;
|
|
@@ -51,18 +74,71 @@ export class SecurityScanClient {
|
|
|
51
74
|
}
|
|
52
75
|
/**
|
|
53
76
|
* Spawn `toolName <versionArgs>` and report whether it answered cleanly.
|
|
54
|
-
* Does NOT mutate `this.available` — callers decide what a hit/miss means
|
|
77
|
+
* Does NOT mutate `this.available` — callers decide what a hit/miss means —
|
|
78
|
+
* but it does classify the failure (`lastProbeOutcome`/`lastProbeCause`) and
|
|
79
|
+
* emit one availability-decision record, so a probe that keeps timing out is
|
|
80
|
+
* visible in latency.log rather than inferred from silence (#1467).
|
|
55
81
|
*/
|
|
56
82
|
async probeVersion(versionArgs) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
const sampler = startHostStallSampler();
|
|
84
|
+
const startedAt = Date.now();
|
|
85
|
+
let probe;
|
|
86
|
+
let hostStallMs;
|
|
87
|
+
try {
|
|
88
|
+
probe = await safeSpawnAsync(this.toolName, versionArgs, {
|
|
89
|
+
timeout: 5000,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
hostStallMs = sampler.stop();
|
|
94
|
+
}
|
|
95
|
+
const elapsedMs = Date.now() - startedAt;
|
|
60
96
|
if (!probe.error && probe.status === 0) {
|
|
97
|
+
this.lastProbeOutcome = "success";
|
|
98
|
+
this.lastProbeCause = "ok";
|
|
61
99
|
this.log(`${this.toolName} found: ${probe.stdout.trim().split("\n")[0]}`);
|
|
100
|
+
logAvailabilityDecision({
|
|
101
|
+
tool: this.toolName,
|
|
102
|
+
verdict: "available",
|
|
103
|
+
outcome: "success",
|
|
104
|
+
cause: "ok",
|
|
105
|
+
elapsedMs,
|
|
106
|
+
latched: true,
|
|
107
|
+
hostStallMs,
|
|
108
|
+
budgetMs: 5000,
|
|
109
|
+
});
|
|
62
110
|
return true;
|
|
63
111
|
}
|
|
112
|
+
const { outcome, cause } = classifyProbeFailure(probe, { hostStallMs });
|
|
113
|
+
this.lastProbeOutcome = outcome;
|
|
114
|
+
this.lastProbeCause = cause;
|
|
115
|
+
logAvailabilityDecision({
|
|
116
|
+
tool: this.toolName,
|
|
117
|
+
verdict: "unavailable",
|
|
118
|
+
outcome,
|
|
119
|
+
cause,
|
|
120
|
+
elapsedMs,
|
|
121
|
+
latched: outcome !== "transient",
|
|
122
|
+
hostStallMs,
|
|
123
|
+
budgetMs: 5000,
|
|
124
|
+
});
|
|
64
125
|
return false;
|
|
65
126
|
}
|
|
127
|
+
/** True when the last probe failed for a reason that is not the tool's fault. */
|
|
128
|
+
probeWasTransient() {
|
|
129
|
+
return this.lastProbeOutcome === "transient";
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Record a non-durable unavailability: the verdict expires after a cooldown
|
|
133
|
+
* and the next `ensureAvailable` re-probes.
|
|
134
|
+
*
|
|
135
|
+
* Returns that cooldown in ms so the caller can put it in its decision
|
|
136
|
+
* record. A latch you can read in `latency.log` without the retry schedule
|
|
137
|
+
* beside it only tells you the tool is off, not when it comes back.
|
|
138
|
+
*/
|
|
139
|
+
markTransientlyUnavailable(cause = "probe-timeout") {
|
|
140
|
+
return this.availabilityLatch.noteUnavailable("transient", cause);
|
|
141
|
+
}
|
|
66
142
|
/**
|
|
67
143
|
* Standard availability path for the GitHub-release tools (gitleaks, trivy):
|
|
68
144
|
* PATH probe first, then fall back to the pi-lens installer's `ensureTool`.
|
|
@@ -73,6 +149,13 @@ export class SecurityScanClient {
|
|
|
73
149
|
this.available = true;
|
|
74
150
|
return true;
|
|
75
151
|
}
|
|
152
|
+
if (this.probeWasTransient()) {
|
|
153
|
+
// A timed-out probe is not evidence the tool is absent, so it must not
|
|
154
|
+
// trigger an install NOR latch a permanent `false`.
|
|
155
|
+
this.log(`${this.toolName} availability probe timed out; not installing, will retry`);
|
|
156
|
+
this.markTransientlyUnavailable(this.lastProbeCause ?? "probe-timeout");
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
76
159
|
this.log(`${this.toolName} not found, attempting auto-install`);
|
|
77
160
|
const { ensureTool } = await import("./installer/index.js");
|
|
78
161
|
const installed = await ensureTool(this.toolName);
|
|
@@ -12,6 +12,7 @@ import { getSgCommand, resolveManagedToolClient, } from "./dispatch/runners/util
|
|
|
12
12
|
import { getProjectIgnoreGlobs } from "./file-utils.js";
|
|
13
13
|
import { findGlobalBinary } from "./package-manager.js";
|
|
14
14
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
15
|
+
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
15
16
|
/**
|
|
16
17
|
* Build the `bash -c` argv that runs `cmd` with `allArgs` as POSITIONAL
|
|
17
18
|
* parameters. The script is the constant `"$0" "$@"`, so bash re-emits the
|
|
@@ -33,6 +34,8 @@ function sgExcludeArgsForProject(rootDir) {
|
|
|
33
34
|
]);
|
|
34
35
|
}
|
|
35
36
|
const DEFAULT_EXEC_TIMEOUT_MS = 30_000;
|
|
37
|
+
/** Budget for a single `--version` candidate probe, ms. */
|
|
38
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
36
39
|
const MAX_SG_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
37
40
|
/**
|
|
38
41
|
* Format metavariable captures for display below a match line.
|
|
@@ -94,8 +97,31 @@ export class SgRunner {
|
|
|
94
97
|
log;
|
|
95
98
|
sgPath = null;
|
|
96
99
|
sgArgsPrefix = [];
|
|
97
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Availability memo, backed by the shared transient-aware latch (#1476).
|
|
102
|
+
*
|
|
103
|
+
* Before this, one failed sweep over every candidate — timeouts included —
|
|
104
|
+
* latched `false` for the life of the process AND ran a full auto-install
|
|
105
|
+
* first. A host stall during warm-up therefore both disabled ast-grep until
|
|
106
|
+
* restart and paid for an install nobody needed.
|
|
107
|
+
*/
|
|
108
|
+
availabilityLatch = createAvailabilityLatch();
|
|
98
109
|
ensureInFlight = null;
|
|
110
|
+
/**
|
|
111
|
+
* Whether a DIRECT candidate — one that would have been ast-grep itself —
|
|
112
|
+
* failed for a transient reason in the current sweep. Only these block the
|
|
113
|
+
* install: a slow `npx` says nothing about whether ast-grep is on this
|
|
114
|
+
* machine, and letting it block turned "ast-grep is genuinely absent on a
|
|
115
|
+
* slow host" into "ast-grep is never installed and npx is re-spawned every
|
|
116
|
+
* sweep, forever".
|
|
117
|
+
*/
|
|
118
|
+
sweepSawTransient = false;
|
|
119
|
+
sweepTransientCause = "probe-timeout";
|
|
120
|
+
/** A transient on the npx fallback: not evidence, but not nothing either. */
|
|
121
|
+
sweepFallbackTransient = false;
|
|
122
|
+
sweepFallbackCause = "probe-timeout";
|
|
123
|
+
/** Host stall summed over every probe of the current sweep, ms. */
|
|
124
|
+
sweepHostStallMs = 0;
|
|
99
125
|
constructor(verbose = false) {
|
|
100
126
|
this.log = verbose
|
|
101
127
|
? createSubsystemLogger("sg-runner")
|
|
@@ -110,9 +136,11 @@ export class SgRunner {
|
|
|
110
136
|
* `KnipClient.ensureAvailable` and `DependencyChecker.ensureAvailable`.
|
|
111
137
|
*/
|
|
112
138
|
async ensureAvailable() {
|
|
113
|
-
// Fast path: already
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
// Fast path: already decided. `read()` returns null when the last verdict
|
|
140
|
+
// was transient and its cooldown expired, which re-enters the sweep.
|
|
141
|
+
const memo = this.availabilityLatch.read();
|
|
142
|
+
if (memo !== null)
|
|
143
|
+
return memo;
|
|
116
144
|
if (this.ensureInFlight)
|
|
117
145
|
return this.ensureInFlight;
|
|
118
146
|
this.ensureInFlight = this.doEnsureAvailable();
|
|
@@ -124,18 +152,26 @@ export class SgRunner {
|
|
|
124
152
|
}
|
|
125
153
|
}
|
|
126
154
|
async doEnsureAvailable() {
|
|
155
|
+
const startedAt = Date.now();
|
|
156
|
+
this.sweepSawTransient = false;
|
|
157
|
+
this.sweepTransientCause = "probe-timeout";
|
|
158
|
+
this.sweepFallbackTransient = false;
|
|
159
|
+
this.sweepFallbackCause = "probe-timeout";
|
|
160
|
+
this.sweepHostStallMs = 0;
|
|
127
161
|
// Step 1: PATH — canonical binary names + npx fallback.
|
|
128
162
|
// Prefer ast-grep over sg on Linux: /usr/bin/sg is util-linux, not ast-grep.
|
|
129
163
|
const pathCommand = await this.probeCommandCandidates([
|
|
130
164
|
{ cmd: "ast-grep", argsPrefix: [] },
|
|
131
165
|
{ cmd: "sg", argsPrefix: [] },
|
|
132
|
-
|
|
166
|
+
// `npx --no -- ast-grep` starts a Node process before it can answer, so
|
|
167
|
+
// it is the candidate most likely to blow a 5 s budget on a cold or busy
|
|
168
|
+
// box. Marked a fallback so its timeout cannot veto the install.
|
|
169
|
+
{ cmd: "npx", argsPrefix: ["--no", "--", "ast-grep"], fallback: true },
|
|
133
170
|
]);
|
|
134
171
|
if (pathCommand) {
|
|
135
172
|
this.sgPath = pathCommand.cmd;
|
|
136
173
|
this.sgArgsPrefix = pathCommand.argsPrefix;
|
|
137
|
-
this.
|
|
138
|
-
this.log(`ast-grep found on PATH: ${pathCommand.cmd}`);
|
|
174
|
+
this.noteAvailable(startedAt, `ast-grep found on PATH: ${pathCommand.cmd}`);
|
|
139
175
|
return true;
|
|
140
176
|
}
|
|
141
177
|
// Step 1b: any package manager's global bin dir (npm/pnpm/yarn/bun).
|
|
@@ -147,8 +183,7 @@ export class SgRunner {
|
|
|
147
183
|
if (globalBin && (await this.probeCommand(globalBin, []))) {
|
|
148
184
|
this.sgPath = globalBin;
|
|
149
185
|
this.sgArgsPrefix = [];
|
|
150
|
-
this.
|
|
151
|
-
this.log(`ast-grep found in global bin: ${globalBin}`);
|
|
186
|
+
this.noteAvailable(startedAt, `ast-grep found in global bin: ${globalBin}`);
|
|
152
187
|
return true;
|
|
153
188
|
}
|
|
154
189
|
}
|
|
@@ -159,8 +194,7 @@ export class SgRunner {
|
|
|
159
194
|
if (platformBinary) {
|
|
160
195
|
this.sgPath = platformBinary;
|
|
161
196
|
this.sgArgsPrefix = [];
|
|
162
|
-
this.
|
|
163
|
-
this.log(`ast-grep found via platform package: ${platformBinary}`);
|
|
197
|
+
this.noteAvailable(startedAt, `ast-grep found via platform package: ${platformBinary}`);
|
|
164
198
|
return true;
|
|
165
199
|
}
|
|
166
200
|
// Step 3: Homebrew (macOS only).
|
|
@@ -169,11 +203,24 @@ export class SgRunner {
|
|
|
169
203
|
if (brewBinary) {
|
|
170
204
|
this.sgPath = brewBinary;
|
|
171
205
|
this.sgArgsPrefix = [];
|
|
172
|
-
this.
|
|
173
|
-
this.log(`ast-grep found via Homebrew: ${brewBinary}`);
|
|
206
|
+
this.noteAvailable(startedAt, `ast-grep found via Homebrew: ${brewBinary}`);
|
|
174
207
|
return true;
|
|
175
208
|
}
|
|
176
209
|
}
|
|
210
|
+
// A timeout on a DIRECT candidate means the machine answered, not the tool
|
|
211
|
+
// (#1476). Installing ast-grep because the host event loop stalled is a
|
|
212
|
+
// heavyweight reaction to a hiccup, and latching the result disabled the
|
|
213
|
+
// tool until restart. Retry the sweep later instead.
|
|
214
|
+
//
|
|
215
|
+
// The npx fallback is deliberately NOT part of this test. Gating the
|
|
216
|
+
// install on it regressed the very host this change targets: a slow box
|
|
217
|
+
// with no ast-grep timed out `npx --no -- ast-grep` every sweep, so the
|
|
218
|
+
// install below was never reached and the slow npx was re-spawned on each
|
|
219
|
+
// escalating retry instead — worse than the latch it replaced.
|
|
220
|
+
if (this.sweepSawTransient) {
|
|
221
|
+
this.log("ast-grep availability probe timed out; will retry (not installing)");
|
|
222
|
+
return this.noteUnavailable(startedAt, "transient", this.sweepTransientCause);
|
|
223
|
+
}
|
|
177
224
|
// Step 4: install via the typed shared seam, then validate the returned
|
|
178
225
|
// absolute binary before publishing it.
|
|
179
226
|
const installed = await resolveManagedToolClient({
|
|
@@ -185,11 +232,51 @@ export class SgRunner {
|
|
|
185
232
|
if (installed.outcome === "success") {
|
|
186
233
|
this.sgPath = installed.value;
|
|
187
234
|
this.sgArgsPrefix = [];
|
|
188
|
-
this.
|
|
189
|
-
this.log(`ast-grep auto-installed: ${installed.value}`);
|
|
235
|
+
this.noteAvailable(startedAt, `ast-grep auto-installed: ${installed.value}`);
|
|
190
236
|
return true;
|
|
191
237
|
}
|
|
192
|
-
|
|
238
|
+
// The install failed AND the npx fallback never got a fair hearing, so
|
|
239
|
+
// "ast-grep is not installed" is not a fact yet. Expire the verdict.
|
|
240
|
+
if (this.sweepFallbackTransient) {
|
|
241
|
+
return this.noteUnavailable(startedAt, "transient", this.sweepFallbackCause);
|
|
242
|
+
}
|
|
243
|
+
return this.noteUnavailable(startedAt, "missing", "not-found");
|
|
244
|
+
}
|
|
245
|
+
/** Record a successful sweep: available, latched, one decision record. */
|
|
246
|
+
noteAvailable(startedAt, message) {
|
|
247
|
+
this.availabilityLatch.noteAvailable();
|
|
248
|
+
this.log(message);
|
|
249
|
+
logAvailabilityDecision({
|
|
250
|
+
tool: "ast-grep",
|
|
251
|
+
verdict: "available",
|
|
252
|
+
outcome: "success",
|
|
253
|
+
cause: "ok",
|
|
254
|
+
elapsedMs: Date.now() - startedAt,
|
|
255
|
+
latched: true,
|
|
256
|
+
hostStallMs: this.sweepHostStallMs,
|
|
257
|
+
budgetMs: PROBE_TIMEOUT_MS,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Record a failed sweep. A `transient` outcome expires after a bounded
|
|
262
|
+
* cooldown; anything else is remembered for the session, as before. The
|
|
263
|
+
* elapsed/stall numbers describe the WHOLE sweep, because the verdict is
|
|
264
|
+
* about the sweep rather than any single candidate — reporting zero here
|
|
265
|
+
* would erase the evidence that cracked #1467.
|
|
266
|
+
*/
|
|
267
|
+
noteUnavailable(startedAt, outcome, cause) {
|
|
268
|
+
const retryAfterMs = this.availabilityLatch.noteUnavailable(outcome, cause);
|
|
269
|
+
logAvailabilityDecision({
|
|
270
|
+
tool: "ast-grep",
|
|
271
|
+
verdict: "unavailable",
|
|
272
|
+
outcome,
|
|
273
|
+
cause,
|
|
274
|
+
elapsedMs: Date.now() - startedAt,
|
|
275
|
+
latched: outcome !== "transient",
|
|
276
|
+
hostStallMs: this.sweepHostStallMs,
|
|
277
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
278
|
+
budgetMs: PROBE_TIMEOUT_MS,
|
|
279
|
+
});
|
|
193
280
|
return false;
|
|
194
281
|
}
|
|
195
282
|
/**
|
|
@@ -270,17 +357,48 @@ export class SgRunner {
|
|
|
270
357
|
isAstGrepVersionOutput(output) {
|
|
271
358
|
return /\bast[- ]grep\b/i.test(output);
|
|
272
359
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Probe one candidate. A failure is CLASSIFIED, not merely counted: a
|
|
362
|
+
* timeout/abort marks the whole sweep transient so the caller retries later
|
|
363
|
+
* instead of installing and latching. A command that answers but is not
|
|
364
|
+
* ast-grep (Linux `/usr/bin/sg` is util-linux) is a durable no.
|
|
365
|
+
*/
|
|
366
|
+
async probeCommand(cmd, argsPrefix, fallback = false) {
|
|
367
|
+
// Host-side budget: measure the loop stall that overlapped the window so
|
|
368
|
+
// the classifier can tell "the tool is slow" from "the host was busy".
|
|
369
|
+
const sampler = startHostStallSampler();
|
|
370
|
+
let result;
|
|
371
|
+
let hostStallMs;
|
|
372
|
+
try {
|
|
373
|
+
result = await safeSpawnAsync(cmd, [...argsPrefix, "--version"], {
|
|
374
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
hostStallMs = sampler.stop();
|
|
379
|
+
this.sweepHostStallMs += hostStallMs;
|
|
380
|
+
}
|
|
381
|
+
if (!result.error &&
|
|
278
382
|
result.status === 0 &&
|
|
279
|
-
this.isAstGrepVersionOutput(`${result.stdout}\n${result.stderr}`))
|
|
383
|
+
this.isAstGrepVersionOutput(`${result.stdout}\n${result.stderr}`)) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const { outcome, cause } = classifyProbeFailure(result, { hostStallMs });
|
|
387
|
+
if (outcome === "transient") {
|
|
388
|
+
if (fallback) {
|
|
389
|
+
this.sweepFallbackTransient = true;
|
|
390
|
+
this.sweepFallbackCause = cause;
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
this.sweepSawTransient = true;
|
|
394
|
+
this.sweepTransientCause = cause;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
280
398
|
}
|
|
281
399
|
async probeCommandCandidates(candidates) {
|
|
282
400
|
for (const candidate of candidates) {
|
|
283
|
-
if (await this.probeCommand(candidate.cmd, candidate.argsPrefix)) {
|
|
401
|
+
if (await this.probeCommand(candidate.cmd, candidate.argsPrefix, candidate.fallback ?? false)) {
|
|
284
402
|
return candidate;
|
|
285
403
|
}
|
|
286
404
|
}
|
|
@@ -24,14 +24,19 @@
|
|
|
24
24
|
* alongside.
|
|
25
25
|
*
|
|
26
26
|
* The tail scan is bounded, but the tail can contain prior-session rows. The
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
27
|
+
* `session_start` and `turn_end` callers supply the in-process session
|
|
28
|
+
* boundary (`runtime.sessionStartedAt`), and rows are admitted only when
|
|
29
|
+
* their own UTC timestamp is at or after that boundary. `/lens-health` has
|
|
30
|
+
* no per-session caller to anchor to (it can run at any point, including
|
|
31
|
+
* outside a turn) so it omits `sessionStartMs` and falls back to
|
|
32
|
+
* `SMELLS_ROLLING_WINDOW_MS` (24h) — its rendered line is labeled "last 24h
|
|
33
|
+
* tail-scan" to say so explicitly rather than implying a session-scoped
|
|
34
|
+
* count it isn't. This keeps the same bounded read without reporting
|
|
35
|
+
* historical failures as current, so this module does not additionally
|
|
36
|
+
* instrument the write call sites (`bus-publish.ts`, `clients/lsp/index.ts`'s
|
|
37
|
+
* respawn path) with parallel live counters. That keeps the change contained
|
|
38
|
+
* to one new module + three small call sites (session_start, `/lens-health`,
|
|
39
|
+
* `turn_end`) instead of touching every producer of the two source logs.
|
|
35
40
|
*
|
|
36
41
|
* Smells covered (deliberately a SUBSET — the two the issue named as having
|
|
37
42
|
* gone unnoticed; the full catalogue stays in the manual analyzer):
|
|
@@ -53,6 +58,7 @@ import * as path from "node:path";
|
|
|
53
58
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
54
59
|
/** Bounded tail-read budget PER source log file — never a full-file scan. */
|
|
55
60
|
export const SMELLS_TAIL_BYTES_PER_FILE = 64 * 1024;
|
|
61
|
+
export const SMELLS_ROLLING_WINDOW_MS = 24 * 60 * 60_000;
|
|
56
62
|
/** Re-check cadence at `turn_end`, mirroring `memory-sampler.ts`'s pattern —
|
|
57
63
|
* cheap enough (bounded ~128KB I/O) not to need finer throttling, but a
|
|
58
64
|
* fixed interval keeps it from running on every single turn. */
|
|
@@ -154,11 +160,12 @@ function isOpengrepRespawn(entry) {
|
|
|
154
160
|
* up to ~10MB) source files are. `root` is injectable for tests.
|
|
155
161
|
*/
|
|
156
162
|
export function countRecentSmells(root = getGlobalPiLensDir(), sessionStartMs) {
|
|
163
|
+
const sinceMs = sessionStartMs ?? Date.now() - SMELLS_ROLLING_WINDOW_MS;
|
|
157
164
|
const busTail = tailReadText(path.join(root, "bus-events.log"), SMELLS_TAIL_BYTES_PER_FILE);
|
|
158
165
|
const latencyTail = tailReadText(path.join(root, "latency.log"), SMELLS_TAIL_BYTES_PER_FILE);
|
|
159
166
|
return {
|
|
160
|
-
staleCtxEmitFailed: countMatchingLines(busTail, isStaleCtxEmitFailed,
|
|
161
|
-
opengrepRespawn: countMatchingLines(latencyTail, isOpengrepRespawn,
|
|
167
|
+
staleCtxEmitFailed: countMatchingLines(busTail, isStaleCtxEmitFailed, sinceMs),
|
|
168
|
+
opengrepRespawn: countMatchingLines(latencyTail, isOpengrepRespawn, sinceMs),
|
|
162
169
|
};
|
|
163
170
|
}
|
|
164
171
|
/**
|
|
@@ -186,7 +193,7 @@ export function formatSmellsSessionStartLine(counts) {
|
|
|
186
193
|
* everything is below threshold).
|
|
187
194
|
*/
|
|
188
195
|
export function formatSmellsHealthLine(counts) {
|
|
189
|
-
return (`Smells (
|
|
196
|
+
return (`Smells (last 24h tail-scan): stale-ctx emit_failed=${counts.staleCtxEmitFailed}` +
|
|
190
197
|
` · opengrep respawn=${counts.opengrepRespawn}`);
|
|
191
198
|
}
|
|
192
199
|
/** `true` on turn 20, 40, 60, ... — never turn 0. Pure so the cadence is
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
|
-
import
|
|
2
|
+
// #1434 S3d: import the constant from the side-effect-free module, not from
|
|
3
|
+
// `console-guard-install.ts` — that module installs the console guard as an
|
|
4
|
+
// import side effect, so importing it here (for a single constant, from a
|
|
5
|
+
// module measuring load time) would have silently installed the guard
|
|
6
|
+
// wherever this file is imported, including test-only measurement code that
|
|
7
|
+
// has no business flipping a global switch.
|
|
8
|
+
import { PI_LENS_EVAL_STARTED_MS } from "./eval-timestamp.js";
|
|
3
9
|
/**
|
|
4
10
|
* Startup timing for pi-lens.
|
|
5
11
|
*
|