zeitzeuge 0.8.1 → 0.9.0
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/dist/cli.js +660 -88
- package/package.json +9 -8
package/dist/cli.js
CHANGED
|
@@ -125,6 +125,7 @@ class TodoProgressRenderer {
|
|
|
125
125
|
totalTodos = 0;
|
|
126
126
|
completedTodos = 0;
|
|
127
127
|
subagentTodos = new Map;
|
|
128
|
+
pendingAutoTasks = new Set;
|
|
128
129
|
canAnimate;
|
|
129
130
|
constructor(spinner, { animate = true } = {}) {
|
|
130
131
|
this.spinner = spinner;
|
|
@@ -234,11 +235,13 @@ class TodoProgressRenderer {
|
|
|
234
235
|
}
|
|
235
236
|
const toolCalls = extractToolCallsFromStreamChunk(chunk);
|
|
236
237
|
if (toolCalls && toolCalls.length > 0) {
|
|
238
|
+
const newlyDispatched = [];
|
|
237
239
|
for (const tc of toolCalls) {
|
|
238
240
|
if (!isSubagent && tc.name === "task") {
|
|
239
241
|
const subagentType = tc.args.subagent_type;
|
|
240
242
|
if (typeof subagentType === "string" && !this.dispatchedSubagents.includes(subagentType)) {
|
|
241
243
|
this.dispatchedSubagents.push(subagentType);
|
|
244
|
+
newlyDispatched.push(subagentType);
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
247
|
if (tc.name === "write_todos") {
|
|
@@ -247,6 +250,11 @@ class TodoProgressRenderer {
|
|
|
247
250
|
if (Array.isArray(todos2)) {
|
|
248
251
|
const displayName = this.resolveSubagentName(nsKey, meta?.namespace);
|
|
249
252
|
this.handleSubagentTodos(todos2, nsKey, displayName);
|
|
253
|
+
const autoNsKey = `auto:${displayName}`;
|
|
254
|
+
if (this.subagentTodos.has(autoNsKey)) {
|
|
255
|
+
this.subagentTodos.delete(autoNsKey);
|
|
256
|
+
this.pendingAutoTasks.delete(displayName);
|
|
257
|
+
}
|
|
250
258
|
}
|
|
251
259
|
}
|
|
252
260
|
continue;
|
|
@@ -276,6 +284,21 @@ class TodoProgressRenderer {
|
|
|
276
284
|
this.persistLine(" ", pc.dim(label));
|
|
277
285
|
}
|
|
278
286
|
}
|
|
287
|
+
for (const name of newlyDispatched) {
|
|
288
|
+
const autoNsKey = `auto:${name}`;
|
|
289
|
+
this.handleSubagentTodos([{ content: "analyzing", status: "in_progress" }], autoNsKey, name);
|
|
290
|
+
this.pendingAutoTasks.add(name);
|
|
291
|
+
}
|
|
292
|
+
if (!isSubagent && this.pendingAutoTasks.size > 0) {
|
|
293
|
+
const hasNonTaskCalls = toolCalls.some((tc) => tc.name !== "task" && tc.name !== "write_todos");
|
|
294
|
+
if (hasNonTaskCalls) {
|
|
295
|
+
for (const name of this.pendingAutoTasks) {
|
|
296
|
+
const autoNsKey = `auto:${name}`;
|
|
297
|
+
this.handleSubagentTodos([{ content: "analyzing", status: "completed" }], autoNsKey, name);
|
|
298
|
+
}
|
|
299
|
+
this.pendingAutoTasks.clear();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
279
302
|
}
|
|
280
303
|
if (isSubagent)
|
|
281
304
|
return;
|
|
@@ -497,6 +520,49 @@ async function invokeWithTodoStreaming(agent, userMessage, spinner, { animatePro
|
|
|
497
520
|
}
|
|
498
521
|
return lastValues;
|
|
499
522
|
}
|
|
523
|
+
// ../utils/src/analysis/merge-findings.ts
|
|
524
|
+
var FINDINGS_DIR = "/findings";
|
|
525
|
+
var MERGED_FILENAME = "merged.json";
|
|
526
|
+
function toAbsoluteFindingsPath(entryPath) {
|
|
527
|
+
const lastSlash = entryPath.lastIndexOf("/");
|
|
528
|
+
const filename = lastSlash >= 0 ? entryPath.slice(lastSlash + 1) : entryPath;
|
|
529
|
+
return `${FINDINGS_DIR}/${filename}`;
|
|
530
|
+
}
|
|
531
|
+
async function mergeFindings(backend) {
|
|
532
|
+
let entries;
|
|
533
|
+
try {
|
|
534
|
+
entries = await backend.lsInfo(FINDINGS_DIR);
|
|
535
|
+
} catch {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
const jsonFiles = entries.filter((e) => e.path.endsWith(".json") && !e.path.endsWith(MERGED_FILENAME));
|
|
539
|
+
if (jsonFiles.length === 0) {
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
const allFindings = [];
|
|
543
|
+
for (const entry of jsonFiles) {
|
|
544
|
+
const filePath = toAbsoluteFindingsPath(entry.path);
|
|
545
|
+
try {
|
|
546
|
+
const fileData = await backend.readRaw(filePath);
|
|
547
|
+
const raw = fileData.content.join(`
|
|
548
|
+
`);
|
|
549
|
+
const parsed = JSON.parse(raw);
|
|
550
|
+
const validated = FindingsSchema.safeParse(parsed);
|
|
551
|
+
if (validated.success) {
|
|
552
|
+
allFindings.push(...validated.data.findings);
|
|
553
|
+
} else {
|
|
554
|
+
console.warn(`[merge-findings] Skipping ${filePath}: schema validation failed`);
|
|
555
|
+
}
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.warn(`[merge-findings] Skipping ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const mergedContent = JSON.stringify({ findings: allFindings }, null, 2);
|
|
562
|
+
await backend.write(`${FINDINGS_DIR}/${MERGED_FILENAME}`, mergedContent);
|
|
563
|
+
} catch {}
|
|
564
|
+
return allFindings;
|
|
565
|
+
}
|
|
500
566
|
// ../utils/src/analysis/deduplication.ts
|
|
501
567
|
function extractFunctionName(finding) {
|
|
502
568
|
if (finding.hotFunction?.name) {
|
|
@@ -655,21 +721,47 @@ Each finding MUST use one of these EXACT category values — do NOT invent new c
|
|
|
655
721
|
|
|
656
722
|
Prefer more specific categories (algorithm, serialization, allocation, event-handling,
|
|
657
723
|
blocking-io, listener-leak, gc-pressure) over generic ones (hot-function, other).`;
|
|
658
|
-
var
|
|
724
|
+
var PARALLEL_TOOL_CALLS = `## CRITICAL: Tool call strategy — scripts for data, read_file for source
|
|
725
|
+
|
|
726
|
+
Your FIRST turn MUST:
|
|
727
|
+
1. Run analysis scripts (execute_command) to query the JSON data files.
|
|
728
|
+
Use pre-built helper scripts in skills/ or write your own using the
|
|
729
|
+
data-scripting skill.
|
|
730
|
+
2. Call read_file for ALL application source files listed above.
|
|
731
|
+
|
|
732
|
+
Batch everything into ONE turn. Do NOT read data files one-at-a-time.
|
|
733
|
+
|
|
734
|
+
For data files: run a helper script or write a custom one. This is faster
|
|
735
|
+
and uses fewer tokens than reading raw JSON.
|
|
736
|
+
|
|
737
|
+
For source files: use read_file since you need to see the exact code for
|
|
738
|
+
beforeCode/afterCode suggestions.
|
|
659
739
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
740
|
+
FORBIDDEN actions:
|
|
741
|
+
- ls — NEVER call ls. File paths are already listed above.
|
|
742
|
+
- glob — NEVER call glob. File paths are already listed above.
|
|
743
|
+
- Reading JSON data files with read_file — use scripts instead.`;
|
|
744
|
+
var WRITE_FINDINGS_REQUIREMENT = `## CRITICAL — Persist your findings to a file
|
|
745
|
+
|
|
746
|
+
When your analysis is complete, you MUST write ALL findings to a JSON file using write_file.
|
|
747
|
+
|
|
748
|
+
1. Call write_file with path: \`/findings/<YOUR_AGENT_NAME>.json\`
|
|
749
|
+
Use your agent name as the filename (e.g. memory-heap, page-load, runtime-blocking,
|
|
750
|
+
code-pattern, cpu-hotspot, listener-leak, memory-closure).
|
|
663
751
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
752
|
+
2. The file content MUST be a JSON object with this exact structure:
|
|
753
|
+
{ "findings": [ { "severity": "...", "title": "...", ... }, ... ] }
|
|
754
|
+
Each finding must include ALL required fields: severity, title, description,
|
|
755
|
+
category, sourceFile, lineNumber, suggestedFix, beforeCode, afterCode,
|
|
756
|
+
confidence, impactMs, estimatedSavingsMs.
|
|
669
757
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
758
|
+
3. Write ALL findings in a SINGLE write_file call. Do NOT write findings one at a time.
|
|
759
|
+
|
|
760
|
+
4. After writing the file, respond with ONLY a brief summary like:
|
|
761
|
+
"Found 4 issues: 2 critical, 1 warning, 1 info. Written to /findings/memory-heap.json"
|
|
762
|
+
|
|
763
|
+
The orchestrator reads findings from the file directly — your text response is only for
|
|
764
|
+
progress display. If a finding is not in the JSON file, it does not exist.`;
|
|
673
765
|
var STRUCTURED_OUTPUT_FIELDS = `## Structured output fields — REQUIRED for every finding
|
|
674
766
|
|
|
675
767
|
Every finding MUST include ALL of these fields:
|
|
@@ -1906,46 +1998,504 @@ var BROWSER_ANALYSIS_SKILL_FILES = {
|
|
|
1906
1998
|
"skills/browser-analysis/helpers/analyze-heap.js": ANALYZE_HEAP_JS,
|
|
1907
1999
|
"skills/browser-analysis/helpers/find-patterns.js": FIND_PATTERNS_JS
|
|
1908
2000
|
};
|
|
1909
|
-
// ../utils/src/
|
|
1910
|
-
var
|
|
2001
|
+
// ../utils/src/profiling/profile-parser.ts
|
|
2002
|
+
var META_FUNCTIONS = new Set(["(root)", "(idle)", "(program)"]);
|
|
2003
|
+
// ../utils/src/profiling/agent.ts
|
|
2004
|
+
import { createDeepAgent } from "deepagents";
|
|
1911
2005
|
|
|
1912
|
-
|
|
2006
|
+
// ../utils/src/profiling/prompts/test-shared.ts
|
|
2007
|
+
var SEVERITY_RULES = `## Severity classification
|
|
1913
2008
|
|
|
1914
|
-
|
|
2009
|
+
Assign severity based on the nature and measured impact of the issue:
|
|
1915
2010
|
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2011
|
+
- **critical** — Any of:
|
|
2012
|
+
- Synchronous blocking of the event loop (CPU-bound loops, sync crypto, sync I/O)
|
|
2013
|
+
- Functions that CALL blocking functions (compound blockers)
|
|
2014
|
+
- Listener exceedances (count exceeding maxListeners threshold)
|
|
2015
|
+
- GC overhead >10% of total profile duration
|
|
2016
|
+
- A single function consuming >15% of APPLICATION code self-time
|
|
2017
|
+
- **warning** — Any of:
|
|
2018
|
+
- Listener add/remove imbalance (addCount > 2× removeCount) without exceedance
|
|
2019
|
+
- O(n²) or worse algorithms on collections
|
|
2020
|
+
- Unnecessary serialization (JSON.parse/JSON.stringify) on hot paths
|
|
2021
|
+
- Closure-based memory leaks or unbounded data structures
|
|
2022
|
+
- A function consuming 5–15% of application self-time
|
|
2023
|
+
- **info** — Minor inefficiencies, small optimisation opportunities, per-call
|
|
2024
|
+
object allocation (TextEncoder, RegExp, DateTimeFormat), or patterns that only
|
|
2025
|
+
matter at scale
|
|
2026
|
+
|
|
2027
|
+
IMPORTANT: Blocking/event-loop-blocking operations are ALWAYS critical, regardless
|
|
2028
|
+
of measured self-time percentage. Even a short blocking call prevents the event loop
|
|
2029
|
+
from processing other work and is a correctness issue, not just a performance issue.`;
|
|
2030
|
+
|
|
2031
|
+
// ../utils/src/profiling/prompts/cpu-hotspot.ts
|
|
2032
|
+
var CPU_HOTSPOT_PROMPT = `You are a specialist in detecting CPU-blocking operations and excessive object instantiation in JavaScript/TypeScript code.
|
|
2033
|
+
|
|
2034
|
+
You have access to a workspace with V8 CPU profiling data from a Vitest test run.
|
|
2035
|
+
|
|
2036
|
+
## Your focus areas
|
|
2037
|
+
|
|
2038
|
+
### 1. Blocking / Event-Loop-Blocking Operations (HIGHEST PRIORITY)
|
|
2039
|
+
|
|
2040
|
+
Look for functions that block the event loop with synchronous CPU-intensive work:
|
|
2041
|
+
- Synchronous crypto operations (hashing, encryption) that should use async APIs
|
|
2042
|
+
- CPU-bound loops (e.g., manual hashing with many iterations, busy-waits)
|
|
2043
|
+
- Functions that CALL other blocking functions (compound blocking). Report the
|
|
2044
|
+
CALLER as a separate finding.
|
|
2045
|
+
- Synchronous file I/O in hot paths (readFileSync, writeFileSync, etc.)
|
|
2046
|
+
- Heavy computation without yielding (e.g., large matrix operations, parsing)
|
|
2047
|
+
|
|
2048
|
+
**How to detect:** Read hot-functions/application.json for functions with high selfTime.
|
|
2049
|
+
For each one with >= 1% selfPercent, read the source code and check for:
|
|
2050
|
+
- Loops with many iterations doing CPU work
|
|
2051
|
+
- Calls to other blocking functions (trace the call chain — read the callee's source!)
|
|
2052
|
+
- Missing async/await for operations that have async alternatives (e.g., crypto.pbkdf2 vs crypto.pbkdf2Sync)
|
|
2053
|
+
|
|
2054
|
+
**IMPORTANT — Compound blockers are SEPARATE findings:**
|
|
2055
|
+
If function A calls function B and B is blocking, you MUST report TWO findings:
|
|
2056
|
+
1. Function B: the primary blocking operation
|
|
2057
|
+
2. Function A: a "compound blocker" that calls B, inheriting and compounding B's cost
|
|
2058
|
+
Do NOT just report B and skip A. The developer needs to know both call sites.
|
|
2059
|
+
|
|
2060
|
+
### 2. Excessive Object Instantiation (SECONDARY)
|
|
2061
|
+
|
|
2062
|
+
Look for functions creating stateless objects on every call that should be
|
|
2063
|
+
module-level singletons or hoisted out of loops:
|
|
2064
|
+
- \`new TextEncoder()\` / \`new TextDecoder()\` — stateless, should be module-level
|
|
2065
|
+
- \`new Intl.DateTimeFormat()\` / \`new Intl.NumberFormat()\` — locale-dependent but cacheable
|
|
2066
|
+
- \`new Map()\` / \`new Set()\` — if used as temporary lookup then discarded each call
|
|
2067
|
+
- \`new RegExp()\` — if the pattern is constant, compile once at module level
|
|
2068
|
+
- \`new Date()\` inside sort comparators — called O(n log n) times
|
|
2069
|
+
- Any constructor call inside a hot loop that produces a stateless, reusable object
|
|
2070
|
+
|
|
2071
|
+
**How to detect:** Read source files for hot functions and look for object
|
|
2072
|
+
construction inside function bodies that could be hoisted to module scope.
|
|
2073
|
+
|
|
2074
|
+
**IMPORTANT:** If a function has BOTH a blocking issue AND an instantiation issue,
|
|
2075
|
+
report them as TWO separate findings with different categories (blocking-io vs allocation).
|
|
2076
|
+
Do NOT skip the instantiation finding just because you already reported a blocking finding
|
|
2077
|
+
for the same function.
|
|
2078
|
+
|
|
2079
|
+
## Your scope — categories YOU own
|
|
2080
|
+
|
|
2081
|
+
You are one of four parallel subagents. Use ONLY these categories:
|
|
2082
|
+
- **blocking-io** — for event-loop-blocking operations (sync crypto, CPU loops, sync I/O)
|
|
2083
|
+
- **allocation** — for per-call object instantiation (new TextEncoder, new Intl.DateTimeFormat, new Map per call)
|
|
2084
|
+
|
|
2085
|
+
Do NOT report findings with categories: algorithm, serialization, gc-pressure,
|
|
2086
|
+
listener-leak, event-handling, unnecessary-computation. Other subagents handle those.
|
|
2087
|
+
Do NOT report findings about test files (tests/*.ts) — only about src/ files.
|
|
2088
|
+
|
|
2089
|
+
## Your workflow
|
|
2090
|
+
|
|
2091
|
+
1. In your FIRST turn, do ALL of these in ONE batch:
|
|
2092
|
+
a. Run the workspace overview script:
|
|
2093
|
+
execute_command: node skills/profile-analysis/helpers/analyze-workspace.js
|
|
2094
|
+
b. Run the detailed hot functions script:
|
|
2095
|
+
execute_command: node skills/profile-analysis/helpers/analyze-hotfunctions.js
|
|
2096
|
+
c. Call read_file for EVERY src/ file listed in "FILES IN THIS WORKSPACE" above.
|
|
2097
|
+
Do NOT use ls or glob. Batch everything into ONE turn.
|
|
2098
|
+
2. From the script outputs, identify hot functions (>= 1% selfPercent) and match
|
|
2099
|
+
them to the source code you read.
|
|
2100
|
+
3. For EACH hot function, analyze its source for blocking patterns or unnecessary instantiation.
|
|
2101
|
+
4. Check EVERY source file top-to-bottom, not just the hot ones.
|
|
2102
|
+
5. For compound blockers, trace the call chain using the callerChain data from the script output.
|
|
2103
|
+
|
|
2104
|
+
${PARALLEL_TOOL_CALLS}
|
|
2105
|
+
${VERIFICATION_RULES}
|
|
2106
|
+
${SEVERITY_RULES}
|
|
2107
|
+
${FINDING_CATEGORIES}
|
|
2108
|
+
${OUTPUT_FORMAT}
|
|
2109
|
+
${STRUCTURED_OUTPUT_FIELDS}
|
|
2110
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2111
|
+
// ../utils/src/profiling/prompts/listener-leak.ts
|
|
2112
|
+
var LISTENER_LEAK_PROMPT = `You are a specialist in detecting event listener leaks and event handling imbalances in JavaScript/TypeScript code.
|
|
2113
|
+
|
|
2114
|
+
You have access to a workspace with V8 CPU profiling data and event listener tracking from a Vitest test run.
|
|
2115
|
+
|
|
2116
|
+
## Your SOLE focus: Event Listener Leaks
|
|
2117
|
+
|
|
2118
|
+
You look for ONE thing: code that registers event listeners without proper cleanup,
|
|
2119
|
+
causing listener accumulation, memory growth, and MaxListenersExceededWarning.
|
|
2120
|
+
|
|
2121
|
+
### Pattern A — Listener accumulation per call
|
|
2122
|
+
|
|
2123
|
+
A function that adds a new listener EVERY TIME it is called, but never removes
|
|
2124
|
+
old ones. After N calls, there are N active listeners.
|
|
2125
|
+
|
|
2126
|
+
\`\`\`typescript
|
|
2127
|
+
// BAD: adds a new listener on every call
|
|
2128
|
+
function getData() {
|
|
2129
|
+
emitter.on('update', handler); // accumulates!
|
|
2130
|
+
}
|
|
2131
|
+
\`\`\`
|
|
2132
|
+
|
|
2133
|
+
**Correct fix pattern — use a guard flag + named handler:**
|
|
2134
|
+
|
|
2135
|
+
\`\`\`typescript
|
|
2136
|
+
// GOOD: register once, named handler, proper cleanup
|
|
2137
|
+
let registered = false;
|
|
2138
|
+
const onUpdate = () => { cache = null; };
|
|
2139
|
+
|
|
2140
|
+
function getData() {
|
|
2141
|
+
if (!registered) {
|
|
2142
|
+
emitter.on('update', onUpdate);
|
|
2143
|
+
registered = true;
|
|
2144
|
+
}
|
|
2145
|
+
// ... rest of function
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
function reset() {
|
|
2149
|
+
if (registered) {
|
|
2150
|
+
emitter.off('update', onUpdate); // surgical removal
|
|
2151
|
+
registered = false;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
\`\`\`
|
|
2155
|
+
|
|
2156
|
+
CRITICAL for afterCode: always use a NAMED handler (const onUpdate = ...) so
|
|
2157
|
+
it can be removed with .off(). NEVER use anonymous functions with .on().
|
|
2158
|
+
If the file has an existing cleanup/reset function, update it to call
|
|
2159
|
+
.off(event, handler) and reset the guard flag.
|
|
2160
|
+
|
|
2161
|
+
### Pattern B — Missing unsubscribe mechanism
|
|
2162
|
+
|
|
2163
|
+
A subscribe-style function that adds listeners but returns no way to remove them.
|
|
2164
|
+
|
|
2165
|
+
\`\`\`typescript
|
|
2166
|
+
// BAD: no way to unsubscribe
|
|
2167
|
+
function subscribe(channel) {
|
|
2168
|
+
emitter.on(channel, handler); // no return value, no cleanup
|
|
2169
|
+
}
|
|
2170
|
+
\`\`\`
|
|
2171
|
+
|
|
2172
|
+
**Correct fix pattern — return an unsubscribe function:**
|
|
2173
|
+
|
|
2174
|
+
\`\`\`typescript
|
|
2175
|
+
// GOOD: returns cleanup function
|
|
2176
|
+
function subscribe(channel, handler) {
|
|
2177
|
+
emitter.on(channel, handler);
|
|
2178
|
+
return () => { emitter.off(channel, handler); };
|
|
2179
|
+
}
|
|
2180
|
+
\`\`\`
|
|
2181
|
+
|
|
2182
|
+
### Pattern C — MaxListeners exceeded (MUST report as a SEPARATE finding)
|
|
2183
|
+
|
|
2184
|
+
When listener counts exceed the default maxListeners threshold (10), this
|
|
2185
|
+
triggers a MaxListenersExceededWarning at runtime. Check listener-tracking.json
|
|
2186
|
+
for the "exceedances" array — each entry shows an event type where the listener
|
|
2187
|
+
count exceeded the threshold.
|
|
2188
|
+
|
|
2189
|
+
**This is a SEPARATE finding from Pattern A/B**, even if the same function causes
|
|
2190
|
+
both the accumulation AND the exceedance. You MUST report:
|
|
2191
|
+
1. Pattern A or B finding: the code that adds listeners without cleanup
|
|
2192
|
+
2. Pattern C finding: the maxListeners threshold being exceeded, with the
|
|
2193
|
+
specific count, threshold, and event name from the exceedance data
|
|
2194
|
+
|
|
2195
|
+
The Pattern C finding MUST have:
|
|
2196
|
+
- category: **"event-handling"** (do NOT use "listener-leak" — use a DIFFERENT
|
|
2197
|
+
category from Pattern A/B so both findings survive deduplication)
|
|
2198
|
+
- severity: "critical" (exceedances are always critical)
|
|
2199
|
+
- title: focus on the event name and threshold, e.g. "maxListeners threshold
|
|
2200
|
+
exceeded for task:changed event (11 listeners, threshold 10)"
|
|
2201
|
+
- description: MUST mention ALL of these terms: "maxListeners", "threshold",
|
|
2202
|
+
"exceeded", the event name (e.g. "task:changed"), and the numeric count
|
|
2203
|
+
(e.g. "11") and threshold (e.g. "10") from the tracking data
|
|
2204
|
+
|
|
2205
|
+
## Your scope — categories YOU own
|
|
2206
|
+
|
|
2207
|
+
You are one of four parallel subagents. Use ONLY these categories:
|
|
2208
|
+
- **listener-leak** — for Pattern A (accumulation) and Pattern B (missing unsubscribe)
|
|
2209
|
+
- **event-handling** — for Pattern C (maxListeners exceeded)
|
|
2210
|
+
|
|
2211
|
+
Do NOT report findings with categories: blocking-io, allocation, algorithm,
|
|
2212
|
+
serialization, gc-pressure, unnecessary-computation. Other subagents handle those.
|
|
2213
|
+
Do NOT report findings about test files (tests/*.ts) — only about src/ files.
|
|
2214
|
+
|
|
2215
|
+
## Your workflow (follow this EXACTLY)
|
|
2216
|
+
|
|
2217
|
+
1. In your FIRST turn, do ALL of these in ONE batch:
|
|
2218
|
+
a. Run the workspace overview script:
|
|
2219
|
+
execute_command: node skills/profile-analysis/helpers/analyze-workspace.js
|
|
2220
|
+
b. Run the detailed listener analysis script:
|
|
2221
|
+
execute_command: node skills/profile-analysis/helpers/analyze-listeners.js
|
|
2222
|
+
c. Call read_file for EVERY src/ file listed in "FILES IN THIS WORKSPACE" above.
|
|
2223
|
+
Do NOT use ls or glob. Batch everything into ONE turn.
|
|
2224
|
+
2. From the script outputs, identify:
|
|
2225
|
+
- exceedances (listenerCount > maxListeners threshold)
|
|
2226
|
+
- add/remove imbalances (addCount with zero removeCount = leak candidates)
|
|
2227
|
+
3. In the source files you already read, find the .on() / .addEventListener() calls
|
|
2228
|
+
and check if corresponding removal exists.
|
|
2229
|
+
4. For each issue found, provide before/after code.
|
|
2230
|
+
|
|
2231
|
+
## Important: Report EACH pattern as a SEPARATE finding
|
|
2232
|
+
|
|
2233
|
+
- If a function adds a listener without removal → one finding about accumulation
|
|
2234
|
+
- If a subscribe function has no unsubscribe mechanism → a separate finding
|
|
2235
|
+
- If maxListeners is exceeded → a SEPARATE finding (cross-reference with the causal
|
|
2236
|
+
pattern above). This must be its own finding even if you already reported the
|
|
2237
|
+
listener accumulation that caused it. The developer needs to know BOTH that
|
|
2238
|
+
listeners accumulate AND that the threshold is exceeded.
|
|
2239
|
+
|
|
2240
|
+
### Minimum expected findings
|
|
2241
|
+
|
|
2242
|
+
For a typical codebase with listener leaks, expect at least:
|
|
2243
|
+
1. One finding per function that adds listeners without cleanup (Pattern A)
|
|
2244
|
+
2. One finding per subscribe function without unsubscribe (Pattern B)
|
|
2245
|
+
3. One finding per maxListeners exceedance from tracking data (Pattern C)
|
|
2246
|
+
|
|
2247
|
+
${PARALLEL_TOOL_CALLS}
|
|
2248
|
+
${VERIFICATION_RULES}
|
|
2249
|
+
${SEVERITY_RULES}
|
|
2250
|
+
${FINDING_CATEGORIES}
|
|
2251
|
+
${OUTPUT_FORMAT}
|
|
2252
|
+
${STRUCTURED_OUTPUT_FIELDS}
|
|
2253
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2254
|
+
// ../utils/src/profiling/prompts/memory-closure.ts
|
|
2255
|
+
var MEMORY_CLOSURE_PROMPT = `You are a specialist in detecting memory leaks caused by closures, unbounded data structures, and missing cleanup/eviction in JavaScript/TypeScript code.
|
|
2256
|
+
|
|
2257
|
+
You have access to a workspace with V8 CPU profiling data from a Vitest test run.
|
|
2258
|
+
|
|
2259
|
+
## Your SOLE focus: Closure & Memory Leak Patterns
|
|
2260
|
+
|
|
2261
|
+
You look for code where objects, closures, or data structures retain references
|
|
2262
|
+
longer than necessary, preventing garbage collection and causing continuous
|
|
2263
|
+
memory growth.
|
|
2264
|
+
|
|
2265
|
+
### Pattern A — Closures capturing outer-scope data
|
|
2266
|
+
|
|
2267
|
+
Closures stored in long-lived data structures that capture variables from
|
|
2268
|
+
the enclosing scope — even after the captured data is conceptually stale.
|
|
2269
|
+
|
|
2270
|
+
\`\`\`typescript
|
|
2271
|
+
// BAD: closure captures 'value' and 'ctx' from enclosing scope
|
|
2272
|
+
set(key, value, ctx) {
|
|
2273
|
+
this.cache.set(key, {
|
|
2274
|
+
data: value,
|
|
2275
|
+
refresher: () => {
|
|
2276
|
+
// This closure captures 'value' and 'ctx' — they can
|
|
2277
|
+
// never be garbage collected while the cache entry exists
|
|
2278
|
+
return fetchFresh(key, ctx);
|
|
2279
|
+
}
|
|
1920
2280
|
});
|
|
1921
|
-
} catch (e) {
|
|
1922
|
-
console.log('[]');
|
|
1923
|
-
process.exit(0);
|
|
1924
2281
|
}
|
|
2282
|
+
\`\`\`
|
|
1925
2283
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2284
|
+
### Pattern B — Unbounded data structures (no eviction)
|
|
2285
|
+
|
|
2286
|
+
Arrays, Maps, or Sets that only grow — elements are added but never removed,
|
|
2287
|
+
cleared, or evicted. Over time, memory grows monotonically.
|
|
2288
|
+
|
|
2289
|
+
\`\`\`typescript
|
|
2290
|
+
// BAD: log grows without bound
|
|
2291
|
+
process(item) {
|
|
2292
|
+
this.log.push({
|
|
2293
|
+
item,
|
|
2294
|
+
timestamp: Date.now(),
|
|
2295
|
+
context: this.currentContext // retains reference forever
|
|
2296
|
+
});
|
|
1929
2297
|
}
|
|
2298
|
+
\`\`\`
|
|
1930
2299
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2300
|
+
### Pattern C — Closures capturing request/response or transient objects
|
|
2301
|
+
|
|
2302
|
+
Code that stores closures capturing objects meant to be short-lived (e.g.
|
|
2303
|
+
request bodies, response objects, connection handles), preventing them from
|
|
2304
|
+
being freed after their lifecycle ends.
|
|
2305
|
+
|
|
2306
|
+
\`\`\`typescript
|
|
2307
|
+
// BAD: closure captures the full transient object forever
|
|
2308
|
+
record(obj) {
|
|
2309
|
+
this.entries.push({
|
|
2310
|
+
id: obj.id,
|
|
2311
|
+
timestamp: Date.now(),
|
|
2312
|
+
getDetails: () => ({
|
|
2313
|
+
payload: obj.payload, // captures obj.payload forever
|
|
2314
|
+
metadata: obj.metadata // captures obj.metadata forever
|
|
2315
|
+
})
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
\`\`\`
|
|
2319
|
+
|
|
2320
|
+
## Your scope — categories YOU own
|
|
2321
|
+
|
|
2322
|
+
You are one of four parallel subagents. Use ONLY this category:
|
|
2323
|
+
- **gc-pressure** — for closures capturing outer-scope data, unbounded data
|
|
2324
|
+
structures (Maps, arrays) without eviction, and closures retaining transient objects
|
|
2325
|
+
|
|
2326
|
+
Do NOT report findings with categories: blocking-io, allocation, algorithm,
|
|
2327
|
+
serialization, listener-leak, event-handling, unnecessary-computation. Other
|
|
2328
|
+
subagents handle those. Specifically:
|
|
2329
|
+
- Do NOT report event listener leaks (the listener-leak agent handles those)
|
|
2330
|
+
- Do NOT report blocking I/O or CPU loops (the cpu-hotspot agent handles those)
|
|
2331
|
+
- Do NOT report algorithmic inefficiencies (the code-pattern agent handles those)
|
|
2332
|
+
Do NOT report findings about test files (tests/*.ts) — only about src/ files.
|
|
2333
|
+
|
|
2334
|
+
## Your workflow (follow this EXACTLY)
|
|
2335
|
+
|
|
2336
|
+
1. In your FIRST turn, do ALL of these in ONE batch:
|
|
2337
|
+
a. Run the workspace overview script:
|
|
2338
|
+
execute_command: node skills/profile-analysis/helpers/analyze-workspace.js
|
|
2339
|
+
b. Run the leak finder script:
|
|
2340
|
+
execute_command: node skills/profile-analysis/helpers/find-leaks.js
|
|
2341
|
+
c. Call read_file for EVERY src/ file listed in "FILES IN THIS WORKSPACE" above.
|
|
2342
|
+
Do NOT use ls or glob. Batch everything into ONE turn.
|
|
2343
|
+
2. From the script outputs, identify potential leak patterns and allocation hotspots.
|
|
2344
|
+
3. For each source file you read, look for:
|
|
2345
|
+
- Module-level or class-level Maps, Sets, Arrays used as stores
|
|
2346
|
+
- Whether a corresponding removal mechanism exists
|
|
2347
|
+
- Closures stored as values that capture outer-scope variables
|
|
2348
|
+
4. For each issue found, provide before/after code with proper cleanup.
|
|
2349
|
+
|
|
2350
|
+
### CRITICAL: Report EVERY distinct issue, even in the same class
|
|
2351
|
+
|
|
2352
|
+
A single class or module can have multiple closure/memory issues. Report
|
|
2353
|
+
each as a SEPARATE finding. For example, a CacheService class might have:
|
|
2354
|
+
1. A \`set()\` method with a closure that captures outer-scope data
|
|
2355
|
+
2. An unbounded access log in \`get()\` that grows without eviction
|
|
2356
|
+
3. A Map that stores entries without any TTL or maxSize
|
|
2357
|
+
These are THREE separate findings, not one.
|
|
2358
|
+
|
|
2359
|
+
${PARALLEL_TOOL_CALLS}
|
|
2360
|
+
${VERIFICATION_RULES}
|
|
2361
|
+
${SEVERITY_RULES}
|
|
2362
|
+
${FINDING_CATEGORIES}
|
|
2363
|
+
${OUTPUT_FORMAT}
|
|
2364
|
+
${STRUCTURED_OUTPUT_FIELDS}
|
|
2365
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2366
|
+
// ../utils/src/profiling/prompts/code-pattern.ts
|
|
2367
|
+
var CODE_PATTERN_PROMPT = `You are a specialist in detecting algorithmic inefficiencies, unnecessary computation, and serialization overhead in JavaScript/TypeScript code.
|
|
2368
|
+
|
|
2369
|
+
You have access to a workspace with V8 CPU profiling data from a Vitest test run.
|
|
2370
|
+
|
|
2371
|
+
## Your focus areas
|
|
2372
|
+
|
|
2373
|
+
### 1. Quadratic or Worse Algorithms (HIGHEST PRIORITY)
|
|
2374
|
+
|
|
2375
|
+
Look for O(n²) or worse complexity patterns:
|
|
2376
|
+
|
|
2377
|
+
**Pattern A — Nested iteration over same collection:**
|
|
2378
|
+
\`\`\`typescript
|
|
2379
|
+
// BAD: O(n²) — filter inside a loop
|
|
2380
|
+
for (const item of items) {
|
|
2381
|
+
const dupes = items.filter(other => other.id === item.id);
|
|
2382
|
+
}
|
|
2383
|
+
\`\`\`
|
|
2384
|
+
|
|
2385
|
+
**Pattern B — Pairwise comparison:**
|
|
2386
|
+
\`\`\`typescript
|
|
2387
|
+
// BAD: O(n²) or worse — nested loops over the same or related collections
|
|
2388
|
+
for (const a of items) {
|
|
2389
|
+
for (const b of items) {
|
|
2390
|
+
// comparison or accumulation logic
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
\`\`\`
|
|
2394
|
+
|
|
2395
|
+
**Pattern C — O(n²) duplicate detection:**
|
|
2396
|
+
\`\`\`typescript
|
|
2397
|
+
// BAD: filter().length for each element = O(n²)
|
|
2398
|
+
items.forEach(item => {
|
|
2399
|
+
if (items.filter(x => x === item).length > 1) { /* duplicate */ }
|
|
2400
|
+
});
|
|
2401
|
+
// FIX: Use a Set or Map for O(n)
|
|
2402
|
+
\`\`\`
|
|
2403
|
+
|
|
2404
|
+
### 2. Unnecessary Serialization (SECONDARY)
|
|
2405
|
+
|
|
2406
|
+
\`\`\`typescript
|
|
2407
|
+
// BAD: deep clone via JSON roundtrip on every call
|
|
2408
|
+
return JSON.parse(JSON.stringify(data));
|
|
2409
|
+
// FIX: structuredClone(data) or spread operator for shallow copies
|
|
2410
|
+
\`\`\`
|
|
2411
|
+
|
|
2412
|
+
### 3. Regex Recompilation
|
|
2413
|
+
|
|
2414
|
+
\`\`\`typescript
|
|
2415
|
+
// BAD: compiles regex on every call
|
|
2416
|
+
function validate(input) {
|
|
2417
|
+
const pattern = new RegExp('^[a-z]+$'); // recompiled every call!
|
|
2418
|
+
return pattern.test(input);
|
|
2419
|
+
}
|
|
2420
|
+
// FIX: const PATTERN = /^[a-z]+$/; at module level
|
|
2421
|
+
\`\`\`
|
|
2422
|
+
|
|
2423
|
+
### 4. Expensive Sort Comparators
|
|
2424
|
+
|
|
2425
|
+
\`\`\`typescript
|
|
2426
|
+
// BAD: creates objects inside sort comparator (called O(n log n) times)
|
|
2427
|
+
items.sort((a, b) => {
|
|
2428
|
+
const dateA = new Date(a.createdAt); // new object per comparison!
|
|
2429
|
+
return dateA.getTime() - new Date(b.createdAt).getTime();
|
|
2430
|
+
});
|
|
2431
|
+
// FIX: pre-compute timestamps before sorting
|
|
2432
|
+
\`\`\`
|
|
2433
|
+
|
|
2434
|
+
Also check for **functions called FROM sort comparators**. If \`items.sort((a, b) => computeWeight(a) - computeWeight(b))\` calls a function that does expensive work (Date parsing, string operations, object creation), that function runs O(n log n) times per sort — report it as a separate finding.
|
|
2435
|
+
|
|
2436
|
+
### 5. Pairwise Correlation / Tag Comparison (O(n² × m²))
|
|
2437
|
+
|
|
2438
|
+
Look for functions that compare every pair of items AND every pair of their sub-elements:
|
|
2439
|
+
\`\`\`typescript
|
|
2440
|
+
// BAD: O(n²×m²) — for each pair of tasks, compare all pairs of their tags
|
|
2441
|
+
for (const taskA of tasks) {
|
|
2442
|
+
for (const taskB of tasks) {
|
|
2443
|
+
for (const tagA of taskA.tags) {
|
|
2444
|
+
for (const tagB of taskB.tags) { /* ... */ }
|
|
1937
2445
|
}
|
|
1938
|
-
} catch (e) {
|
|
1939
|
-
// skip malformed files
|
|
1940
2446
|
}
|
|
1941
2447
|
}
|
|
2448
|
+
\`\`\`
|
|
2449
|
+
Functions named like \`computeCorrelations\`, \`computeTagCorrelations\`, \`findPairs\`, etc. are prime suspects. Also look for \`.sort()\` and \`.join()\` inside inner loops.
|
|
2450
|
+
|
|
2451
|
+
## How to detect
|
|
2452
|
+
|
|
2453
|
+
1. Read hot-functions/application.json to identify which functions are CPU-hot
|
|
2454
|
+
2. Read EVERY application source file — not just the hot ones
|
|
2455
|
+
3. Go through EVERY FUNCTION in every file and check for the patterns above
|
|
2456
|
+
4. Pay special attention to:
|
|
2457
|
+
- Functions that operate on arrays or collections
|
|
2458
|
+
- Any function containing nested loops or chained .filter/.map/.reduce calls
|
|
2459
|
+
- Functions that call JSON.parse, JSON.stringify, or new RegExp inside a loop or on every invocation
|
|
2460
|
+
- Sort comparators that create objects (new Date(), etc.) — the comparator runs O(n log n) times
|
|
2461
|
+
- Functions called from sort comparators (they inherit O(n log n) invocations)
|
|
2462
|
+
- Functions that do pairwise comparison of collection elements (O(n²) or O(n²×m²))
|
|
2463
|
+
- Duplicate detection using .filter() instead of Set (O(n²) vs O(n))
|
|
2464
|
+
|
|
2465
|
+
## Your scope — categories YOU own
|
|
2466
|
+
|
|
2467
|
+
You are one of four parallel subagents. Use ONLY these categories:
|
|
2468
|
+
- **algorithm** — for O(n²) loops, brute-force, pairwise comparison, expensive sort comparators
|
|
2469
|
+
- **serialization** — for unnecessary JSON.parse/JSON.stringify roundtrips
|
|
2470
|
+
- **unnecessary-computation** — for regex recompilation with constant patterns
|
|
2471
|
+
|
|
2472
|
+
Do NOT report findings with categories: blocking-io, allocation, gc-pressure,
|
|
2473
|
+
listener-leak, event-handling. Other subagents handle those. Specifically:
|
|
2474
|
+
- Do NOT report per-call object instantiation (new TextEncoder, etc.) — the cpu-hotspot agent handles those
|
|
2475
|
+
- Do NOT report event listener leaks — the listener-leak agent handles those
|
|
2476
|
+
- Do NOT report closure/memory leaks — the memory-closure agent handles those
|
|
2477
|
+
Do NOT report findings about test files (tests/*.ts) — only about src/ files.
|
|
1942
2478
|
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2479
|
+
## Your workflow
|
|
2480
|
+
|
|
2481
|
+
1. In your FIRST turn, do ALL of these in ONE batch:
|
|
2482
|
+
a. Run the workspace overview script:
|
|
2483
|
+
execute_command: node skills/profile-analysis/helpers/analyze-workspace.js
|
|
2484
|
+
b. Call read_file for ALL of these in ONE batch:
|
|
2485
|
+
- scripts/application.json
|
|
2486
|
+
- EVERY src/ file listed in "FILES IN THIS WORKSPACE" above
|
|
2487
|
+
Do NOT use ls or glob.
|
|
2488
|
+
2. From the script output, identify which functions are CPU-hot.
|
|
2489
|
+
3. For EVERY function in EVERY source file, check for the patterns above.
|
|
2490
|
+
4. Report each distinct pattern as a separate finding.
|
|
2491
|
+
|
|
2492
|
+
${PARALLEL_TOOL_CALLS}
|
|
2493
|
+
${VERIFICATION_RULES}
|
|
2494
|
+
${SEVERITY_RULES}
|
|
2495
|
+
${FINDING_CATEGORIES}
|
|
2496
|
+
${OUTPUT_FORMAT}
|
|
2497
|
+
${STRUCTURED_OUTPUT_FIELDS}
|
|
2498
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
1949
2499
|
// ../utils/src/output/terminal.ts
|
|
1950
2500
|
import pc2 from "picocolors";
|
|
1951
2501
|
import ora from "ora";
|
|
@@ -2270,8 +2820,7 @@ function generateMarkdown(options) {
|
|
|
2270
2820
|
`);
|
|
2271
2821
|
}
|
|
2272
2822
|
// src/analysis/agent.ts
|
|
2273
|
-
import { createDeepAgent } from "deepagents";
|
|
2274
|
-
import { toolStrategy } from "langchain";
|
|
2823
|
+
import { createDeepAgent as createDeepAgent2 } from "deepagents";
|
|
2275
2824
|
|
|
2276
2825
|
// src/analysis/prompts/shared.ts
|
|
2277
2826
|
var BROWSER_TOOL_CALL_STRATEGY = `## CRITICAL: Tool call strategy — scripts first, source selectively
|
|
@@ -2314,7 +2863,7 @@ When source code is minified/compiled:
|
|
|
2314
2863
|
Only provide \`afterCode\` when the source is clearly human-authored (readable
|
|
2315
2864
|
variable names, formatting, comments) — e.g. inline scripts in HTML or
|
|
2316
2865
|
un-minified CSS.`;
|
|
2317
|
-
var
|
|
2866
|
+
var SEVERITY_RULES2 = `## Severity classification
|
|
2318
2867
|
|
|
2319
2868
|
Assign severity based on measured impact — do NOT guess:
|
|
2320
2869
|
|
|
@@ -2469,14 +3018,14 @@ These are THREE separate findings, not one.
|
|
|
2469
3018
|
|
|
2470
3019
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
2471
3020
|
${VERIFICATION_RULES}
|
|
2472
|
-
${
|
|
3021
|
+
${SEVERITY_RULES2}
|
|
2473
3022
|
${FINDING_CATEGORIES}
|
|
2474
3023
|
${OUTPUT_FORMAT}
|
|
2475
3024
|
${STRUCTURED_OUTPUT_FIELDS}
|
|
2476
3025
|
${MINIFIED_SOURCE_HANDLING}
|
|
2477
3026
|
${CROSS_REFERENCING}
|
|
2478
3027
|
${IMPACT_ESTIMATION}
|
|
2479
|
-
${
|
|
3028
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2480
3029
|
// src/analysis/prompts/page-load.ts
|
|
2481
3030
|
var PAGE_LOAD_PROMPT = `You are a specialist in analyzing page load performance, render-blocking resources, and network waterfall patterns.
|
|
2482
3031
|
|
|
@@ -2570,13 +3119,13 @@ Assets served without compression or with missing cache headers.
|
|
|
2570
3119
|
|
|
2571
3120
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
2572
3121
|
${VERIFICATION_RULES}
|
|
2573
|
-
${
|
|
3122
|
+
${SEVERITY_RULES2}
|
|
2574
3123
|
${FINDING_CATEGORIES}
|
|
2575
3124
|
${OUTPUT_FORMAT}
|
|
2576
3125
|
${STRUCTURED_OUTPUT_FIELDS}
|
|
2577
3126
|
${MINIFIED_SOURCE_HANDLING}
|
|
2578
3127
|
${IMPACT_ESTIMATION}
|
|
2579
|
-
${
|
|
3128
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2580
3129
|
// src/analysis/prompts/runtime-blocking.ts
|
|
2581
3130
|
var RUNTIME_BLOCKING_PROMPT = `You are a specialist in analyzing Chrome runtime traces to find main-thread blocking operations, event listener leaks, and layout performance issues.
|
|
2582
3131
|
|
|
@@ -2744,16 +3293,16 @@ window.addEventListener('scroll', () => {
|
|
|
2744
3293
|
|
|
2745
3294
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
2746
3295
|
${VERIFICATION_RULES}
|
|
2747
|
-
${
|
|
3296
|
+
${SEVERITY_RULES2}
|
|
2748
3297
|
${FINDING_CATEGORIES}
|
|
2749
3298
|
${OUTPUT_FORMAT}
|
|
2750
3299
|
${STRUCTURED_OUTPUT_FIELDS}
|
|
2751
3300
|
${MINIFIED_SOURCE_HANDLING}
|
|
2752
3301
|
${CROSS_REFERENCING}
|
|
2753
3302
|
${IMPACT_ESTIMATION}
|
|
2754
|
-
${
|
|
3303
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2755
3304
|
// src/analysis/prompts/code-pattern.ts
|
|
2756
|
-
var
|
|
3305
|
+
var CODE_PATTERN_PROMPT2 = `You are a specialist in detecting frontend performance anti-patterns in JavaScript, CSS, and HTML source code.
|
|
2757
3306
|
|
|
2758
3307
|
You have access to a workspace with actual source files captured from a real page load.
|
|
2759
3308
|
|
|
@@ -2905,13 +3454,13 @@ point to as potentially problematic. A typical page has 3-8 issues.
|
|
|
2905
3454
|
|
|
2906
3455
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
2907
3456
|
${VERIFICATION_RULES}
|
|
2908
|
-
${
|
|
3457
|
+
${SEVERITY_RULES2}
|
|
2909
3458
|
${FINDING_CATEGORIES}
|
|
2910
3459
|
${OUTPUT_FORMAT}
|
|
2911
3460
|
${STRUCTURED_OUTPUT_FIELDS}
|
|
2912
3461
|
${MINIFIED_SOURCE_HANDLING}
|
|
2913
3462
|
${IMPACT_ESTIMATION}
|
|
2914
|
-
${
|
|
3463
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2915
3464
|
|
|
2916
3465
|
// src/analysis/prompts.ts
|
|
2917
3466
|
var BROWSER_ORCHESTRATOR_PROMPT = `You are a performance analysis orchestrator.
|
|
@@ -2923,22 +3472,15 @@ var BROWSER_ORCHESTRATOR_PROMPT = `You are a performance analysis orchestrator.
|
|
|
2923
3472
|
For each, set subagent_type and description EXACTLY as written in the
|
|
2924
3473
|
user message. Copy the FULL multi-line description verbatim, including
|
|
2925
3474
|
every file path listed.
|
|
2926
|
-
3. After all 4 subagents return,
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
- Do NOT
|
|
2933
|
-
|
|
2934
|
-
-
|
|
2935
|
-
- Preserve the exact sourceFile, category, severity, beforeCode, and afterCode
|
|
2936
|
-
from each subagent finding. Do NOT rewrite or abbreviate them.
|
|
2937
|
-
- If two subagents report findings for the same script but with DIFFERENT
|
|
2938
|
-
categories (e.g., render-blocking vs frame-blocking-function), include BOTH
|
|
2939
|
-
as separate findings.
|
|
2940
|
-
- Do NOT add your own findings — only include what subagents reported.
|
|
2941
|
-
- Do NOT call read_file, grep, ls, or glob. All analysis is done by subagents.`;
|
|
3475
|
+
3. After all 4 subagents return, respond with: "All subagents complete."
|
|
3476
|
+
|
|
3477
|
+
## CRITICAL rules
|
|
3478
|
+
|
|
3479
|
+
- Do NOT consolidate, re-read, or re-serialize findings. Subagents write
|
|
3480
|
+
their findings to /findings/*.json files directly.
|
|
3481
|
+
- Do NOT add your own findings — all analysis is done by subagents.
|
|
3482
|
+
- Do NOT call read_file, grep, ls, or glob.
|
|
3483
|
+
- Your response should be SHORT — just confirm completion.`;
|
|
2942
3484
|
|
|
2943
3485
|
// src/analysis/agent.ts
|
|
2944
3486
|
function categoriseWorkspaceFiles(workspaceFiles) {
|
|
@@ -3070,7 +3612,7 @@ function buildSubagents(ctx) {
|
|
|
3070
3612
|
{
|
|
3071
3613
|
name: "code-pattern",
|
|
3072
3614
|
description: "Detects frontend code anti-patterns: inline scripts, DOM manipulation in loops, missing event delegation, non-passive listeners.",
|
|
3073
|
-
prompt:
|
|
3615
|
+
prompt: CODE_PATTERN_PROMPT2
|
|
3074
3616
|
}
|
|
3075
3617
|
];
|
|
3076
3618
|
return agentDefs.map(({ name, description, prompt }) => {
|
|
@@ -3085,13 +3627,12 @@ function buildSubagents(ctx) {
|
|
|
3085
3627
|
}
|
|
3086
3628
|
async function analyze(model, backend, spinner, context, { animateProgress = true } = {}) {
|
|
3087
3629
|
const subagents = buildSubagents(context);
|
|
3088
|
-
const agent =
|
|
3630
|
+
const agent = createDeepAgent2({
|
|
3089
3631
|
model,
|
|
3090
3632
|
systemPrompt: BROWSER_ORCHESTRATOR_PROMPT,
|
|
3091
3633
|
backend,
|
|
3092
3634
|
subagents,
|
|
3093
|
-
skills: ["skills/"]
|
|
3094
|
-
responseFormat: toolStrategy(FindingsSchema)
|
|
3635
|
+
skills: ["skills/"]
|
|
3095
3636
|
});
|
|
3096
3637
|
const userMessage = context ? buildBrowserUserMessage(context) : [
|
|
3097
3638
|
"Analyze the frontend performance data in this workspace.",
|
|
@@ -3100,10 +3641,10 @@ async function analyze(model, backend, spinner, context, { animateProgress = tru
|
|
|
3100
3641
|
"to understand the overall picture, then explore source files to verify root causes."
|
|
3101
3642
|
].join(`
|
|
3102
3643
|
`);
|
|
3103
|
-
|
|
3104
|
-
const findings =
|
|
3105
|
-
if (
|
|
3106
|
-
throw new Error(
|
|
3644
|
+
await invokeWithTodoStreaming(agent, userMessage, spinner, { animateProgress });
|
|
3645
|
+
const findings = await mergeFindings(backend);
|
|
3646
|
+
if (findings.length === 0) {
|
|
3647
|
+
throw new Error("Subagents did not write any findings to /findings/*.json");
|
|
3107
3648
|
}
|
|
3108
3649
|
const deduped = deduplicateFindings(findings);
|
|
3109
3650
|
return rankFindings(deduped);
|
|
@@ -3940,7 +4481,6 @@ async function createWorkspace(options) {
|
|
|
3940
4481
|
}
|
|
3941
4482
|
Object.assign(files, DATA_SCRIPTING_SKILL_FILES);
|
|
3942
4483
|
Object.assign(files, BROWSER_ANALYSIS_SKILL_FILES);
|
|
3943
|
-
Object.assign(files, RESULT_WRITER_SKILL_FILES);
|
|
3944
4484
|
const result = await createWorkspaceFromFiles(files);
|
|
3945
4485
|
return {
|
|
3946
4486
|
backend: result.backend,
|
|
@@ -4023,7 +4563,11 @@ var argv = yargs(hideBin(process.argv)).scriptName("zeitzeuge").usage("Usage: $0
|
|
|
4023
4563
|
alias: "o",
|
|
4024
4564
|
type: "string",
|
|
4025
4565
|
default: "zeitzeuge-report.md",
|
|
4026
|
-
describe: "Output path for the
|
|
4566
|
+
describe: "Output path for the report (use .json extension for JSON output)"
|
|
4567
|
+
}).option("format", {
|
|
4568
|
+
type: "string",
|
|
4569
|
+
choices: ["markdown", "json"],
|
|
4570
|
+
describe: "Output format (auto-detected from --output extension if omitted)"
|
|
4027
4571
|
}).help("help", "Show help").alias("h", "help").version(VERSION).strict().parseSync();
|
|
4028
4572
|
function validateUrl(url) {
|
|
4029
4573
|
try {
|
|
@@ -4115,24 +4659,52 @@ async function main() {
|
|
|
4115
4659
|
agentSpinner.succeed(`Analysis complete — ${findings.length} findings`);
|
|
4116
4660
|
} catch (err) {
|
|
4117
4661
|
agentSpinner.fail(`Analysis failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
4662
|
+
console.error(err);
|
|
4118
4663
|
throw new Error(`LLM analysis failed. Check your API key and network connection.
|
|
4119
4664
|
` + (err instanceof Error ? ` Details: ${err.message}` : ""));
|
|
4120
4665
|
} finally {
|
|
4121
4666
|
workspace.cleanup();
|
|
4122
4667
|
}
|
|
4123
|
-
printFindings(findings);
|
|
4124
|
-
printCaptureInfo(heapSummary, captureResult.trace);
|
|
4125
4668
|
const outputPath = argv.output;
|
|
4126
|
-
const
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4669
|
+
const explicitFormat = argv.format;
|
|
4670
|
+
const outputFormat = explicitFormat ?? (outputPath.endsWith(".json") ? "json" : "markdown");
|
|
4671
|
+
if (outputFormat === "json") {
|
|
4672
|
+
const jsonReport = {
|
|
4673
|
+
url,
|
|
4674
|
+
version: VERSION,
|
|
4675
|
+
analyzedAt: new Date().toISOString(),
|
|
4676
|
+
findings,
|
|
4677
|
+
metrics: {
|
|
4678
|
+
loadComplete: captureResult.trace.metrics.loadComplete,
|
|
4679
|
+
firstContentfulPaint: captureResult.trace.metrics.firstContentfulPaint,
|
|
4680
|
+
largestContentfulPaint: captureResult.trace.metrics.largestContentfulPaint,
|
|
4681
|
+
totalBlockingTime: captureResult.trace.metrics.totalBlockingTime,
|
|
4682
|
+
heapTotalSize: heapSummary.metadata.totalSize,
|
|
4683
|
+
heapNodeCount: heapSummary.metadata.nodeCount,
|
|
4684
|
+
detachedDomNodes: heapSummary.detachedNodes.count,
|
|
4685
|
+
networkRequests: captureResult.trace.networkRequests.length,
|
|
4686
|
+
totalTransferSize: captureResult.trace.networkRequests.reduce((s, r) => s + r.encodedSize, 0)
|
|
4687
|
+
}
|
|
4688
|
+
};
|
|
4689
|
+
const { writeFileSync: writeFileSync2 } = await import("node:fs");
|
|
4690
|
+
writeFileSync2(resolve(outputPath), JSON.stringify(jsonReport, null, 2), "utf-8");
|
|
4691
|
+
console.log(`
|
|
4692
|
+
\uD83D\uDCC4 JSON report written to ${resolve(outputPath)}
|
|
4693
|
+
`);
|
|
4694
|
+
} else {
|
|
4695
|
+
printFindings(findings);
|
|
4696
|
+
printCaptureInfo(heapSummary, captureResult.trace);
|
|
4697
|
+
const reportPath = writeReport(resolve(outputPath), {
|
|
4698
|
+
url,
|
|
4699
|
+
version: VERSION,
|
|
4700
|
+
findings,
|
|
4701
|
+
heapSummary,
|
|
4702
|
+
trace: captureResult.trace
|
|
4703
|
+
});
|
|
4704
|
+
console.log(`
|
|
4134
4705
|
\uD83D\uDCC4 Report written to ${reportPath}
|
|
4135
4706
|
`);
|
|
4707
|
+
}
|
|
4136
4708
|
} catch (err) {
|
|
4137
4709
|
printError(err);
|
|
4138
4710
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeitzeuge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A deepagent to witnessing slowdowns in your test runs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"analysis",
|
|
@@ -38,15 +38,16 @@
|
|
|
38
38
|
"test": "bun test"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@langchain/anthropic": "^1.3.
|
|
42
|
-
"@langchain/core": "^1.1.
|
|
41
|
+
"@langchain/anthropic": "^1.3.20",
|
|
42
|
+
"@langchain/core": "^1.1.27",
|
|
43
43
|
"@langchain/node-vfs": "^0.1.2",
|
|
44
|
-
"@langchain/openai": "^1.2.
|
|
44
|
+
"@langchain/openai": "^1.2.9",
|
|
45
|
+
"chalk": "^5.6.2",
|
|
45
46
|
"deepagents": "^1.8.0",
|
|
46
|
-
"langchain": "^1.2.
|
|
47
|
+
"langchain": "^1.2.26",
|
|
47
48
|
"ora": "^9.3.0",
|
|
48
49
|
"picocolors": "^1.1.1",
|
|
49
|
-
"puppeteer-core": "^24.37.
|
|
50
|
+
"puppeteer-core": "^24.37.5",
|
|
50
51
|
"webdriverio": "^9.24.0",
|
|
51
52
|
"yargs": "^18.0.0",
|
|
52
53
|
"zod": "^4.3.6"
|
|
@@ -56,8 +57,8 @@
|
|
|
56
57
|
"@types/yargs": "^17",
|
|
57
58
|
"@zeitzeuge/utils": "workspace:*",
|
|
58
59
|
"lint-staged": "^16.2.7",
|
|
59
|
-
"oxfmt": "^0.
|
|
60
|
-
"oxlint": "^1.
|
|
60
|
+
"oxfmt": "^0.35.0",
|
|
61
|
+
"oxlint": "^1.50.0",
|
|
61
62
|
"simple-git-hooks": "^2.13.1",
|
|
62
63
|
"typescript": "^5"
|
|
63
64
|
},
|