zeitzeuge 0.8.2 → 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 +672 -59
- 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.
|
|
659
733
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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.
|
|
739
|
+
|
|
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
|
|
663
745
|
|
|
664
|
-
|
|
665
|
-
- Title, category, severity, sourceFile, lineNumber
|
|
666
|
-
- Full description of the issue
|
|
667
|
-
- Complete beforeCode (verbatim from the source file)
|
|
668
|
-
- Complete afterCode (working drop-in replacement)
|
|
746
|
+
When your analysis is complete, you MUST write ALL findings to a JSON file using write_file.
|
|
669
747
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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).
|
|
751
|
+
|
|
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.
|
|
757
|
+
|
|
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,6 +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
|
};
|
|
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";
|
|
2005
|
+
|
|
2006
|
+
// ../utils/src/profiling/prompts/test-shared.ts
|
|
2007
|
+
var SEVERITY_RULES = `## Severity classification
|
|
2008
|
+
|
|
2009
|
+
Assign severity based on the nature and measured impact of the issue:
|
|
2010
|
+
|
|
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
|
+
}
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
\`\`\`
|
|
2283
|
+
|
|
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
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
\`\`\`
|
|
2299
|
+
|
|
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) { /* ... */ }
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
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.
|
|
2478
|
+
|
|
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}`;
|
|
1909
2499
|
// ../utils/src/output/terminal.ts
|
|
1910
2500
|
import pc2 from "picocolors";
|
|
1911
2501
|
import ora from "ora";
|
|
@@ -2230,8 +2820,7 @@ function generateMarkdown(options) {
|
|
|
2230
2820
|
`);
|
|
2231
2821
|
}
|
|
2232
2822
|
// src/analysis/agent.ts
|
|
2233
|
-
import { createDeepAgent } from "deepagents";
|
|
2234
|
-
import { toolStrategy } from "langchain";
|
|
2823
|
+
import { createDeepAgent as createDeepAgent2 } from "deepagents";
|
|
2235
2824
|
|
|
2236
2825
|
// src/analysis/prompts/shared.ts
|
|
2237
2826
|
var BROWSER_TOOL_CALL_STRATEGY = `## CRITICAL: Tool call strategy — scripts first, source selectively
|
|
@@ -2274,7 +2863,7 @@ When source code is minified/compiled:
|
|
|
2274
2863
|
Only provide \`afterCode\` when the source is clearly human-authored (readable
|
|
2275
2864
|
variable names, formatting, comments) — e.g. inline scripts in HTML or
|
|
2276
2865
|
un-minified CSS.`;
|
|
2277
|
-
var
|
|
2866
|
+
var SEVERITY_RULES2 = `## Severity classification
|
|
2278
2867
|
|
|
2279
2868
|
Assign severity based on measured impact — do NOT guess:
|
|
2280
2869
|
|
|
@@ -2429,14 +3018,14 @@ These are THREE separate findings, not one.
|
|
|
2429
3018
|
|
|
2430
3019
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
2431
3020
|
${VERIFICATION_RULES}
|
|
2432
|
-
${
|
|
3021
|
+
${SEVERITY_RULES2}
|
|
2433
3022
|
${FINDING_CATEGORIES}
|
|
2434
3023
|
${OUTPUT_FORMAT}
|
|
2435
3024
|
${STRUCTURED_OUTPUT_FIELDS}
|
|
2436
3025
|
${MINIFIED_SOURCE_HANDLING}
|
|
2437
3026
|
${CROSS_REFERENCING}
|
|
2438
3027
|
${IMPACT_ESTIMATION}
|
|
2439
|
-
${
|
|
3028
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2440
3029
|
// src/analysis/prompts/page-load.ts
|
|
2441
3030
|
var PAGE_LOAD_PROMPT = `You are a specialist in analyzing page load performance, render-blocking resources, and network waterfall patterns.
|
|
2442
3031
|
|
|
@@ -2530,13 +3119,13 @@ Assets served without compression or with missing cache headers.
|
|
|
2530
3119
|
|
|
2531
3120
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
2532
3121
|
${VERIFICATION_RULES}
|
|
2533
|
-
${
|
|
3122
|
+
${SEVERITY_RULES2}
|
|
2534
3123
|
${FINDING_CATEGORIES}
|
|
2535
3124
|
${OUTPUT_FORMAT}
|
|
2536
3125
|
${STRUCTURED_OUTPUT_FIELDS}
|
|
2537
3126
|
${MINIFIED_SOURCE_HANDLING}
|
|
2538
3127
|
${IMPACT_ESTIMATION}
|
|
2539
|
-
${
|
|
3128
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2540
3129
|
// src/analysis/prompts/runtime-blocking.ts
|
|
2541
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.
|
|
2542
3131
|
|
|
@@ -2704,16 +3293,16 @@ window.addEventListener('scroll', () => {
|
|
|
2704
3293
|
|
|
2705
3294
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
2706
3295
|
${VERIFICATION_RULES}
|
|
2707
|
-
${
|
|
3296
|
+
${SEVERITY_RULES2}
|
|
2708
3297
|
${FINDING_CATEGORIES}
|
|
2709
3298
|
${OUTPUT_FORMAT}
|
|
2710
3299
|
${STRUCTURED_OUTPUT_FIELDS}
|
|
2711
3300
|
${MINIFIED_SOURCE_HANDLING}
|
|
2712
3301
|
${CROSS_REFERENCING}
|
|
2713
3302
|
${IMPACT_ESTIMATION}
|
|
2714
|
-
${
|
|
3303
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2715
3304
|
// src/analysis/prompts/code-pattern.ts
|
|
2716
|
-
var
|
|
3305
|
+
var CODE_PATTERN_PROMPT2 = `You are a specialist in detecting frontend performance anti-patterns in JavaScript, CSS, and HTML source code.
|
|
2717
3306
|
|
|
2718
3307
|
You have access to a workspace with actual source files captured from a real page load.
|
|
2719
3308
|
|
|
@@ -2865,13 +3454,13 @@ point to as potentially problematic. A typical page has 3-8 issues.
|
|
|
2865
3454
|
|
|
2866
3455
|
${BROWSER_TOOL_CALL_STRATEGY}
|
|
2867
3456
|
${VERIFICATION_RULES}
|
|
2868
|
-
${
|
|
3457
|
+
${SEVERITY_RULES2}
|
|
2869
3458
|
${FINDING_CATEGORIES}
|
|
2870
3459
|
${OUTPUT_FORMAT}
|
|
2871
3460
|
${STRUCTURED_OUTPUT_FIELDS}
|
|
2872
3461
|
${MINIFIED_SOURCE_HANDLING}
|
|
2873
3462
|
${IMPACT_ESTIMATION}
|
|
2874
|
-
${
|
|
3463
|
+
${WRITE_FINDINGS_REQUIREMENT}`;
|
|
2875
3464
|
|
|
2876
3465
|
// src/analysis/prompts.ts
|
|
2877
3466
|
var BROWSER_ORCHESTRATOR_PROMPT = `You are a performance analysis orchestrator.
|
|
@@ -2883,22 +3472,15 @@ var BROWSER_ORCHESTRATOR_PROMPT = `You are a performance analysis orchestrator.
|
|
|
2883
3472
|
For each, set subagent_type and description EXACTLY as written in the
|
|
2884
3473
|
user message. Copy the FULL multi-line description verbatim, including
|
|
2885
3474
|
every file path listed.
|
|
2886
|
-
3. After all 4 subagents return,
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
- Do NOT
|
|
2893
|
-
|
|
2894
|
-
-
|
|
2895
|
-
- Preserve the exact sourceFile, category, severity, beforeCode, and afterCode
|
|
2896
|
-
from each subagent finding. Do NOT rewrite or abbreviate them.
|
|
2897
|
-
- If two subagents report findings for the same script but with DIFFERENT
|
|
2898
|
-
categories (e.g., render-blocking vs frame-blocking-function), include BOTH
|
|
2899
|
-
as separate findings.
|
|
2900
|
-
- Do NOT add your own findings — only include what subagents reported.
|
|
2901
|
-
- 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.`;
|
|
2902
3484
|
|
|
2903
3485
|
// src/analysis/agent.ts
|
|
2904
3486
|
function categoriseWorkspaceFiles(workspaceFiles) {
|
|
@@ -3030,7 +3612,7 @@ function buildSubagents(ctx) {
|
|
|
3030
3612
|
{
|
|
3031
3613
|
name: "code-pattern",
|
|
3032
3614
|
description: "Detects frontend code anti-patterns: inline scripts, DOM manipulation in loops, missing event delegation, non-passive listeners.",
|
|
3033
|
-
prompt:
|
|
3615
|
+
prompt: CODE_PATTERN_PROMPT2
|
|
3034
3616
|
}
|
|
3035
3617
|
];
|
|
3036
3618
|
return agentDefs.map(({ name, description, prompt }) => {
|
|
@@ -3045,13 +3627,12 @@ function buildSubagents(ctx) {
|
|
|
3045
3627
|
}
|
|
3046
3628
|
async function analyze(model, backend, spinner, context, { animateProgress = true } = {}) {
|
|
3047
3629
|
const subagents = buildSubagents(context);
|
|
3048
|
-
const agent =
|
|
3630
|
+
const agent = createDeepAgent2({
|
|
3049
3631
|
model,
|
|
3050
3632
|
systemPrompt: BROWSER_ORCHESTRATOR_PROMPT,
|
|
3051
3633
|
backend,
|
|
3052
3634
|
subagents,
|
|
3053
|
-
skills: ["skills/"]
|
|
3054
|
-
responseFormat: toolStrategy(FindingsSchema)
|
|
3635
|
+
skills: ["skills/"]
|
|
3055
3636
|
});
|
|
3056
3637
|
const userMessage = context ? buildBrowserUserMessage(context) : [
|
|
3057
3638
|
"Analyze the frontend performance data in this workspace.",
|
|
@@ -3060,10 +3641,10 @@ async function analyze(model, backend, spinner, context, { animateProgress = tru
|
|
|
3060
3641
|
"to understand the overall picture, then explore source files to verify root causes."
|
|
3061
3642
|
].join(`
|
|
3062
3643
|
`);
|
|
3063
|
-
|
|
3064
|
-
const findings =
|
|
3065
|
-
if (
|
|
3066
|
-
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");
|
|
3067
3648
|
}
|
|
3068
3649
|
const deduped = deduplicateFindings(findings);
|
|
3069
3650
|
return rankFindings(deduped);
|
|
@@ -3982,7 +4563,11 @@ var argv = yargs(hideBin(process.argv)).scriptName("zeitzeuge").usage("Usage: $0
|
|
|
3982
4563
|
alias: "o",
|
|
3983
4564
|
type: "string",
|
|
3984
4565
|
default: "zeitzeuge-report.md",
|
|
3985
|
-
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)"
|
|
3986
4571
|
}).help("help", "Show help").alias("h", "help").version(VERSION).strict().parseSync();
|
|
3987
4572
|
function validateUrl(url) {
|
|
3988
4573
|
try {
|
|
@@ -4074,24 +4659,52 @@ async function main() {
|
|
|
4074
4659
|
agentSpinner.succeed(`Analysis complete — ${findings.length} findings`);
|
|
4075
4660
|
} catch (err) {
|
|
4076
4661
|
agentSpinner.fail(`Analysis failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
4662
|
+
console.error(err);
|
|
4077
4663
|
throw new Error(`LLM analysis failed. Check your API key and network connection.
|
|
4078
4664
|
` + (err instanceof Error ? ` Details: ${err.message}` : ""));
|
|
4079
4665
|
} finally {
|
|
4080
4666
|
workspace.cleanup();
|
|
4081
4667
|
}
|
|
4082
|
-
printFindings(findings);
|
|
4083
|
-
printCaptureInfo(heapSummary, captureResult.trace);
|
|
4084
4668
|
const outputPath = argv.output;
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
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(`
|
|
4093
4705
|
\uD83D\uDCC4 Report written to ${reportPath}
|
|
4094
4706
|
`);
|
|
4707
|
+
}
|
|
4095
4708
|
} catch (err) {
|
|
4096
4709
|
printError(err);
|
|
4097
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
|
},
|