zeitzeuge 0.7.4 → 0.8.1

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.
Files changed (2) hide show
  1. package/dist/cli.js +1297 -178
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -602,28 +602,31 @@ var VERIFICATION_RULES = `## Verification rules (mandatory for every finding)
602
602
  2. **Copy code verbatim** — beforeCode must be copied exactly from the file you
603
603
  read, not paraphrased. Line numbers must match what you observed.
604
604
  3. **Provide a working fix** — afterCode must be a complete drop-in replacement
605
- that compiles, preserves the function signature, and only fixes the perf issue.
606
- 4. **Never omit beforeCode/afterCode** — every finding MUST have both fields set.`;
605
+ that compiles, preserves the EXACT function signature, and only fixes the perf issue.
606
+ 4. **Never omit beforeCode/afterCode** — every finding MUST have both fields set.
607
+ 5. **Do NOT change sync to async** — if a function is synchronous, afterCode must
608
+ also be synchronous. Replace the inefficient implementation with a faster sync
609
+ alternative (e.g., use crypto.createHash instead of a manual loop). Mention the
610
+ async alternative in the description, but keep afterCode as a sync drop-in.`;
607
611
  var OUTPUT_FORMAT = `## Output requirements
608
612
 
609
- - Report ALL findings you discover — typically 3–8 per subagent. Do NOT
610
- stop at 2-3 findings. Exhaustively analyze every function in every file.
613
+ - Report ALL findings within YOUR scope — typically 3–5 per subagent.
614
+ Exhaustively analyze every function but stay within your assigned categories.
611
615
  - Each finding MUST have sourceFile, beforeCode, and afterCode
612
616
  - Be specific — name exact files, functions, and line numbers
613
617
  - Provide concrete code-level fixes, not generic advice
618
+ - Do NOT report findings about test files — only about application source files
614
619
 
615
620
  ### CRITICAL: Multiple findings per function and per file
616
621
 
617
- - A single function CAN have multiple distinct issues report each as a
618
- SEPARATE finding. For example, hashPassword() might both block the event
619
- loop AND allocate a TextEncoder on every call — these are TWO findings
620
- with different categories.
622
+ - A single function CAN have multiple distinct issues WITHIN YOUR SCOPE
623
+ report each as a SEPARATE finding with a different category.
621
624
  - A single file often has MANY issues across different functions. Read the
622
- ENTIRE file top-to-bottom and report EVERY issue you find, not just the
623
- first one.
625
+ ENTIRE file top-to-bottom and report EVERY issue you find within your scope.
624
626
  - If function A calls function B and both have issues, report findings for
625
627
  BOTH functions separately.
626
- - Do NOT skip issues you consider "minor" — report them with severity: info.`;
628
+ - Do NOT skip issues you consider "minor" — report them with severity: info.
629
+ - Do NOT report issues that belong to another subagent's scope.`;
627
630
  var FINDING_CATEGORIES = `## Finding categories
628
631
 
629
632
  Each finding MUST use one of these EXACT category values — do NOT invent new categories:
@@ -671,7 +674,7 @@ var STRUCTURED_OUTPUT_FIELDS = `## Structured output fields — REQUIRED for eve
671
674
 
672
675
  Every finding MUST include ALL of these fields:
673
676
 
674
- - \`sourceFile\` — (REQUIRED) the workspace path (e.g. /src/utils/parser.ts or /scripts/app.js)
677
+ - \`sourceFile\` — (REQUIRED) the workspace path (e.g. src/utils/parser.ts or scripts/app.js)
675
678
  - \`lineNumber\` — (REQUIRED) the 1-based line number, verified by reading the file
676
679
  - \`confidence\` — \`high\` if you read the source, \`medium\` if strongly suggested,
677
680
  \`low\` if inferred
@@ -695,17 +698,46 @@ Every finding MUST include ALL of these fields:
695
698
  Copy the COMPLETE function (or the complete relevant section of 5-30 lines).
696
699
  Do NOT use "..." or "// ..." to skip lines. Include the full code block.
697
700
  - afterCode must be a COMPLETE, WORKING replacement for the beforeCode block:
698
- - Same function signature, same exports, same return type
701
+ - SAME function signature — same name, same parameters, same return type
702
+ - SAME sync/async — if the original is sync, afterCode MUST be sync. Do NOT
703
+ add async/await, Promises, or callbacks. Replace the slow implementation
704
+ with a faster synchronous alternative instead.
705
+ - SAME exports — if the function is exported, afterCode must also export it
699
706
  - Must compile and produce identical behavior except for the performance fix
700
707
  - Include ALL the code from beforeCode, not just the changed lines
701
708
  - If the fix requires adding a module-level constant (e.g., hoisting a RegExp or
702
- TextEncoder), include that declaration in afterCode
703
- - For blocking operations: the fix should actually make the operation non-blocking
704
- (e.g., use async APIs, yield to the event loop, or use workers)
705
- - For excessive instantiation: hoist the construction to module level and reuse it
709
+ TextEncoder), include that declaration ABOVE the function in afterCode
710
+ - For blocking CPU loops: replace with a faster sync algorithm (e.g., use
711
+ crypto.createHash() instead of a manual loop). Mention async alternatives
712
+ in the finding description, not in afterCode.
713
+ - For excessive instantiation: hoist the construction to module level and reuse it.
714
+ Show the module-level const AND the modified function in afterCode.
715
+ - For listener leaks: show the fix (e.g., .once() instead of .on(), or return
716
+ an unsubscribe function). The beforeCode/afterCode should show the same function
717
+ with only the listener fix changed.
706
718
  - afterCode must NOT be a diff, pseudocode, or description of changes
707
719
  - If you cannot provide a concrete fix, still include beforeCode and describe
708
- the fix approach in afterCode as a code comment within the actual code`;
720
+ the fix approach in afterCode as a code comment within the actual code
721
+
722
+ ### Code fix quality rules
723
+
724
+ 1. **Named functions for event handlers**: NEVER use anonymous functions with
725
+ .on() or .addEventListener(). Always define a named function or const so
726
+ it can be removed with .off(event, handler). Example:
727
+ - BAD: emitter.on('change', () => { cache = null; })
728
+ - GOOD: const invalidateCache = () => { cache = null; };
729
+ emitter.on('change', invalidateCache);
730
+ 2. **Surgical listener removal**: Use .off(event, specificHandler) instead of
731
+ .removeAllListeners(). The cleanup/reset function must remove the EXACT
732
+ handler that was added.
733
+ 3. **Complete guard logic**: If you add a guard flag (e.g., listenerRegistered),
734
+ the cleanup function MUST reset the flag AND remove the specific listener.
735
+ 4. **Include surrounding context**: If the fix adds module-level variables
736
+ (guard flags, hoisted constants, named handlers), include ALL of them in
737
+ afterCode so it is self-contained.
738
+ 5. **Preserve existing functions**: If the original file has a cleanup/reset
739
+ function, update it in afterCode to properly undo whatever your fix added.
740
+ Do NOT ignore existing cleanup functions.`;
709
741
  // ../utils/src/prompts/file-list.ts
710
742
  function buildFileListPromptSection(config) {
711
743
  const { dataFiles, sourceFiles, testFiles, additionalSections } = config;
@@ -762,29 +794,1158 @@ function insertFileListIntoPrompt(prompt, fileSection) {
762
794
  ` + prompt.slice(firstHeadingIdx);
763
795
  }
764
796
  // ../utils/src/workspace/builder.ts
765
- import { FilesystemBackend } from "deepagents";
766
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
767
- import { join } from "node:path";
768
- import { tmpdir } from "node:os";
769
- function createWorkspaceFromFiles(files, prefix = "zeitzeuge-workspace-") {
770
- const tempDir = mkdtempSync(join(tmpdir(), prefix));
771
- for (const [filePath, content] of Object.entries(files)) {
772
- const relPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
773
- const fullPath = join(tempDir, relPath);
774
- mkdirSync(join(fullPath, ".."), { recursive: true });
775
- writeFileSync(fullPath, content, "utf-8");
776
- }
777
- const backend = new FilesystemBackend({
778
- rootDir: tempDir,
779
- virtualMode: true
780
- });
781
- const cleanup = () => {
797
+ import { VfsSandbox } from "@langchain/node-vfs";
798
+
799
+ class PerfAgentSandbox extends VfsSandbox {
800
+ static #toRelative(p) {
801
+ const stripped = p.startsWith("/") ? p.slice(1) : p;
802
+ return stripped || ".";
803
+ }
804
+ async read(filePath, offset = 0, limit = 500) {
805
+ return super.read(PerfAgentSandbox.#toRelative(filePath), offset, limit);
806
+ }
807
+ async lsInfo(dirPath) {
808
+ return super.lsInfo(PerfAgentSandbox.#toRelative(dirPath));
809
+ }
810
+ async grepRaw(pattern, searchPath = "/", glob = null) {
811
+ return super.grepRaw(pattern, PerfAgentSandbox.#toRelative(searchPath), glob);
812
+ }
813
+ async globInfo(pattern, searchPath = "/") {
814
+ return super.globInfo(pattern, PerfAgentSandbox.#toRelative(searchPath));
815
+ }
816
+ static async create(options) {
817
+ const sandbox = new PerfAgentSandbox(options);
818
+ await sandbox.initialize();
819
+ return sandbox;
820
+ }
821
+ }
822
+ async function createWorkspaceFromFiles(files) {
823
+ const sandbox = await PerfAgentSandbox.create({ initialFiles: files });
824
+ const cleanup = async () => {
782
825
  try {
783
- rmSync(tempDir, { recursive: true, force: true });
826
+ await sandbox.stop();
784
827
  } catch {}
785
828
  };
786
- return { backend, cleanup, tempDir };
829
+ return { backend: sandbox, cleanup };
830
+ }
831
+ // ../utils/src/skills/data-scripting.ts
832
+ var SKILL_MD = `---
833
+ name: data-scripting
834
+ description: Use this skill when you need to analyze JSON data files in the workspace. Provides instructions for writing Node.js scripts to query, filter, aggregate, and cross-reference data files instead of reading them raw. Includes helper scripts and data file schemas.
835
+ ---
836
+
837
+ # Data Scripting
838
+
839
+ ## Overview
840
+
841
+ You have a full Node.js runtime available via execute_command. Instead of
842
+ reading large JSON data files with read_file (which consumes many tokens),
843
+ write short scripts that extract exactly what you need.
844
+
845
+ ## When to use scripts vs. read_file
846
+
847
+ - **read_file**: Source code files you need to see verbatim for
848
+ beforeCode/afterCode, or small files (<50 lines)
849
+ - **Scripts**: JSON data files, any file >100 lines, cross-referencing
850
+ multiple files, computing aggregations, filtering by thresholds
851
+
852
+ ## How to run a script
853
+
854
+ IMPORTANT: All file paths in scripts must use relative paths (no leading
855
+ \`/\`). The execute_command tool runs with the workspace as the current
856
+ directory, so \`fs.readFileSync('hot-functions/application.json')\` resolves
857
+ to \`<workspace>/hot-functions/application.json\`.
858
+
859
+ Option 1 — inline:
860
+ execute_command: node -e "
861
+ const data = JSON.parse(require('fs').readFileSync('path/to/file', 'utf8'));
862
+ const results = data.filter(x => x.duration > 100);
863
+ console.log(JSON.stringify(results, null, 2));
864
+ "
865
+
866
+ Option 2 — use a pre-built helper:
867
+ execute_command: node skills/data-scripting/helpers/top-items.js hot-functions/application.json selfTime 10
868
+
869
+ Option 3 — write a custom script:
870
+ write_file: tmp/my-analysis.js
871
+ execute_command: node tmp/my-analysis.js
872
+
873
+ ## Pre-built helper scripts
874
+
875
+ ### skills/data-scripting/helpers/top-items.js
876
+ Usage: \`node top-items.js <file> <sortField> [limit]\`
877
+ Reads a JSON array, sorts by the given field descending, prints top N items.
878
+
879
+ ### skills/data-scripting/helpers/cross-reference.js
880
+ Usage: \`node cross-reference.js <file1> <field1> <file2> <field2>\`
881
+ Finds items in file1 whose field1 value also appears as field2 in file2.
882
+
883
+ ## Data file schemas
884
+
885
+ See skills/data-scripting/schemas.md for the JSON structure of every
886
+ data file in this workspace. Read it before writing custom scripts.
887
+ `;
888
+ var SCHEMAS_MD = `# Workspace Data File Schemas
889
+
890
+ ---
891
+ # Workspace files
892
+ ---
893
+
894
+ ## summary.json
895
+ {
896
+ totalTests: number,
897
+ totalDuration: number, // ms
898
+ passCount: number,
899
+ failCount: number,
900
+ profileCount: number,
901
+ slowestFile: string | null, // file:// URL
902
+ slowestFileDuration: number,
903
+ totalGcTime: number,
904
+ gcPercentage: number
905
+ }
906
+
907
+ ## hot-functions/application.json
908
+ Array of application-code hot functions (filtered to sourceCategory "application"):
909
+ {
910
+ functionName: string,
911
+ workspacePath: string, // e.g. "/src/services/crypto.ts" (has leading /)
912
+ lineNumber: number,
913
+ columnNumber: number,
914
+ selfTime: number, // ms of CPU self-time
915
+ totalTime: number, // ms including callees
916
+ hitCount: number,
917
+ selfPercent: number, // % of total profile duration
918
+ sourceCategory: "application",
919
+ sourceSnippet?: string, // source code context around the hot line
920
+ callerChain?: [{ functionName, workspacePath, lineNumber }]
921
+ }
922
+
923
+ ## hot-functions/dependencies.json
924
+ Same structure as hot-functions/application.json but sourceCategory "dependency".
925
+
926
+ ## hot-functions/global.json
927
+ All hot functions across all categories (application, dependency, framework, unknown).
928
+ Same item structure. Can be very large (2000+ lines).
929
+
930
+ ## scripts/application.json
931
+ Per-script summary for application code:
932
+ [{ workspacePath: string, selfTime: number, selfPercent: number, functionCount: number }]
933
+
934
+ ## scripts/dependencies.json
935
+ Same structure as scripts/application.json but for dependency scripts.
936
+
937
+ ## src/index.json
938
+ Maps source file paths to their hot functions. Key = workspacePath, value = array:
939
+ {
940
+ "/src/services/notification-service.ts": [
941
+ { functionName: string, lineNumber: number, selfTime: number, selfPercent: number }
942
+ ],
943
+ "/src/utils/crypto.ts": [...]
944
+ }
945
+ Use this to know WHICH source files have CPU-hot functions.
946
+
947
+ ## listener-tracking.json
948
+ {
949
+ eventTargetCounts: {}, // browser EventTarget counts (usually empty in Node)
950
+ emitterCounts: { // keyed by event name
951
+ "<eventName>": { addCount: number, removeCount: number },
952
+ ...
953
+ },
954
+ exceedances: [{ // maxListeners threshold exceeded
955
+ targetType: string, // e.g. "EventEmitter"
956
+ eventType: string, // e.g. "task:changed"
957
+ listenerCount: number, // current count that exceeded threshold
958
+ threshold: number, // the maxListeners value (default 10)
959
+ stack: string // stack trace showing where listener was added
960
+ }]
961
+ }
962
+
963
+ ## metrics/current.json
964
+ Comprehensive pre-computed metrics (large file):
965
+ {
966
+ version: number,
967
+ timestamp: string,
968
+ suite: { totalDuration, totalTests, passCount, failCount, averageTestDuration,
969
+ medianTestDuration, p95TestDuration, slowestTestDuration, slowestTestName },
970
+ cpu: { gcPercentage, gcTime, idlePercentage, idleTime, applicationTime,
971
+ applicationPercent, dependencyTime, dependencyPercent, testFrameworkTime,
972
+ testFrameworkPercent },
973
+ files: { "<file:// URL>": { duration, testCount, setupTime, gcPercentage } },
974
+ tests: { "<file::testName>": { duration, status } },
975
+ hotFunctions: [{ key, functionName, scriptUrl, lineNumber, selfTime, selfPercent,
976
+ sourceCategory }],
977
+ listenerTracking: { eventTargetCounts, emitterCounts, exceedances }
978
+ }
979
+
980
+ ## timing/overview.json
981
+ Array of per-file timing data:
982
+ [{
983
+ file: string, // file:// URL
984
+ duration: number,
985
+ testCount: number,
986
+ passCount: number,
987
+ failCount: number,
988
+ setupTime: number,
989
+ tests: [{ name: string, duration: number, status: string }]
990
+ }]
991
+
992
+ ## timing/slow-tests.json
993
+ Array of slow tests sorted by duration descending:
994
+ [{ file: string, name: string, duration: number }]
995
+
996
+ ## profiles/index.json
997
+ Manifest mapping test files to their CPU profile paths:
998
+ [{ testFile: string, profilePath: string }]
999
+
1000
+ ## profiles/<file>.json
1001
+ Per-test-file profile summary:
1002
+ {
1003
+ profilePath: string,
1004
+ duration: number,
1005
+ sampleCount: number,
1006
+ hotFunctions: [{
1007
+ functionName, lineNumber, columnNumber, selfTime, totalTime,
1008
+ hitCount, selfPercent, callerChain, sourceCategory, workspacePath
1009
+ }]
1010
+ }
1011
+
1012
+ ---
1013
+ # Browser workspace files (CLI agent only — not present in Vitest workspaces)
1014
+ ---
1015
+
1016
+ ## heap/summary.json
1017
+ {
1018
+ metadata: { url, capturedAt, totalSize, nodeCount, edgeCount },
1019
+ largestObjects: [{ name, type, selfSize, retainedSize, retainerPath: string[] }],
1020
+ typeStats: [{ type, count, totalSize, avgSize }],
1021
+ constructorStats: [{ constructor, count, totalSize, avgSize }],
1022
+ detachedNodes: { count, totalSize, examples: [{ name, retainerPath }] },
1023
+ closureStats: { count, totalSize, topClosures: [{ name, contextSize, retainerPath }] }
1024
+ }
1025
+
1026
+ ## trace/summary.json
1027
+ {
1028
+ url: string,
1029
+ timing: { loadComplete, firstContentfulPaint, largestContentfulPaint, totalBlockingTime, longTasks: [...] },
1030
+ requestCount: number,
1031
+ totalTransferSize: number,
1032
+ totalDecodedSize: number,
1033
+ renderBlockingResources: [{ url, type, size, duration, path }],
1034
+ resourceBreakdown: { scripts: { count, totalSize }, stylesheets: {...}, fonts: {...}, images: {...}, other: {...} }
1035
+ }
1036
+
1037
+ ## trace/runtime/blocking-functions.json
1038
+ Array of (up to 50 entries, sorted by duration descending):
1039
+ {
1040
+ functionName: string,
1041
+ scriptUrl: string, // URL of the script
1042
+ lineNumber: number,
1043
+ columnNumber: number,
1044
+ duration: number, // ms blocked on main thread
1045
+ startTime: number, // ms relative to navigation start
1046
+ callStack: [{ // caller chain (array of objects, NOT strings)
1047
+ functionName: string,
1048
+ scriptUrl: string,
1049
+ lineNumber: number
1050
+ }],
1051
+ category: string // "scripting" | "layout" | "paint" | etc.
1052
+ }
1053
+ To get the workspace file path for a scriptUrl, extract the filename:
1054
+ e.g. "https://example.com/static/abc123.js" -> "scripts/abc123.js"
1055
+
1056
+ ## trace/runtime/event-listeners.json
1057
+ Array of (only listeners with addCount > 0):
1058
+ {
1059
+ eventType: string,
1060
+ targetType: string,
1061
+ addCount: number,
1062
+ removeCount: number,
1063
+ activeCount: number,
1064
+ stackSnippets: string[]
1065
+ }
1066
+
1067
+ ## trace/runtime/summary.json
1068
+ {
1069
+ totalEvents: number,
1070
+ traceDuration: number, // ms
1071
+ mainThreadId: number,
1072
+ frameBreakdown: { scripting, layout, paint, gc, other }, // all in ms
1073
+ blockingFunctionCount: number,
1074
+ listenerImbalances: number,
1075
+ gcPauseCount: number,
1076
+ gcTotalDuration: number, // ms
1077
+ frequentEventTypes: string[] // event types dispatched >10 times
1078
+ }
1079
+
1080
+ ## trace/runtime/frame-breakdown.json
1081
+ {
1082
+ scripting: number, // ms spent in script execution
1083
+ layout: number, // ms spent in layout calculations
1084
+ paint: number, // ms spent painting
1085
+ gc: number, // ms spent in garbage collection
1086
+ other: number // ms spent in other tasks
1087
+ }
1088
+
1089
+ ## trace/network-waterfall.json
1090
+ Array of (sorted by startTime):
1091
+ {
1092
+ url: string,
1093
+ type: string, // "Script" | "Stylesheet" | "Font" | "Document" | "Image"
1094
+ status: number,
1095
+ size: number, // decoded size in bytes
1096
+ startTime: number, // ms from navigation start
1097
+ endTime: number,
1098
+ duration: number,
1099
+ isRenderBlocking: boolean,
1100
+ priority: string,
1101
+ path: string | null // workspace path to stored content (e.g. "/scripts/abc.js")
1102
+ }
1103
+
1104
+ ## trace/asset-manifest.json
1105
+ Array of all network assets:
1106
+ {
1107
+ url: string,
1108
+ type: string,
1109
+ size: number, // decoded size in bytes
1110
+ duration: number,
1111
+ isRenderBlocking: boolean,
1112
+ stored: boolean, // true if content was captured and stored
1113
+ path: string | null // workspace path if stored
1114
+ }
1115
+
1116
+ ## trace/runtime/raw-events.json
1117
+ Array of raw Chrome trace events (can be very large). Each entry has:
1118
+ {
1119
+ name: string, // event name (e.g. "FunctionCall", "Layout", "GCEvent")
1120
+ cat: string, // category
1121
+ ph: string, // phase ("X" = complete, "B"/"E" = begin/end)
1122
+ ts: number, // timestamp in microseconds
1123
+ dur: number, // duration in microseconds
1124
+ tid: number, // thread ID
1125
+ pid: number, // process ID
1126
+ args: object // event-specific arguments
1127
+ }
1128
+ Only use for deep investigation — prefer the summary files first.
1129
+ `;
1130
+ var TOP_ITEMS_JS = `'use strict';
1131
+
1132
+ const fs = require('fs');
1133
+
1134
+ const [filePath, sortField, limitArg] = process.argv.slice(2);
1135
+ if (!filePath || !sortField) {
1136
+ console.error('Usage: node top-items.js <file> <sortField> [limit]');
1137
+ process.exit(1);
1138
+ }
1139
+
1140
+ const limit = parseInt(limitArg, 10) || 10;
1141
+
1142
+ let raw;
1143
+ try {
1144
+ raw = fs.readFileSync(filePath, 'utf8');
1145
+ } catch (err) {
1146
+ console.error(\`Error reading \${filePath}: \${err.message}\`);
1147
+ process.exit(1);
787
1148
  }
1149
+
1150
+ let data;
1151
+ try {
1152
+ data = JSON.parse(raw);
1153
+ } catch (err) {
1154
+ console.error(\`Error parsing JSON from \${filePath}: \${err.message}\`);
1155
+ process.exit(1);
1156
+ }
1157
+
1158
+ if (!Array.isArray(data)) {
1159
+ console.error(\`Expected a JSON array in \${filePath}, got \${typeof data}\`);
1160
+ process.exit(1);
1161
+ }
1162
+
1163
+ if (data.length > 0 && !(sortField in data[0])) {
1164
+ console.error(\`Field "\${sortField}" not found in items. Available fields: \${Object.keys(data[0]).join(', ')}\`);
1165
+ process.exit(1);
1166
+ }
1167
+
1168
+ const sorted = data
1169
+ .slice()
1170
+ .sort((a, b) => (Number(b[sortField]) || 0) - (Number(a[sortField]) || 0))
1171
+ .slice(0, limit);
1172
+
1173
+ console.log(JSON.stringify(sorted, null, 2));
1174
+ `;
1175
+ var CROSS_REFERENCE_JS = `'use strict';
1176
+
1177
+ const fs = require('fs');
1178
+
1179
+ const [file1Path, field1, file2Path, field2] = process.argv.slice(2);
1180
+ if (!file1Path || !field1 || !file2Path || !field2) {
1181
+ console.error('Usage: node cross-reference.js <file1> <field1> <file2> <field2>');
1182
+ process.exit(1);
1183
+ }
1184
+
1185
+ function readJsonArray(filePath) {
1186
+ let raw;
1187
+ try {
1188
+ raw = fs.readFileSync(filePath, 'utf8');
1189
+ } catch (err) {
1190
+ console.error(\`Error reading \${filePath}: \${err.message}\`);
1191
+ process.exit(1);
1192
+ }
1193
+ let data;
1194
+ try {
1195
+ data = JSON.parse(raw);
1196
+ } catch (err) {
1197
+ console.error(\`Error parsing JSON from \${filePath}: \${err.message}\`);
1198
+ process.exit(1);
1199
+ }
1200
+ if (!Array.isArray(data)) {
1201
+ console.error(\`Expected a JSON array in \${filePath}, got \${typeof data}\`);
1202
+ process.exit(1);
1203
+ }
1204
+ return data;
1205
+ }
1206
+
1207
+ const data1 = readJsonArray(file1Path);
1208
+ const data2 = readJsonArray(file2Path);
1209
+
1210
+ const lookupValues = new Set(data2.map(item => item[field2]));
1211
+ const matches = data1.filter(item => lookupValues.has(item[field1]));
1212
+
1213
+ if (matches.length === 0) {
1214
+ console.log(\`No items in \${file1Path} have \${field1} matching \${field2} values from \${file2Path}.\`);
1215
+ process.exit(0);
1216
+ }
1217
+
1218
+ console.log(\`Found \${matches.length} item(s) in \${file1Path} where \${field1} matches \${field2} in \${file2Path}:\\n\`);
1219
+ for (const item of matches) {
1220
+ console.log(\` [\${field1}=\${JSON.stringify(item[field1])}]\`);
1221
+ console.log(JSON.stringify(item, null, 2));
1222
+ console.log();
1223
+ }
1224
+ `;
1225
+ var DATA_SCRIPTING_SKILL_FILES = {
1226
+ "skills/data-scripting/SKILL.md": SKILL_MD,
1227
+ "skills/data-scripting/schemas.md": SCHEMAS_MD,
1228
+ "skills/data-scripting/helpers/top-items.js": TOP_ITEMS_JS,
1229
+ "skills/data-scripting/helpers/cross-reference.js": CROSS_REFERENCE_JS
1230
+ };
1231
+ // ../utils/src/skills/browser-analysis.ts
1232
+ var SKILL_MD2 = `---
1233
+ name: browser-analysis
1234
+ description: Use this skill when analyzing browser page-load performance data including Chrome traces, heap snapshots, network waterfalls, and runtime blocking functions. Provides pre-built analysis scripts for common browser performance patterns.
1235
+ ---
1236
+
1237
+ # Browser Analysis Scripts
1238
+
1239
+ ## Overview
1240
+
1241
+ Pre-built scripts for analyzing browser performance data. Run these
1242
+ directly or use them as templates for custom analysis.
1243
+
1244
+ ## START HERE — Workspace overview
1245
+
1246
+ Run this FIRST to get a prioritized summary of the entire workspace:
1247
+
1248
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
1249
+
1250
+ Reads trace/summary.json, heap/summary.json, trace/runtime/summary.json,
1251
+ blocking-functions.json, event-listeners.json, and network-waterfall.json.
1252
+ Outputs:
1253
+ - Page load metrics (FCP, LCP, TBT, load time)
1254
+ - Heap snapshot overview (total size, detached DOM nodes, closures)
1255
+ - Runtime trace summary (blocking function count, GC stats, frame breakdown)
1256
+ - Top blocking functions with script locations
1257
+ - Listener imbalances
1258
+ - Render-blocking resources
1259
+ - Large resources (>100KB)
1260
+ - Full list of available source files with sizes
1261
+
1262
+ Use this output to decide which source files to read and which scripts
1263
+ to run for deeper analysis.
1264
+
1265
+ ## Additional scripts
1266
+
1267
+ ### Analyze blocking functions (detailed)
1268
+ execute_command: node skills/browser-analysis/helpers/analyze-blockers.js [--threshold 50]
1269
+
1270
+ Reads trace/runtime/blocking-functions.json, filters by duration threshold
1271
+ (default 50ms), and outputs a ranked summary with call stacks and script
1272
+ locations. Also identifies compound blockers.
1273
+
1274
+ ### Analyze network waterfall (detailed)
1275
+ execute_command: node skills/browser-analysis/helpers/analyze-waterfall.js
1276
+
1277
+ Reads trace/network-waterfall.json and trace/summary.json, outputs:
1278
+ - Render-blocking resources with sizes and durations
1279
+ - Large scripts (>100KB) with workspace paths
1280
+ - Sequential chains that could be parallelized
1281
+ - Uncompressed resources
1282
+
1283
+ ### Analyze heap snapshot (detailed)
1284
+ execute_command: node skills/browser-analysis/helpers/analyze-heap.js
1285
+
1286
+ Reads heap/summary.json, outputs:
1287
+ - Detached DOM nodes with retainer paths
1288
+ - Top 10 largest retained objects
1289
+ - Constructor hotspots
1290
+ - Closures with large retained sizes
1291
+
1292
+ ### Find source code patterns
1293
+ execute_command: node skills/browser-analysis/helpers/find-patterns.js [--pattern addEventListener]
1294
+
1295
+ Searches source files for performance anti-patterns.
1296
+
1297
+ ## Key data files
1298
+
1299
+ - heap/summary.json — parsed V8 heap snapshot (detached nodes, retained objects, closures)
1300
+ - trace/summary.json — page load metrics, render-blocking resources, resource breakdown
1301
+ - trace/network-waterfall.json — every network request with timing, size, priority
1302
+ - trace/asset-manifest.json — index of all assets with stored file paths
1303
+ - trace/runtime/summary.json — runtime trace overview (frame breakdown, GC stats)
1304
+ - trace/runtime/blocking-functions.json — main-thread blocking functions with call stacks
1305
+ - trace/runtime/event-listeners.json — event listener add/remove counts
1306
+ - trace/runtime/frame-breakdown.json — time in scripting vs layout vs paint vs GC
1307
+ - scripts/*.js — actual JavaScript source files captured during page load
1308
+ - styles/*.css — actual CSS source files
1309
+ - html/document.html — the HTML document
1310
+
1311
+ ## Writing custom scripts
1312
+
1313
+ Read skills/data-scripting/schemas.md for JSON structures, then write
1314
+ scripts that load and query the data.
1315
+ `;
1316
+ var ANALYZE_BROWSER_WORKSPACE_JS = `'use strict';
1317
+
1318
+ var fs = require('fs');
1319
+
1320
+ function tryRead(path) {
1321
+ try { return JSON.parse(fs.readFileSync(path, 'utf8')); }
1322
+ catch (_) { return null; }
1323
+ }
1324
+
1325
+ var traceSummary = tryRead('trace/summary.json');
1326
+ var heapSummary = tryRead('heap/summary.json');
1327
+ var rtSummary = tryRead('trace/runtime/summary.json');
1328
+ var waterfall = tryRead('trace/network-waterfall.json');
1329
+ var assetManifest = tryRead('trace/asset-manifest.json');
1330
+ var blockingFunctions = tryRead('trace/runtime/blocking-functions.json');
1331
+ var eventListeners = tryRead('trace/runtime/event-listeners.json');
1332
+
1333
+ console.log('=== Browser Workspace Overview ===\\n');
1334
+
1335
+ if (traceSummary) {
1336
+ var t = traceSummary.timing || traceSummary;
1337
+ console.log('URL: ' + (traceSummary.url || '(unknown)'));
1338
+ console.log('Page load: ' + Math.round(t.loadComplete || 0) + 'ms | FCP: ' + Math.round(t.firstContentfulPaint || 0) + 'ms | LCP: ' + Math.round(t.largestContentfulPaint || 0) + 'ms | TBT: ' + Math.round(t.totalBlockingTime || 0) + 'ms');
1339
+ console.log('Requests: ' + (traceSummary.requestCount || 0) + ' | Transfer: ' + ((traceSummary.totalTransferSize || 0) / 1024).toFixed(1) + ' KB | Decoded: ' + ((traceSummary.totalDecodedSize || 0) / 1024).toFixed(1) + ' KB');
1340
+ if (traceSummary.renderBlockingResources && traceSummary.renderBlockingResources.length > 0) {
1341
+ console.log('Render-blocking: ' + traceSummary.renderBlockingResources.length + ' resources');
1342
+ }
1343
+ console.log();
1344
+ }
1345
+
1346
+ if (heapSummary) {
1347
+ var meta = heapSummary.metadata || {};
1348
+ console.log('=== Heap Summary ===\\n');
1349
+ console.log('Total heap: ' + ((meta.totalSize || 0) / 1024 / 1024).toFixed(2) + ' MB | Nodes: ' + (meta.nodeCount || 0) + ' | Edges: ' + (meta.edgeCount || 0));
1350
+ var detached = heapSummary.detachedNodes || heapSummary.detachedDomNodes;
1351
+ if (detached) {
1352
+ var dCount = detached.count || (Array.isArray(detached) ? detached.length : 0);
1353
+ var dSize = detached.totalSize || 0;
1354
+ console.log('Detached DOM nodes: ' + dCount + (dSize ? ' (' + (dSize / 1024).toFixed(1) + ' KB)' : ''));
1355
+ }
1356
+ var closures = heapSummary.closureStats;
1357
+ if (closures && closures.count) {
1358
+ console.log('Closures: ' + closures.count + ' (' + ((closures.totalSize || 0) / 1024).toFixed(1) + ' KB)');
1359
+ }
1360
+ console.log();
1361
+ }
1362
+
1363
+ if (rtSummary) {
1364
+ console.log('=== Runtime Trace Summary ===\\n');
1365
+ console.log('Trace duration: ' + Math.round(rtSummary.traceDuration || 0) + 'ms');
1366
+ console.log('Blocking functions: ' + (rtSummary.blockingFunctionCount || 0));
1367
+ console.log('Listener imbalances: ' + (rtSummary.listenerImbalances || 0));
1368
+ console.log('GC pauses: ' + (rtSummary.gcPauseCount || 0) + ' (' + Math.round(rtSummary.gcTotalDuration || 0) + 'ms total)');
1369
+ if (rtSummary.frameBreakdown) {
1370
+ var fb = rtSummary.frameBreakdown;
1371
+ console.log('Frame breakdown: scripting=' + Math.round(fb.scripting || 0) + 'ms layout=' + Math.round(fb.layout || 0) + 'ms paint=' + Math.round(fb.paint || 0) + 'ms gc=' + Math.round(fb.gc || 0) + 'ms');
1372
+ }
1373
+ if (rtSummary.frequentEventTypes && rtSummary.frequentEventTypes.length > 0) {
1374
+ console.log('Frequent events: ' + rtSummary.frequentEventTypes.join(', '));
1375
+ }
1376
+ console.log();
1377
+ }
1378
+
1379
+ if (Array.isArray(blockingFunctions) && blockingFunctions.length > 0) {
1380
+ console.log('=== Top Blocking Functions ===\\n');
1381
+ var top = blockingFunctions.slice(0, 10);
1382
+ for (var i = 0; i < top.length; i++) {
1383
+ var fn = top[i];
1384
+ console.log(' [' + (fn.duration || 0) + 'ms] ' + (fn.functionName || '(anonymous)') + ' @ ' + (fn.scriptUrl || 'unknown') + ':' + (fn.lineNumber || '?'));
1385
+ }
1386
+ if (blockingFunctions.length > 10) {
1387
+ console.log(' ... and ' + (blockingFunctions.length - 10) + ' more');
1388
+ }
1389
+ console.log();
1390
+ }
1391
+
1392
+ if (Array.isArray(eventListeners) && eventListeners.length > 0) {
1393
+ var imbalanced = eventListeners.filter(function (l) {
1394
+ return (l.addCount || 0) > (l.removeCount || 0) + 2;
1395
+ });
1396
+ if (imbalanced.length > 0) {
1397
+ console.log('=== Listener Imbalances (adds >> removes) ===\\n');
1398
+ for (var i = 0; i < imbalanced.length; i++) {
1399
+ var l = imbalanced[i];
1400
+ console.log(' ' + l.eventType + ' on ' + (l.targetType || '?') + ' adds=' + l.addCount + ' removes=' + (l.removeCount || 0) + ' active=' + (l.activeCount || 0));
1401
+ }
1402
+ console.log();
1403
+ }
1404
+ }
1405
+
1406
+ if (traceSummary && traceSummary.renderBlockingResources && traceSummary.renderBlockingResources.length > 0) {
1407
+ console.log('=== Render-Blocking Resources ===\\n');
1408
+ var rbs = traceSummary.renderBlockingResources;
1409
+ for (var i = 0; i < rbs.length; i++) {
1410
+ var r = rbs[i];
1411
+ console.log(' [' + (r.type || '?') + '] ' + (r.url || '?'));
1412
+ console.log(' Size: ' + ((r.size || 0) / 1024).toFixed(1) + ' KB | Duration: ' + Math.round(r.duration || 0) + 'ms' + (r.path ? ' | File: ' + r.path : ''));
1413
+ }
1414
+ console.log();
1415
+ }
1416
+
1417
+ if (Array.isArray(waterfall)) {
1418
+ var large = waterfall.filter(function (r) {
1419
+ return (r.size || 0) > 100000;
1420
+ });
1421
+ if (large.length > 0) {
1422
+ large.sort(function (a, b) { return (b.size || 0) - (a.size || 0); });
1423
+ console.log('=== Large Resources (>100KB) ===\\n');
1424
+ for (var i = 0; i < Math.min(10, large.length); i++) {
1425
+ var r = large[i];
1426
+ console.log(' [' + (r.type || '?') + '] ' + ((r.size || 0) / 1024).toFixed(1) + 'KB ' + (r.url || '?'));
1427
+ if (r.path) console.log(' File: ' + r.path);
1428
+ }
1429
+ console.log();
1430
+ }
1431
+ }
1432
+
1433
+ console.log('=== Available Files ===\\n');
1434
+ var dirs = ['scripts', 'styles', 'html', 'other'];
1435
+ for (var d = 0; d < dirs.length; d++) {
1436
+ try {
1437
+ var files = fs.readdirSync(dirs[d]);
1438
+ if (files.length > 0) {
1439
+ for (var i = 0; i < files.length; i++) {
1440
+ var full = dirs[d] + '/' + files[i];
1441
+ try {
1442
+ var stat = fs.statSync(full);
1443
+ if (stat.isFile()) console.log(' ' + full + ' (' + (stat.size / 1024).toFixed(1) + ' KB)');
1444
+ } catch (_) {}
1445
+ }
1446
+ }
1447
+ } catch (_) {}
1448
+ }
1449
+ console.log();
1450
+ `;
1451
+ var ANALYZE_BLOCKERS_JS = `'use strict';
1452
+ var fs = require('fs');
1453
+
1454
+ var threshold = 50;
1455
+ var args = process.argv.slice(2);
1456
+ for (var i = 0; i < args.length; i++) {
1457
+ if (args[i] === '--threshold' && args[i + 1]) {
1458
+ threshold = parseInt(args[i + 1], 10);
1459
+ if (isNaN(threshold)) threshold = 50;
1460
+ }
1461
+ }
1462
+
1463
+ var data;
1464
+ try {
1465
+ data = JSON.parse(fs.readFileSync('trace/runtime/blocking-functions.json', 'utf8'));
1466
+ } catch (e) {
1467
+ console.log('Could not read trace/runtime/blocking-functions.json:', e.message);
1468
+ process.exit(0);
1469
+ }
1470
+
1471
+ if (!Array.isArray(data)) {
1472
+ console.log('Expected an array in blocking-functions.json');
1473
+ process.exit(0);
1474
+ }
1475
+
1476
+ var blockers = data.filter(function (fn) { return fn.duration >= threshold; });
1477
+ blockers.sort(function (a, b) { return b.duration - a.duration; });
1478
+
1479
+ if (blockers.length === 0) {
1480
+ console.log('No blocking functions found above ' + threshold + 'ms threshold.');
1481
+ process.exit(0);
1482
+ }
1483
+
1484
+ function workspacePath(url) {
1485
+ if (!url) return '';
1486
+ var parts = url.split('/');
1487
+ var filename = parts[parts.length - 1];
1488
+ if (!filename) return url;
1489
+ return 'scripts/' + filename;
1490
+ }
1491
+
1492
+ var blockerNames = {};
1493
+ blockers.forEach(function (fn) {
1494
+ blockerNames[fn.functionName] = true;
1495
+ });
1496
+
1497
+ console.log('=== Blocking Functions (>=' + threshold + 'ms) ===');
1498
+ console.log('Found ' + blockers.length + ' blocking function(s)\\n');
1499
+
1500
+ blockers.forEach(function (fn, idx) {
1501
+ var wsPath = workspacePath(fn.scriptUrl);
1502
+ console.log((idx + 1) + '. [' + fn.duration + 'ms] ' + (fn.functionName || '(anonymous)') +
1503
+ ' @ ' + (fn.scriptUrl || 'unknown') + ':' + (fn.lineNumber || '?'));
1504
+ if (wsPath) {
1505
+ console.log(' Workspace: ' + wsPath);
1506
+ }
1507
+
1508
+ if (fn.callStack && fn.callStack.length > 0) {
1509
+ console.log(' Call stack:');
1510
+ fn.callStack.forEach(function (caller) {
1511
+ console.log(' <- ' + (caller.functionName || '(anonymous)') +
1512
+ ' @ ' + (caller.scriptUrl || 'unknown') + ':' + (caller.lineNumber || '?'));
1513
+ });
1514
+ }
1515
+
1516
+ if (fn.callStack && fn.callStack.length > 0) {
1517
+ var compounds = fn.callStack.filter(function (caller) {
1518
+ return blockerNames[caller.functionName];
1519
+ });
1520
+ if (compounds.length > 0) {
1521
+ console.log(' ⚠ Compound blocker — calls into:');
1522
+ compounds.forEach(function (c) {
1523
+ console.log(' → ' + c.functionName);
1524
+ });
1525
+ }
1526
+ }
1527
+
1528
+ console.log('');
1529
+ });
1530
+ `;
1531
+ var ANALYZE_WATERFALL_JS = `'use strict';
1532
+ var fs = require('fs');
1533
+
1534
+ var waterfall;
1535
+ try {
1536
+ waterfall = JSON.parse(fs.readFileSync('trace/network-waterfall.json', 'utf8'));
1537
+ } catch (e) {
1538
+ console.log('Could not read trace/network-waterfall.json:', e.message);
1539
+ process.exit(0);
1540
+ }
1541
+
1542
+ var summary = null;
1543
+ try {
1544
+ summary = JSON.parse(fs.readFileSync('trace/summary.json', 'utf8'));
1545
+ } catch (e) {
1546
+ // summary is optional
1547
+ }
1548
+
1549
+ if (!Array.isArray(waterfall)) {
1550
+ console.log('Expected an array in network-waterfall.json');
1551
+ process.exit(0);
1552
+ }
1553
+
1554
+ function workspacePath(url) {
1555
+ if (!url) return '';
1556
+ var parts = url.split('/');
1557
+ var filename = parts[parts.length - 1];
1558
+ if (!filename) return url;
1559
+ var qIdx = filename.indexOf('?');
1560
+ if (qIdx > -1) filename = filename.substring(0, qIdx);
1561
+ return 'scripts/' + filename;
1562
+ }
1563
+
1564
+ function toKB(bytes) {
1565
+ return (bytes / 1024).toFixed(1) + ' KB';
1566
+ }
1567
+
1568
+ // 1. Render-blocking resources
1569
+ var renderBlocking = waterfall.filter(function (r) { return r.isRenderBlocking; });
1570
+ console.log('=== Render-Blocking Resources ===');
1571
+ if (renderBlocking.length === 0) {
1572
+ console.log('None found.\\n');
1573
+ } else {
1574
+ console.log('Found ' + renderBlocking.length + ' render-blocking resource(s)\\n');
1575
+ renderBlocking.forEach(function (r) {
1576
+ console.log(' ' + (r.type || 'unknown') + ': ' + (r.url || 'unknown'));
1577
+ console.log(' Size: ' + toKB(r.size || r.encodedSize || 0) +
1578
+ ' | Duration: ' + ((r.duration || 0).toFixed(1)) + 'ms');
1579
+ console.log(' Workspace: ' + workspacePath(r.url));
1580
+ console.log('');
1581
+ });
1582
+ }
1583
+
1584
+ // 2. Large scripts (>100KB)
1585
+ var largeScripts = waterfall.filter(function (r) {
1586
+ return r.type === 'Script' && (r.size || r.decodedSize || 0) > 100000;
1587
+ });
1588
+ console.log('=== Large Scripts (>100KB) ===');
1589
+ if (largeScripts.length === 0) {
1590
+ console.log('None found.\\n');
1591
+ } else {
1592
+ largeScripts.sort(function (a, b) {
1593
+ return (b.size || b.decodedSize || 0) - (a.size || a.decodedSize || 0);
1594
+ });
1595
+ console.log('Found ' + largeScripts.length + ' large script(s)\\n');
1596
+ largeScripts.forEach(function (r) {
1597
+ console.log(' ' + (r.url || 'unknown'));
1598
+ console.log(' Size: ' + toKB(r.size || r.decodedSize || 0) +
1599
+ ' | Duration: ' + ((r.duration || 0).toFixed(1)) + 'ms');
1600
+ console.log(' Workspace: ' + workspacePath(r.url));
1601
+ console.log('');
1602
+ });
1603
+ }
1604
+
1605
+ // 3. Sequential chains
1606
+ var scriptAndStyle = waterfall.filter(function (r) {
1607
+ return r.type === 'Script' || r.type === 'Stylesheet';
1608
+ });
1609
+ scriptAndStyle.sort(function (a, b) {
1610
+ return (a.startTime || 0) - (b.startTime || 0);
1611
+ });
1612
+
1613
+ var chains = [];
1614
+ for (var i = 0; i < scriptAndStyle.length - 1; i++) {
1615
+ var a = scriptAndStyle[i];
1616
+ var aEnd = (a.startTime || 0) + (a.duration || 0);
1617
+ for (var j = i + 1; j < scriptAndStyle.length; j++) {
1618
+ var b = scriptAndStyle[j];
1619
+ if ((b.startTime || 0) >= aEnd) {
1620
+ var chainDuration = (b.startTime || 0) + (b.duration || 0) - (a.startTime || 0);
1621
+ if (chainDuration > 200) {
1622
+ chains.push({ a: a, b: b, duration: chainDuration });
1623
+ }
1624
+ break;
1625
+ }
1626
+ }
1627
+ }
1628
+
1629
+ console.log('=== Sequential Chains (could be parallelized) ===');
1630
+ if (chains.length === 0) {
1631
+ console.log('None found.\\n');
1632
+ } else {
1633
+ console.log('Found ' + chains.length + ' sequential chain(s)\\n');
1634
+ chains.forEach(function (c) {
1635
+ console.log(' ' + (c.a.url || 'unknown') + ' → ' + (c.b.url || 'unknown'));
1636
+ console.log(' Chain duration: ' + c.duration.toFixed(1) + 'ms');
1637
+ console.log('');
1638
+ });
1639
+ }
1640
+
1641
+ // 4. Summary render-blocking info
1642
+ if (summary && summary.renderBlockingResources) {
1643
+ console.log('=== Summary: Render-Blocking Resources ===');
1644
+ var rbs = summary.renderBlockingResources;
1645
+ if (Array.isArray(rbs)) {
1646
+ rbs.forEach(function (r) {
1647
+ console.log(' ' + (r.url || JSON.stringify(r)));
1648
+ });
1649
+ } else {
1650
+ console.log(' ' + JSON.stringify(rbs, null, 2));
1651
+ }
1652
+ console.log('');
1653
+ }
1654
+ `;
1655
+ var ANALYZE_HEAP_JS = `'use strict';
1656
+ var fs = require('fs');
1657
+
1658
+ var data;
1659
+ try {
1660
+ data = JSON.parse(fs.readFileSync('heap/summary.json', 'utf8'));
1661
+ } catch (e) {
1662
+ console.log('Could not read heap/summary.json:', e.message);
1663
+ process.exit(0);
1664
+ }
1665
+
1666
+ function retainerChain(retainerPath) {
1667
+ if (!Array.isArray(retainerPath)) return '';
1668
+ return retainerPath.map(function (r) {
1669
+ return r.name || r.className || '(unknown)';
1670
+ }).join(' ← ');
1671
+ }
1672
+
1673
+ function toKB(bytes) {
1674
+ return (bytes / 1024).toFixed(1) + ' KB';
1675
+ }
1676
+
1677
+ // 1. Detached DOM nodes
1678
+ console.log('=== Detached DOM Nodes ===');
1679
+ var detached = data.detachedDomNodes || data.detachedNodes || [];
1680
+ if (!Array.isArray(detached) || detached.length === 0) {
1681
+ console.log('None found.\\n');
1682
+ } else {
1683
+ var totalSize = detached.reduce(function (sum, n) {
1684
+ return sum + (n.retainedSize || n.selfSize || 0);
1685
+ }, 0);
1686
+ console.log('Count: ' + detached.length + ' | Total retained size: ' + toKB(totalSize) + '\\n');
1687
+ detached.forEach(function (node) {
1688
+ console.log(' ' + (node.name || node.className || '(node)'));
1689
+ console.log(' Retained: ' + toKB(node.retainedSize || 0) +
1690
+ ' | Self: ' + toKB(node.selfSize || 0));
1691
+ if (node.retainerPath) {
1692
+ console.log(' Retainer path: ' + retainerChain(node.retainerPath));
1693
+ }
1694
+ if (node.scriptUrl) {
1695
+ console.log(' ➜ Read: ' + node.scriptUrl);
1696
+ }
1697
+ console.log('');
1698
+ });
1699
+ }
1700
+
1701
+ // 2. Top 10 largest retained objects
1702
+ console.log('=== Top 10 Largest Retained Objects ===');
1703
+ var objects = data.topRetainedObjects || data.largestObjects || [];
1704
+ if (!Array.isArray(objects) || objects.length === 0) {
1705
+ console.log('None found.\\n');
1706
+ } else {
1707
+ var top10 = objects.slice(0, 10);
1708
+ top10.forEach(function (obj, idx) {
1709
+ console.log((idx + 1) + '. ' + (obj.name || '(unknown)') + ' [' + (obj.type || obj.className || '?') + ']');
1710
+ console.log(' Self: ' + toKB(obj.selfSize || 0) + ' | Retained: ' + toKB(obj.retainedSize || 0));
1711
+ if (obj.retainerPath) {
1712
+ console.log(' Retainer path: ' + retainerChain(obj.retainerPath));
1713
+ }
1714
+ if (obj.scriptUrl) {
1715
+ console.log(' ➜ Read: ' + obj.scriptUrl);
1716
+ }
1717
+ console.log('');
1718
+ });
1719
+ }
1720
+
1721
+ // 3. Constructor hotspots
1722
+ console.log('=== Constructor Hotspots (Top 10 by instance count) ===');
1723
+ var constructors = data.constructorStats || [];
1724
+ if (!Array.isArray(constructors) || constructors.length === 0) {
1725
+ console.log('None found.\\n');
1726
+ } else {
1727
+ var sorted = constructors.slice().sort(function (a, b) {
1728
+ return (b.instanceCount || b.count || 0) - (a.instanceCount || a.count || 0);
1729
+ });
1730
+ sorted.slice(0, 10).forEach(function (c, idx) {
1731
+ console.log((idx + 1) + '. ' + (c.name || c.constructor || '(unknown)') +
1732
+ ' — ' + (c.instanceCount || c.count || 0) + ' instances' +
1733
+ ' | Total size: ' + toKB(c.totalSize || c.retainedSize || 0));
1734
+ });
1735
+ console.log('');
1736
+ }
1737
+
1738
+ // 4. Top closures
1739
+ console.log('=== Top Closures by Context Size ===');
1740
+ var closureStats = data.closureStats || {};
1741
+ var topClosures = closureStats.topClosures || [];
1742
+ if (!Array.isArray(topClosures) || topClosures.length === 0) {
1743
+ console.log('None found.\\n');
1744
+ } else {
1745
+ topClosures.slice(0, 10).forEach(function (cl, idx) {
1746
+ console.log((idx + 1) + '. ' + (cl.name || cl.functionName || '(anonymous)') +
1747
+ ' — context: ' + toKB(cl.contextSize || 0) +
1748
+ ' | retained: ' + toKB(cl.retainedSize || 0));
1749
+ if (cl.scriptUrl) {
1750
+ console.log(' ➜ Read: ' + cl.scriptUrl);
1751
+ }
1752
+ });
1753
+ console.log('');
1754
+ }
1755
+ `;
1756
+ var FIND_PATTERNS_JS = `'use strict';
1757
+ var fs = require('fs');
1758
+
1759
+ var onlyPattern = null;
1760
+ var args = process.argv.slice(2);
1761
+ for (var i = 0; i < args.length; i++) {
1762
+ if (args[i] === '--pattern' && args[i + 1]) {
1763
+ onlyPattern = args[i + 1];
1764
+ }
1765
+ }
1766
+
1767
+ function normalizePath(p) {
1768
+ return p.startsWith('/') ? p.slice(1) : p;
1769
+ }
1770
+
1771
+ var filePaths = [];
1772
+
1773
+ try {
1774
+ var manifest = JSON.parse(fs.readFileSync('trace/asset-manifest.json', 'utf8'));
1775
+ if (Array.isArray(manifest)) {
1776
+ manifest.forEach(function (entry) {
1777
+ var p = entry.storedPath || entry.path;
1778
+ if (p) filePaths.push(normalizePath(p));
1779
+ });
1780
+ } else if (manifest && typeof manifest === 'object') {
1781
+ Object.keys(manifest).forEach(function (key) {
1782
+ var entry = manifest[key];
1783
+ var p = (typeof entry === 'string') ? entry : (entry && (entry.storedPath || entry.path));
1784
+ if (p) filePaths.push(normalizePath(p));
1785
+ });
1786
+ }
1787
+ } catch (e) {
1788
+ // manifest not available
1789
+ }
1790
+
1791
+ var extraPaths = ['html/document.html'];
1792
+ extraPaths.forEach(function (p) {
1793
+ if (filePaths.indexOf(p) === -1) {
1794
+ try {
1795
+ fs.accessSync(p);
1796
+ filePaths.push(p);
1797
+ } catch (e) {
1798
+ // not available
1799
+ }
1800
+ }
1801
+ });
1802
+
1803
+ function readFile(p) {
1804
+ try {
1805
+ return fs.readFileSync(p, 'utf8');
1806
+ } catch (e) {
1807
+ return null;
1808
+ }
1809
+ }
1810
+
1811
+ var patterns = {
1812
+ addEventListener: {
1813
+ test: function (line) {
1814
+ if (!/addEventListener/.test(line)) return false;
1815
+ if (/\\b(scroll|touchstart|touchmove|wheel)\\b/.test(line) && !/passive\\s*:\\s*true/.test(line)) {
1816
+ return true;
1817
+ }
1818
+ return false;
1819
+ },
1820
+ label: 'addEventListener without passive:true for scroll/touch/wheel'
1821
+ },
1822
+ missingDelegation: {
1823
+ test: function (line, allLines, idx) {
1824
+ if (!/querySelectorAll/.test(line)) return false;
1825
+ var window = allLines.slice(idx, Math.min(idx + 5, allLines.length)).join(' ');
1826
+ return /querySelectorAll/.test(window) && /forEach/.test(window) && /addEventListener/.test(window);
1827
+ },
1828
+ label: 'querySelectorAll+forEach+addEventListener (consider event delegation)'
1829
+ },
1830
+ syncXHR: {
1831
+ test: function (line) {
1832
+ return /XMLHttpRequest/.test(line) && /\\.open\\(/.test(line) && /,\\s*false\\s*[),]/.test(line);
1833
+ },
1834
+ label: 'Synchronous XMLHttpRequest'
1835
+ },
1836
+ documentWrite: {
1837
+ test: function (line) {
1838
+ return /document\\.write\\s*\\(/.test(line);
1839
+ },
1840
+ label: 'document.write() blocks parsing'
1841
+ },
1842
+ cssImport: {
1843
+ test: function (line) {
1844
+ return /@import\\b/.test(line);
1845
+ },
1846
+ label: 'CSS @import creates sequential loading'
1847
+ },
1848
+ imgDimensions: {
1849
+ test: function (line) {
1850
+ if (!/<img\\b/i.test(line)) return false;
1851
+ var hasWidth = /\\bwidth\\s*=/.test(line);
1852
+ var hasHeight = /\\bheight\\s*=/.test(line);
1853
+ return !hasWidth || !hasHeight;
1854
+ },
1855
+ label: '<img> without width/height causes layout shift'
1856
+ }
1857
+ };
1858
+
1859
+ var patternKeys = Object.keys(patterns);
1860
+ if (onlyPattern) {
1861
+ if (!patterns[onlyPattern]) {
1862
+ console.log('Unknown pattern: ' + onlyPattern);
1863
+ console.log('Available: ' + patternKeys.join(', '));
1864
+ process.exit(0);
1865
+ }
1866
+ patternKeys = [onlyPattern];
1867
+ }
1868
+
1869
+ var results = [];
1870
+
1871
+ filePaths.forEach(function (filePath) {
1872
+ var content = readFile(filePath);
1873
+ if (!content) return;
1874
+ var lines = content.split('\\n');
1875
+ lines.forEach(function (line, idx) {
1876
+ patternKeys.forEach(function (key) {
1877
+ if (patterns[key].test(line, lines, idx)) {
1878
+ results.push({
1879
+ path: filePath,
1880
+ lineNumber: idx + 1,
1881
+ pattern: key,
1882
+ label: patterns[key].label,
1883
+ line: line.trim()
1884
+ });
1885
+ }
1886
+ });
1887
+ });
1888
+ });
1889
+
1890
+ if (results.length === 0) {
1891
+ console.log('No performance anti-patterns found.');
1892
+ process.exit(0);
1893
+ }
1894
+
1895
+ console.log('=== Performance Anti-Patterns ===');
1896
+ console.log('Found ' + results.length + ' issue(s)\\n');
1897
+ results.forEach(function (r) {
1898
+ console.log(r.path + ':' + r.lineNumber + ': [' + r.pattern + '] ' + r.line);
1899
+ });
1900
+ `;
1901
+ var BROWSER_ANALYSIS_SKILL_FILES = {
1902
+ "skills/browser-analysis/SKILL.md": SKILL_MD2,
1903
+ "skills/browser-analysis/helpers/analyze-browser-workspace.js": ANALYZE_BROWSER_WORKSPACE_JS,
1904
+ "skills/browser-analysis/helpers/analyze-blockers.js": ANALYZE_BLOCKERS_JS,
1905
+ "skills/browser-analysis/helpers/analyze-waterfall.js": ANALYZE_WATERFALL_JS,
1906
+ "skills/browser-analysis/helpers/analyze-heap.js": ANALYZE_HEAP_JS,
1907
+ "skills/browser-analysis/helpers/find-patterns.js": FIND_PATTERNS_JS
1908
+ };
1909
+ // ../utils/src/skills/result-writer.ts
1910
+ var MERGE_RESULTS_JS = `'use strict';
1911
+
1912
+ var fs = require('fs');
1913
+
1914
+ fs.mkdirSync('results', { recursive: true });
1915
+
1916
+ var resultFiles;
1917
+ try {
1918
+ resultFiles = fs.readdirSync('results').filter(function (f) {
1919
+ return f.endsWith('.json') && f !== 'merged.json';
1920
+ });
1921
+ } catch (e) {
1922
+ console.log('[]');
1923
+ process.exit(0);
1924
+ }
1925
+
1926
+ if (resultFiles.length === 0) {
1927
+ console.log('[]');
1928
+ process.exit(0);
1929
+ }
1930
+
1931
+ var allFindings = [];
1932
+ for (var i = 0; i < resultFiles.length; i++) {
1933
+ try {
1934
+ var data = JSON.parse(fs.readFileSync('results/' + resultFiles[i], 'utf8'));
1935
+ if (Array.isArray(data)) {
1936
+ allFindings = allFindings.concat(data);
1937
+ }
1938
+ } catch (e) {
1939
+ // skip malformed files
1940
+ }
1941
+ }
1942
+
1943
+ fs.writeFileSync('results/merged.json', JSON.stringify(allFindings, null, 2));
1944
+ console.log(JSON.stringify(allFindings));
1945
+ `;
1946
+ var RESULT_WRITER_SKILL_FILES = {
1947
+ "skills/result-writer/merge-results.js": MERGE_RESULTS_JS
1948
+ };
788
1949
  // ../utils/src/output/terminal.ts
789
1950
  import pc2 from "picocolors";
790
1951
  import ora from "ora";
@@ -971,7 +2132,7 @@ function formatBytes(bytes) {
971
2132
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
972
2133
  }
973
2134
  // ../utils/src/output/report.ts
974
- import { writeFileSync as writeFileSync2 } from "node:fs";
2135
+ import { writeFileSync } from "node:fs";
975
2136
  var SEVERITY_EMOJI = {
976
2137
  critical: "\uD83D\uDD34",
977
2138
  warning: "\uD83D\uDFE1",
@@ -1004,7 +2165,7 @@ var CATEGORY_LABELS2 = {
1004
2165
  };
1005
2166
  function writeReport(outputPath, options) {
1006
2167
  const md = generateMarkdown(options);
1007
- writeFileSync2(outputPath, md, "utf-8");
2168
+ writeFileSync(outputPath, md, "utf-8");
1008
2169
  return outputPath;
1009
2170
  }
1010
2171
  function generateMarkdown(options) {
@@ -1113,27 +2274,30 @@ import { createDeepAgent } from "deepagents";
1113
2274
  import { toolStrategy } from "langchain";
1114
2275
 
1115
2276
  // src/analysis/prompts/shared.ts
1116
- var BROWSER_TOOL_CALL_STRATEGY = `## CRITICAL: Tool call strategy — data first, source selectively
2277
+ var BROWSER_TOOL_CALL_STRATEGY = `## CRITICAL: Tool call strategy — scripts first, source selectively
2278
+
2279
+ Your FIRST turn MUST run analysis scripts against the data files (JSON) to
2280
+ extract a concise summary of issues. Use the pre-built helper scripts in
2281
+ skills/browser-analysis/helpers/ or write your own using the data-scripting
2282
+ skill. Do NOT read data JSON files directly with read_file.
1117
2283
 
1118
- Your FIRST turn MUST contain read_file calls ONLY for the data files (JSON)
1119
- listed under "Data files" in "FILES IN THIS WORKSPACE" above. Batch them
1120
- into ONE turn. Do NOT read any source files in your first turn.
2284
+ After your analysis scripts identify specific issues, read at most 1-3
2285
+ source files that are directly implicated. Derive paths from script URLs
2286
+ in the data (e.g. a URL ending in "abc123.js" scripts/abc123.js).
1121
2287
 
1122
- After analyzing the data, read at most 1-3 source files that are directly
1123
- implicated by the issues you found. Derive paths from script URLs in the data
1124
- (e.g. a URL ending in "abc123.js" /scripts/abc123.js). If no source file
1125
- is implicated, skip reading source files entirely and report findings from
1126
- the data alone.
2288
+ PREFERRED actions:
2289
+ - execute_command with pre-built helper scripts or custom Node.js scripts
2290
+ - read_file for source code files you need to see verbatim
1127
2291
 
1128
2292
  FORBIDDEN actions:
1129
2293
  - ls — NEVER call ls.
1130
2294
  - glob — NEVER call glob.
1131
- - Reading ALL source files — only read the specific ones the data points to.
1132
- - Reading source files in the first turn always read data first.
2295
+ - read_file on data JSON files — use scripts to extract what you need.
2296
+ - Reading ALL source files only read the specific ones your scripts point to.
1133
2297
  - Reading more than 3 source files — focus on the most impactful issues.`;
1134
2298
  var MINIFIED_SOURCE_HANDLING = `## Handling minified / compiled source files
1135
2299
 
1136
- The JavaScript files in /scripts/ are captured from a PRODUCTION page. They are
2300
+ The JavaScript files in scripts/ are captured from a PRODUCTION page. They are
1137
2301
  almost always minified, bundled, or compiled (e.g. by webpack, Vite, Turbopack).
1138
2302
  Signs of compiled code: single very long lines, mangled 1-2 character variable
1139
2303
  names, no whitespace or comments.
@@ -1181,11 +2345,11 @@ critical. Even shorter blocking calls prevent the main thread from processing
1181
2345
  user interactions and paint updates.`;
1182
2346
  var CROSS_REFERENCING = `## Cross-referencing data
1183
2347
 
1184
- - When a script appears in BOTH /trace/runtime/blocking-functions.json (CPU)
1185
- AND /heap/summary.json (memory), mention both dimensions in the finding.
1186
- - Check /trace/runtime/event-listeners.json for listener imbalances and
1187
- cross-reference with the actual addEventListener calls in /scripts/.
1188
- - Use /trace/network-waterfall.json to identify sequential chains, then read
2348
+ - When a script appears in BOTH trace/runtime/blocking-functions.json (CPU)
2349
+ AND heap/summary.json (memory), mention both dimensions in the finding.
2350
+ - Check trace/runtime/event-listeners.json for listener imbalances and
2351
+ cross-reference with the actual addEventListener calls in scripts/.
2352
+ - Use trace/network-waterfall.json to identify sequential chains, then read
1189
2353
  the initiating script to confirm the dependency.
1190
2354
  - When GC pauses are significant, cross-reference with heap data to identify
1191
2355
  which constructors or allocation patterns are responsible.`;
@@ -1222,9 +2386,7 @@ document.body.removeChild(el);
1222
2386
  // 'el' still holds a reference → detached DOM node
1223
2387
  \`\`\`
1224
2388
 
1225
- **How to detect:** Read /heap/summary.json and check the \`detachedNodes\` section.
1226
- For each detached node, search the source files for references to that node type
1227
- or constructor name.
2389
+ **How to detect:** Run \`execute_command: node skills/browser-analysis/helpers/analyze-heap.js\` to get a summary of heap issues including detached nodes with retainer paths. Then read ONLY the source files referenced in the retainer paths to verify root causes.
1228
2390
 
1229
2391
  ### 2. Large Retained Objects
1230
2392
 
@@ -1240,10 +2402,7 @@ class DataStore {
1240
2402
  }
1241
2403
  \`\`\`
1242
2404
 
1243
- **How to detect:** Read /heap/summary.json and check \`largestObjects\`. Focus on
1244
- objects where \`retainedSize\` is significantly larger than \`selfSize\` — they are
1245
- roots of large object trees. Cross-reference with \`retainerPath\` to understand
1246
- what keeps them alive.
2405
+ **How to detect:** The analyze-heap.js script outputs the top 10 largest retained objects. Review the retainer paths and read the implicated source files.
1247
2406
 
1248
2407
  ### 3. Constructor Hotspots
1249
2408
 
@@ -1260,8 +2419,7 @@ function processItems(items) {
1260
2419
  }
1261
2420
  \`\`\`
1262
2421
 
1263
- **How to detect:** Read \`constructorStats\` in the heap summary. Focus on types
1264
- with unusually high instance counts or total size.
2422
+ **How to detect:** The analyze-heap.js script outputs constructor hotspots. Focus on types with unusually high instance counts.
1265
2423
 
1266
2424
  ### 4. Closure Leaks
1267
2425
 
@@ -1278,32 +2436,27 @@ function setupHandler(response) {
1278
2436
  }
1279
2437
  \`\`\`
1280
2438
 
1281
- **How to detect:** Read \`closureStats\` in the heap summary. For closures with
1282
- large retained sizes, search the source files for the function patterns and
1283
- check what variables they capture.
2439
+ **How to detect:** The analyze-heap.js script outputs top closures by retained size. Read the implicated source files to check what variables they capture.
1284
2440
 
1285
2441
  ### 5. Unbounded Caches/Maps
1286
2442
 
1287
2443
  Data structures that grow monotonically without eviction, TTL, or size limits.
1288
2444
 
1289
- **How to detect:** Read source files and look for Maps, Sets, arrays, or plain
1290
- objects used as stores where items are added but never removed.
2445
+ **How to detect:** After identifying suspicious objects from the heap analysis script output, read the relevant source files and look for Maps, Sets, arrays used as stores where items are added but never removed.
1291
2446
 
1292
2447
  ## Your workflow
1293
2448
 
1294
- 1. In your FIRST turn, call read_file for /heap/summary.json ONLY.
1295
- Do NOT use ls or glob. Do NOT read any source files yet.
1296
- 2. From the heap summary, identify:
1297
- - Detached DOM nodes (count and types)
1298
- - Top 10 largest retained objects
1299
- - Constructor types with high instance counts
1300
- - Closures with large retained sizes
1301
- 3. For issues that reference script URLs, derive the workspace path
1302
- (e.g. a script URL ending in "abc123.js" maps to /scripts/abc123.js).
1303
- Read ONLY the 1-3 source files directly implicated do NOT read all
1304
- scripts. Source files are at /scripts/*.js, /styles/*.css, /html/.
1305
- 4. Cross-reference with source code to find the root cause and provide
1306
- before/after code with a concrete fix.
2449
+ 1. In your FIRST turn, run BOTH of these:
2450
+ a. Run the workspace overview:
2451
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
2452
+ b. Run the detailed heap analysis:
2453
+ execute_command: node skills/browser-analysis/helpers/analyze-heap.js
2454
+ Do NOT use ls, glob, or read_file on heap/summary.json directly.
2455
+ 2. From the script outputs, identify issues and the script URLs that need verification.
2456
+ 3. Derive workspace paths from script URLs (e.g. URL ending in "abc123.js" → scripts/abc123.js).
2457
+ Read ONLY the 1-3 source files directly implicated do NOT read all scripts.
2458
+ 4. Cross-reference with source code to find the root cause and provide before/after code.
2459
+ 5. For custom queries, use the data-scripting skill to write targeted scripts.
1307
2460
 
1308
2461
  ### CRITICAL: Report EVERY distinct issue
1309
2462
 
@@ -1349,10 +2502,7 @@ Scripts in \`<head>\` without \`async\` or \`defer\` that block first paint.
1349
2502
  </head>
1350
2503
  \`\`\`
1351
2504
 
1352
- **How to detect:** Read /trace/summary.json for \`renderBlockingResources\`. For each
1353
- render-blocking script, read the actual source file (from the \`path\` field) and judge
1354
- whether it MUST be synchronous (e.g., it modifies the DOM before paint) or can safely
1355
- be deferred.
2505
+ **How to detect:** Run \`execute_command: node skills/browser-analysis/helpers/analyze-waterfall.js\` to get a summary of render-blocking resources, large bundles, and sequential chains. For each render-blocking script, read the actual source file and judge whether it MUST be synchronous or can safely be deferred.
1356
2506
 
1357
2507
  ### 2. Render-Blocking CSS
1358
2508
 
@@ -1367,8 +2517,7 @@ Large stylesheets that block first contentful paint.
1367
2517
  <link rel="stylesheet" href="/styles/all.css" media="print" onload="this.media='all'">
1368
2518
  \`\`\`
1369
2519
 
1370
- **How to detect:** Check render-blocking resources of type "Stylesheet" in the trace
1371
- summary. Large stylesheets (>50KB) that block FCP are prime candidates for splitting.
2520
+ **How to detect:** The analyze-waterfall.js script flags render-blocking stylesheets. Large stylesheets (>50KB) that block FCP are prime candidates for splitting.
1372
2521
 
1373
2522
  ### 3. Large Bundles (>100KB)
1374
2523
 
@@ -1383,8 +2532,7 @@ const Chart = lazy(() => import('./components/Chart'));
1383
2532
  const DataGrid = lazy(() => import('./components/DataGrid'));
1384
2533
  \`\`\`
1385
2534
 
1386
- **How to detect:** Read /trace/network-waterfall.json and identify scripts >100KB.
1387
- Read their source to find imports or code that could be deferred.
2535
+ **How to detect:** The analyze-waterfall.js script identifies bundles >100KB. Read their source to find imports or code that could be deferred.
1388
2536
 
1389
2537
  ### 4. Sequential Waterfalls
1390
2538
 
@@ -1398,34 +2546,27 @@ Resources loaded sequentially that could be parallelised or preloaded.
1398
2546
  <link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>
1399
2547
  \`\`\`
1400
2548
 
1401
- **How to detect:** Read /trace/network-waterfall.json sorted by startTime. Look for
1402
- chains where Resource B starts AFTER Resource A finishes, and both are needed for
1403
- initial render. Calculate the potential savings from parallelisation.
2549
+ **How to detect:** The analyze-waterfall.js script detects sequential chains where Resource B starts AFTER Resource A finishes. Review the chains and calculate the potential savings from parallelisation.
1404
2550
 
1405
2551
  ### 5. Uncompressed or Poorly Cached Resources
1406
2552
 
1407
2553
  Assets served without compression or with missing cache headers.
1408
2554
 
1409
- **How to detect:** Check the network waterfall for resources where \`encodedSize\` is
1410
- close to \`decodedSize\` (no compression), or where large assets have no caching.
2555
+ **How to detect:** The analyze-waterfall.js script flags uncompressed resources where \`encodedSize\` is close to \`decodedSize\`, and large assets with no caching.
1411
2556
 
1412
2557
  ## Your workflow
1413
2558
 
1414
- 1. In your FIRST turn, call read_file for these data files in ONE batch:
1415
- - /trace/summary.json (PRIMARY timing + render-blocking resources)
1416
- - /trace/network-waterfall.json (request timing and sizes)
1417
- - /trace/asset-manifest.json (index of stored assets)
1418
- Do NOT use ls or glob. Do NOT read source files yet.
1419
- 2. From the trace summary, identify:
1420
- - Render-blocking resources (scripts and stylesheets)
1421
- - Long tasks during page load
1422
- - Large bundles (>100KB)
1423
- 3. From the network waterfall, identify:
1424
- - Sequential chains that could be parallelised
1425
- - Resources with high load times
1426
- 4. Read ONLY the source files flagged as problematic (render-blocking,
1427
- oversized, etc.) from "Available source files" above. Batch these reads.
1428
- 5. For EACH issue, verify with the source and provide before/after code
2559
+ 1. In your FIRST turn, run BOTH of these:
2560
+ a. Run the workspace overview:
2561
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
2562
+ b. Run the detailed waterfall analysis:
2563
+ execute_command: node skills/browser-analysis/helpers/analyze-waterfall.js
2564
+ Do NOT use ls, glob, or read_file on trace JSON files directly.
2565
+ 2. From the script outputs, identify render-blocking resources, large bundles,
2566
+ and sequential chains along with their workspace paths.
2567
+ 3. Read ONLY the source files flagged as problematic. Batch these reads.
2568
+ 4. For EACH issue, verify with the source and provide before/after code.
2569
+ 5. For deeper analysis, use the data-scripting skill to write custom scripts.
1429
2570
 
1430
2571
  ${BROWSER_TOOL_CALL_STRATEGY}
1431
2572
  ${VERIFICATION_RULES}
@@ -1466,9 +2607,7 @@ async function loadData() {
1466
2607
  }
1467
2608
  \`\`\`
1468
2609
 
1469
- **How to detect:** Read /trace/runtime/blocking-functions.json for functions with
1470
- duration >50ms. For each one, read the source file at the reported line number
1471
- to understand what the function does.
2610
+ **How to detect:** Run \`execute_command: node skills/browser-analysis/helpers/analyze-blockers.js\` to get a summary of blocking functions with durations, script locations, and compound blockers. For each one, read the source file at the reported line number to understand what the function does.
1472
2611
 
1473
2612
  **IMPORTANT — Compound blockers are SEPARATE findings:**
1474
2613
  If function A calls function B and B blocks the main thread, report TWO findings:
@@ -1499,9 +2638,7 @@ function onRouteChange(route) {
1499
2638
  }
1500
2639
  \`\`\`
1501
2640
 
1502
- **How to detect:** Read /trace/runtime/event-listeners.json for event types where
1503
- addCount >> removeCount. Then search the source files for addEventListener calls
1504
- for those event types.
2641
+ **How to detect:** Write a custom script using the data-scripting skill to query trace/runtime/event-listeners.json for event types where addCount >> removeCount. Then search the source files for addEventListener calls for those event types.
1505
2642
 
1506
2643
  ### 3. GC Pressure
1507
2644
 
@@ -1527,9 +2664,7 @@ function animate() {
1527
2664
  }
1528
2665
  \`\`\`
1529
2666
 
1530
- **How to detect:** Read /trace/runtime/summary.json for GC pause count and total
1531
- duration. If significant, cross-reference with /heap/summary.json to find which
1532
- constructors are responsible. Check source files for hot loops creating objects.
2667
+ **How to detect:** Write a custom script using the data-scripting skill to query trace/runtime/summary.json for GC pause count and total duration. If significant, cross-reference with heap data to find which constructors are responsible. Check source files for hot loops creating objects.
1533
2668
 
1534
2669
  ### 4. Layout Thrashing
1535
2670
 
@@ -1549,7 +2684,7 @@ elements.forEach((el, i) => {
1549
2684
  });
1550
2685
  \`\`\`
1551
2686
 
1552
- **How to detect:** Read /trace/runtime/raw-events.json and look for rapid
2687
+ **How to detect:** Read trace/runtime/raw-events.json and look for rapid
1553
2688
  alternation of Layout and scripting events. Also search source files for
1554
2689
  patterns that read offsetHeight/offsetWidth/getBoundingClientRect inside
1555
2690
  loops that also modify DOM styles.
@@ -1578,28 +2713,26 @@ window.addEventListener('scroll', () => {
1578
2713
  });
1579
2714
  \`\`\`
1580
2715
 
1581
- **How to detect:** Check /trace/runtime/summary.json for \`frequentEventTypes\`.
1582
- These are event types dispatched >10 times during the trace period. Search
1583
- source files for their handlers and check if they use throttle/debounce/rAF.
2716
+ **How to detect:** Write a custom script using the data-scripting skill to query trace/runtime/summary.json for \`frequentEventTypes\`. These are event types dispatched >10 times during the trace period. Search source files for their handlers and check if they use throttle/debounce/rAF.
1584
2717
 
1585
2718
  ## Your workflow
1586
2719
 
1587
- 1. In your FIRST turn, call read_file for these data files in ONE batch:
1588
- - /trace/runtime/blocking-functions.json (PRIMARY blocking functions)
1589
- - /trace/runtime/event-listeners.json (listener add/remove counts)
1590
- - /trace/runtime/frame-breakdown.json (scripting vs layout vs paint vs GC)
1591
- - /trace/runtime/summary.json (overview with GC stats, frequent events)
1592
- Do NOT use ls or glob. Do NOT read any source files yet.
1593
- 2. Analyze blocking functions: for each with duration >50ms, note the
1594
- scriptUrl and line number from the data.
1595
- 3. Derive workspace paths from the scriptUrl field (e.g. a script URL
1596
- ending in "abc123.js" maps to /scripts/abc123.js). Read ONLY the 1-3
1597
- source files directly implicated by blocking functions or listener
1598
- imbalances — do NOT read all scripts.
2720
+ 1. In your FIRST turn, run BOTH of these:
2721
+ a. Run the workspace overview:
2722
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
2723
+ b. Run the detailed blocking functions analysis:
2724
+ execute_command: node skills/browser-analysis/helpers/analyze-blockers.js
2725
+ Do NOT use ls, glob, or read_file on trace data files directly.
2726
+ 2. From the script outputs, note blocking functions, their durations, script
2727
+ locations, and compound blockers. Also note listener imbalances and GC stats
2728
+ from the workspace overview.
2729
+ 3. Derive workspace paths from scriptUrl (e.g. URL ending in "abc123.js" →
2730
+ scripts/abc123.js). Read ONLY the 1-3 source files directly implicated.
1599
2731
  4. Check for compound blockers: if function A calls blocking function B,
1600
- report BOTH as separate findings
1601
- 5. Check event listener imbalances and find the source code responsible
1602
- 6. Check GC stats and cross-reference with heap data if available
2732
+ report BOTH as separate findings.
2733
+ 5. For deeper listener imbalance or GC analysis, write a custom script using
2734
+ the data-scripting skill to query trace/runtime/event-listeners.json and
2735
+ trace/runtime/summary.json.
1603
2736
 
1604
2737
  ### CRITICAL: Report EACH pattern as a SEPARATE finding
1605
2738
 
@@ -1646,8 +2779,7 @@ Large \`<script>\` blocks in the HTML document that block rendering.
1646
2779
  </head>
1647
2780
  \`\`\`
1648
2781
 
1649
- **How to detect:** Read /html/document.html and look for inline \`<script>\` blocks
1650
- that are large (>20 lines or >1KB) and don't need to execute before first paint.
2782
+ **How to detect:** Run \`execute_command: node skills/browser-analysis/helpers/find-patterns.js\` to scan HTML, CSS, and JS files for known anti-patterns. Also read html/document.html to check for inline \`<script>\` blocks that are large (>20 lines or >1KB) and don't need to execute before first paint.
1651
2783
 
1652
2784
  ### 2. DOM Manipulation in Loops (Layout Thrashing)
1653
2785
 
@@ -1671,9 +2803,7 @@ function resizeAll(elements) {
1671
2803
  }
1672
2804
  \`\`\`
1673
2805
 
1674
- **How to detect:** Search source files for loops containing both DOM reads
1675
- (offsetWidth, offsetHeight, getBoundingClientRect, getComputedStyle) and
1676
- DOM writes (style.*, setAttribute, classList.*).
2806
+ **How to detect:** The find-patterns.js script detects DOM read+write patterns in loops. Review the flagged files for loops containing both DOM reads (offsetWidth, offsetHeight, getBoundingClientRect, getComputedStyle) and DOM writes (style.*, setAttribute, classList.*).
1677
2807
 
1678
2808
  ### 3. Missing Event Delegation
1679
2809
 
@@ -1693,9 +2823,7 @@ document.querySelector('.list').addEventListener('click', (e) => {
1693
2823
  });
1694
2824
  \`\`\`
1695
2825
 
1696
- **How to detect:** Search source files for querySelectorAll(...).forEach(... =>
1697
- addEventListener...) patterns or similar loops that add the same listener type
1698
- to many elements.
2826
+ **How to detect:** The find-patterns.js script flags querySelectorAll+forEach+addEventListener patterns. Review the flagged files for similar loops that add the same listener type to many elements.
1699
2827
 
1700
2828
  ### 4. Synchronous XMLHttpRequest or Blocking APIs
1701
2829
 
@@ -1712,9 +2840,7 @@ const response = await fetch('/api/data');
1712
2840
  const data = await response.json();
1713
2841
  \`\`\`
1714
2842
 
1715
- **How to detect:** Search source files for \`XMLHttpRequest\` with the third
1716
- argument set to \`false\`, or \`document.write()\`, or other deprecated
1717
- synchronous APIs.
2843
+ **How to detect:** The find-patterns.js script flags synchronous XHR, \`document.write()\`, and other deprecated synchronous APIs.
1718
2844
 
1719
2845
  ### 5. Non-Passive Scroll/Touch Event Listeners
1720
2846
 
@@ -1729,9 +2855,7 @@ document.addEventListener('touchstart', handler);
1729
2855
  document.addEventListener('touchstart', handler, { passive: true });
1730
2856
  \`\`\`
1731
2857
 
1732
- **How to detect:** Search source files for addEventListener calls with
1733
- 'scroll', 'touchstart', 'touchmove', or 'wheel' that don't specify
1734
- \`{ passive: true }\` as the options argument.
2858
+ **How to detect:** The find-patterns.js script flags non-passive listeners for scroll, touchstart, touchmove, and wheel events.
1735
2859
 
1736
2860
  ### 6. CSS Issues
1737
2861
 
@@ -1745,11 +2869,7 @@ document.addEventListener('touchstart', handler, { passive: true });
1745
2869
  /* GOOD: use <link> with preconnect for external fonts */
1746
2870
  \`\`\`
1747
2871
 
1748
- **How to detect:** Read CSS files and look for:
1749
- - \`@import\` statements (add network round-trips vs \`<link>\`)
1750
- - Complex selectors with many combinators
1751
- - Large unused rule blocks
1752
- - Missing \`will-change\` or \`contain\` for animated elements
2872
+ **How to detect:** The find-patterns.js script flags CSS \`@import\` statements and complex selectors. Also read CSS files directly to check for large unused rule blocks and missing \`will-change\` or \`contain\` for animated elements.
1753
2873
 
1754
2874
  ### 7. Missing Image Dimensions Causing Layout Shifts
1755
2875
 
@@ -1763,26 +2883,20 @@ Images without explicit width/height that cause Cumulative Layout Shift (CLS).
1763
2883
  <img src="/hero.jpg" width="1200" height="600" loading="lazy">
1764
2884
  \`\`\`
1765
2885
 
1766
- **How to detect:** Read /html/document.html and look for \`<img>\` tags without
1767
- \`width\` and \`height\` attributes.
2886
+ **How to detect:** The find-patterns.js script flags \`<img>\` tags without \`width\` and \`height\` attributes.
1768
2887
 
1769
2888
  ## Your workflow
1770
2889
 
1771
- 1. In your FIRST turn, call read_file for HTML and CSS files (listed under
1772
- "Data files" above) in ONE batch. Do NOT read script files yet.
1773
- 2. Check HTML for: inline \`<script>\` blocks, \`<img>\` without width/height,
1774
- render-blocking resource references.
1775
- 3. Check CSS for: \`@import\` statements, complex selectors, missing
1776
- \`will-change\`/\`contain\` for animated elements.
1777
- 4. Based on issues found in HTML/CSS, read ONLY the script files that need
1778
- inspection (from "Available script files" above). For example:
1779
- - Scripts referenced by inline patterns in HTML
1780
- - Scripts that the HTML loads synchronously
1781
- Batch these reads.
1782
- 5. Check each script for: DOM reads+writes in loops, querySelectorAll+forEach
1783
- +addEventListener (missing delegation), non-passive scroll/touch listeners,
1784
- synchronous XHR.
1785
- 6. Report EACH pattern as a separate finding with before/after code.
2890
+ 1. In your FIRST turn, run BOTH of these:
2891
+ a. Run the workspace overview:
2892
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
2893
+ b. Run the pattern finder:
2894
+ execute_command: node skills/browser-analysis/helpers/find-patterns.js
2895
+ This searches HTML, CSS, and JS files for known anti-patterns.
2896
+ 2. Also read HTML and CSS files directly (they're typically small) to check
2897
+ for inline scripts, img without dimensions, and CSS issues.
2898
+ 3. Based on issues found, read ONLY the script files that need inspection.
2899
+ 4. Report EACH pattern as a separate finding with before/after code.
1786
2900
 
1787
2901
  ### CRITICAL: Be thorough but selective
1788
2902
 
@@ -1964,7 +3078,8 @@ function buildSubagents(ctx) {
1964
3078
  return {
1965
3079
  name,
1966
3080
  description,
1967
- systemPrompt: insertFileListIntoPrompt(prompt, fileSection)
3081
+ systemPrompt: insertFileListIntoPrompt(prompt, fileSection),
3082
+ skills: ["skills/data-scripting/", "skills/browser-analysis/"]
1968
3083
  };
1969
3084
  });
1970
3085
  }
@@ -1975,12 +3090,13 @@ async function analyze(model, backend, spinner, context, { animateProgress = tru
1975
3090
  systemPrompt: BROWSER_ORCHESTRATOR_PROMPT,
1976
3091
  backend,
1977
3092
  subagents,
3093
+ skills: ["skills/"],
1978
3094
  responseFormat: toolStrategy(FindingsSchema)
1979
3095
  });
1980
3096
  const userMessage = context ? buildBrowserUserMessage(context) : [
1981
3097
  "Analyze the frontend performance data in this workspace.",
1982
3098
  "",
1983
- "Start by reading /heap/summary.json, /trace/summary.json, and /trace/runtime/summary.json",
3099
+ "Start by reading heap/summary.json, trace/summary.json, and trace/runtime/summary.json",
1984
3100
  "to understand the overall picture, then explore source files to verify root causes."
1985
3101
  ].join(`
1986
3102
  `);
@@ -2822,7 +3938,10 @@ async function createWorkspace(options) {
2822
3938
  }
2823
3939
  }
2824
3940
  }
2825
- const result = createWorkspaceFromFiles(files, "zeitzeuge-browser-workspace-");
3941
+ Object.assign(files, DATA_SCRIPTING_SKILL_FILES);
3942
+ Object.assign(files, BROWSER_ANALYSIS_SKILL_FILES);
3943
+ Object.assign(files, RESULT_WRITER_SKILL_FILES);
3944
+ const result = await createWorkspaceFromFiles(files);
2826
3945
  return {
2827
3946
  backend: result.backend,
2828
3947
  cleanup: result.cleanup,