zeitzeuge 0.7.4 → 0.8.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.
Files changed (2) hide show
  1. package/dist/cli.js +1256 -178
  2. package/package.json +1 -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,1118 @@ 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);
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; }
787
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
+ };
788
1909
  // ../utils/src/output/terminal.ts
789
1910
  import pc2 from "picocolors";
790
1911
  import ora from "ora";
@@ -971,7 +2092,7 @@ function formatBytes(bytes) {
971
2092
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
972
2093
  }
973
2094
  // ../utils/src/output/report.ts
974
- import { writeFileSync as writeFileSync2 } from "node:fs";
2095
+ import { writeFileSync } from "node:fs";
975
2096
  var SEVERITY_EMOJI = {
976
2097
  critical: "\uD83D\uDD34",
977
2098
  warning: "\uD83D\uDFE1",
@@ -1004,7 +2125,7 @@ var CATEGORY_LABELS2 = {
1004
2125
  };
1005
2126
  function writeReport(outputPath, options) {
1006
2127
  const md = generateMarkdown(options);
1007
- writeFileSync2(outputPath, md, "utf-8");
2128
+ writeFileSync(outputPath, md, "utf-8");
1008
2129
  return outputPath;
1009
2130
  }
1010
2131
  function generateMarkdown(options) {
@@ -1113,27 +2234,30 @@ import { createDeepAgent } from "deepagents";
1113
2234
  import { toolStrategy } from "langchain";
1114
2235
 
1115
2236
  // src/analysis/prompts/shared.ts
1116
- var BROWSER_TOOL_CALL_STRATEGY = `## CRITICAL: Tool call strategy — data first, source selectively
2237
+ var BROWSER_TOOL_CALL_STRATEGY = `## CRITICAL: Tool call strategy — scripts first, source selectively
2238
+
2239
+ Your FIRST turn MUST run analysis scripts against the data files (JSON) to
2240
+ extract a concise summary of issues. Use the pre-built helper scripts in
2241
+ skills/browser-analysis/helpers/ or write your own using the data-scripting
2242
+ skill. Do NOT read data JSON files directly with read_file.
1117
2243
 
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.
2244
+ After your analysis scripts identify specific issues, read at most 1-3
2245
+ source files that are directly implicated. Derive paths from script URLs
2246
+ in the data (e.g. a URL ending in "abc123.js" scripts/abc123.js).
1121
2247
 
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.
2248
+ PREFERRED actions:
2249
+ - execute_command with pre-built helper scripts or custom Node.js scripts
2250
+ - read_file for source code files you need to see verbatim
1127
2251
 
1128
2252
  FORBIDDEN actions:
1129
2253
  - ls — NEVER call ls.
1130
2254
  - 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.
2255
+ - read_file on data JSON files — use scripts to extract what you need.
2256
+ - Reading ALL source files only read the specific ones your scripts point to.
1133
2257
  - Reading more than 3 source files — focus on the most impactful issues.`;
1134
2258
  var MINIFIED_SOURCE_HANDLING = `## Handling minified / compiled source files
1135
2259
 
1136
- The JavaScript files in /scripts/ are captured from a PRODUCTION page. They are
2260
+ The JavaScript files in scripts/ are captured from a PRODUCTION page. They are
1137
2261
  almost always minified, bundled, or compiled (e.g. by webpack, Vite, Turbopack).
1138
2262
  Signs of compiled code: single very long lines, mangled 1-2 character variable
1139
2263
  names, no whitespace or comments.
@@ -1181,11 +2305,11 @@ critical. Even shorter blocking calls prevent the main thread from processing
1181
2305
  user interactions and paint updates.`;
1182
2306
  var CROSS_REFERENCING = `## Cross-referencing data
1183
2307
 
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
2308
+ - When a script appears in BOTH trace/runtime/blocking-functions.json (CPU)
2309
+ AND heap/summary.json (memory), mention both dimensions in the finding.
2310
+ - Check trace/runtime/event-listeners.json for listener imbalances and
2311
+ cross-reference with the actual addEventListener calls in scripts/.
2312
+ - Use trace/network-waterfall.json to identify sequential chains, then read
1189
2313
  the initiating script to confirm the dependency.
1190
2314
  - When GC pauses are significant, cross-reference with heap data to identify
1191
2315
  which constructors or allocation patterns are responsible.`;
@@ -1222,9 +2346,7 @@ document.body.removeChild(el);
1222
2346
  // 'el' still holds a reference → detached DOM node
1223
2347
  \`\`\`
1224
2348
 
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.
2349
+ **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
2350
 
1229
2351
  ### 2. Large Retained Objects
1230
2352
 
@@ -1240,10 +2362,7 @@ class DataStore {
1240
2362
  }
1241
2363
  \`\`\`
1242
2364
 
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.
2365
+ **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
2366
 
1248
2367
  ### 3. Constructor Hotspots
1249
2368
 
@@ -1260,8 +2379,7 @@ function processItems(items) {
1260
2379
  }
1261
2380
  \`\`\`
1262
2381
 
1263
- **How to detect:** Read \`constructorStats\` in the heap summary. Focus on types
1264
- with unusually high instance counts or total size.
2382
+ **How to detect:** The analyze-heap.js script outputs constructor hotspots. Focus on types with unusually high instance counts.
1265
2383
 
1266
2384
  ### 4. Closure Leaks
1267
2385
 
@@ -1278,32 +2396,27 @@ function setupHandler(response) {
1278
2396
  }
1279
2397
  \`\`\`
1280
2398
 
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.
2399
+ **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
2400
 
1285
2401
  ### 5. Unbounded Caches/Maps
1286
2402
 
1287
2403
  Data structures that grow monotonically without eviction, TTL, or size limits.
1288
2404
 
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.
2405
+ **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
2406
 
1292
2407
  ## Your workflow
1293
2408
 
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.
2409
+ 1. In your FIRST turn, run BOTH of these:
2410
+ a. Run the workspace overview:
2411
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
2412
+ b. Run the detailed heap analysis:
2413
+ execute_command: node skills/browser-analysis/helpers/analyze-heap.js
2414
+ Do NOT use ls, glob, or read_file on heap/summary.json directly.
2415
+ 2. From the script outputs, identify issues and the script URLs that need verification.
2416
+ 3. Derive workspace paths from script URLs (e.g. URL ending in "abc123.js" → scripts/abc123.js).
2417
+ Read ONLY the 1-3 source files directly implicated do NOT read all scripts.
2418
+ 4. Cross-reference with source code to find the root cause and provide before/after code.
2419
+ 5. For custom queries, use the data-scripting skill to write targeted scripts.
1307
2420
 
1308
2421
  ### CRITICAL: Report EVERY distinct issue
1309
2422
 
@@ -1349,10 +2462,7 @@ Scripts in \`<head>\` without \`async\` or \`defer\` that block first paint.
1349
2462
  </head>
1350
2463
  \`\`\`
1351
2464
 
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.
2465
+ **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
2466
 
1357
2467
  ### 2. Render-Blocking CSS
1358
2468
 
@@ -1367,8 +2477,7 @@ Large stylesheets that block first contentful paint.
1367
2477
  <link rel="stylesheet" href="/styles/all.css" media="print" onload="this.media='all'">
1368
2478
  \`\`\`
1369
2479
 
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.
2480
+ **How to detect:** The analyze-waterfall.js script flags render-blocking stylesheets. Large stylesheets (>50KB) that block FCP are prime candidates for splitting.
1372
2481
 
1373
2482
  ### 3. Large Bundles (>100KB)
1374
2483
 
@@ -1383,8 +2492,7 @@ const Chart = lazy(() => import('./components/Chart'));
1383
2492
  const DataGrid = lazy(() => import('./components/DataGrid'));
1384
2493
  \`\`\`
1385
2494
 
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.
2495
+ **How to detect:** The analyze-waterfall.js script identifies bundles >100KB. Read their source to find imports or code that could be deferred.
1388
2496
 
1389
2497
  ### 4. Sequential Waterfalls
1390
2498
 
@@ -1398,34 +2506,27 @@ Resources loaded sequentially that could be parallelised or preloaded.
1398
2506
  <link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>
1399
2507
  \`\`\`
1400
2508
 
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.
2509
+ **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
2510
 
1405
2511
  ### 5. Uncompressed or Poorly Cached Resources
1406
2512
 
1407
2513
  Assets served without compression or with missing cache headers.
1408
2514
 
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.
2515
+ **How to detect:** The analyze-waterfall.js script flags uncompressed resources where \`encodedSize\` is close to \`decodedSize\`, and large assets with no caching.
1411
2516
 
1412
2517
  ## Your workflow
1413
2518
 
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
2519
+ 1. In your FIRST turn, run BOTH of these:
2520
+ a. Run the workspace overview:
2521
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
2522
+ b. Run the detailed waterfall analysis:
2523
+ execute_command: node skills/browser-analysis/helpers/analyze-waterfall.js
2524
+ Do NOT use ls, glob, or read_file on trace JSON files directly.
2525
+ 2. From the script outputs, identify render-blocking resources, large bundles,
2526
+ and sequential chains along with their workspace paths.
2527
+ 3. Read ONLY the source files flagged as problematic. Batch these reads.
2528
+ 4. For EACH issue, verify with the source and provide before/after code.
2529
+ 5. For deeper analysis, use the data-scripting skill to write custom scripts.
1429
2530
 
1430
2531
  ${BROWSER_TOOL_CALL_STRATEGY}
1431
2532
  ${VERIFICATION_RULES}
@@ -1466,9 +2567,7 @@ async function loadData() {
1466
2567
  }
1467
2568
  \`\`\`
1468
2569
 
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.
2570
+ **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
2571
 
1473
2572
  **IMPORTANT — Compound blockers are SEPARATE findings:**
1474
2573
  If function A calls function B and B blocks the main thread, report TWO findings:
@@ -1499,9 +2598,7 @@ function onRouteChange(route) {
1499
2598
  }
1500
2599
  \`\`\`
1501
2600
 
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.
2601
+ **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
2602
 
1506
2603
  ### 3. GC Pressure
1507
2604
 
@@ -1527,9 +2624,7 @@ function animate() {
1527
2624
  }
1528
2625
  \`\`\`
1529
2626
 
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.
2627
+ **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
2628
 
1534
2629
  ### 4. Layout Thrashing
1535
2630
 
@@ -1549,7 +2644,7 @@ elements.forEach((el, i) => {
1549
2644
  });
1550
2645
  \`\`\`
1551
2646
 
1552
- **How to detect:** Read /trace/runtime/raw-events.json and look for rapid
2647
+ **How to detect:** Read trace/runtime/raw-events.json and look for rapid
1553
2648
  alternation of Layout and scripting events. Also search source files for
1554
2649
  patterns that read offsetHeight/offsetWidth/getBoundingClientRect inside
1555
2650
  loops that also modify DOM styles.
@@ -1578,28 +2673,26 @@ window.addEventListener('scroll', () => {
1578
2673
  });
1579
2674
  \`\`\`
1580
2675
 
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.
2676
+ **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
2677
 
1585
2678
  ## Your workflow
1586
2679
 
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.
2680
+ 1. In your FIRST turn, run BOTH of these:
2681
+ a. Run the workspace overview:
2682
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
2683
+ b. Run the detailed blocking functions analysis:
2684
+ execute_command: node skills/browser-analysis/helpers/analyze-blockers.js
2685
+ Do NOT use ls, glob, or read_file on trace data files directly.
2686
+ 2. From the script outputs, note blocking functions, their durations, script
2687
+ locations, and compound blockers. Also note listener imbalances and GC stats
2688
+ from the workspace overview.
2689
+ 3. Derive workspace paths from scriptUrl (e.g. URL ending in "abc123.js" →
2690
+ scripts/abc123.js). Read ONLY the 1-3 source files directly implicated.
1599
2691
  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
2692
+ report BOTH as separate findings.
2693
+ 5. For deeper listener imbalance or GC analysis, write a custom script using
2694
+ the data-scripting skill to query trace/runtime/event-listeners.json and
2695
+ trace/runtime/summary.json.
1603
2696
 
1604
2697
  ### CRITICAL: Report EACH pattern as a SEPARATE finding
1605
2698
 
@@ -1646,8 +2739,7 @@ Large \`<script>\` blocks in the HTML document that block rendering.
1646
2739
  </head>
1647
2740
  \`\`\`
1648
2741
 
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.
2742
+ **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
2743
 
1652
2744
  ### 2. DOM Manipulation in Loops (Layout Thrashing)
1653
2745
 
@@ -1671,9 +2763,7 @@ function resizeAll(elements) {
1671
2763
  }
1672
2764
  \`\`\`
1673
2765
 
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.*).
2766
+ **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
2767
 
1678
2768
  ### 3. Missing Event Delegation
1679
2769
 
@@ -1693,9 +2783,7 @@ document.querySelector('.list').addEventListener('click', (e) => {
1693
2783
  });
1694
2784
  \`\`\`
1695
2785
 
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.
2786
+ **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
2787
 
1700
2788
  ### 4. Synchronous XMLHttpRequest or Blocking APIs
1701
2789
 
@@ -1712,9 +2800,7 @@ const response = await fetch('/api/data');
1712
2800
  const data = await response.json();
1713
2801
  \`\`\`
1714
2802
 
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.
2803
+ **How to detect:** The find-patterns.js script flags synchronous XHR, \`document.write()\`, and other deprecated synchronous APIs.
1718
2804
 
1719
2805
  ### 5. Non-Passive Scroll/Touch Event Listeners
1720
2806
 
@@ -1729,9 +2815,7 @@ document.addEventListener('touchstart', handler);
1729
2815
  document.addEventListener('touchstart', handler, { passive: true });
1730
2816
  \`\`\`
1731
2817
 
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.
2818
+ **How to detect:** The find-patterns.js script flags non-passive listeners for scroll, touchstart, touchmove, and wheel events.
1735
2819
 
1736
2820
  ### 6. CSS Issues
1737
2821
 
@@ -1745,11 +2829,7 @@ document.addEventListener('touchstart', handler, { passive: true });
1745
2829
  /* GOOD: use <link> with preconnect for external fonts */
1746
2830
  \`\`\`
1747
2831
 
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
2832
+ **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
2833
 
1754
2834
  ### 7. Missing Image Dimensions Causing Layout Shifts
1755
2835
 
@@ -1763,26 +2843,20 @@ Images without explicit width/height that cause Cumulative Layout Shift (CLS).
1763
2843
  <img src="/hero.jpg" width="1200" height="600" loading="lazy">
1764
2844
  \`\`\`
1765
2845
 
1766
- **How to detect:** Read /html/document.html and look for \`<img>\` tags without
1767
- \`width\` and \`height\` attributes.
2846
+ **How to detect:** The find-patterns.js script flags \`<img>\` tags without \`width\` and \`height\` attributes.
1768
2847
 
1769
2848
  ## Your workflow
1770
2849
 
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.
2850
+ 1. In your FIRST turn, run BOTH of these:
2851
+ a. Run the workspace overview:
2852
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
2853
+ b. Run the pattern finder:
2854
+ execute_command: node skills/browser-analysis/helpers/find-patterns.js
2855
+ This searches HTML, CSS, and JS files for known anti-patterns.
2856
+ 2. Also read HTML and CSS files directly (they're typically small) to check
2857
+ for inline scripts, img without dimensions, and CSS issues.
2858
+ 3. Based on issues found, read ONLY the script files that need inspection.
2859
+ 4. Report EACH pattern as a separate finding with before/after code.
1786
2860
 
1787
2861
  ### CRITICAL: Be thorough but selective
1788
2862
 
@@ -1964,7 +3038,8 @@ function buildSubagents(ctx) {
1964
3038
  return {
1965
3039
  name,
1966
3040
  description,
1967
- systemPrompt: insertFileListIntoPrompt(prompt, fileSection)
3041
+ systemPrompt: insertFileListIntoPrompt(prompt, fileSection),
3042
+ skills: ["skills/data-scripting/", "skills/browser-analysis/"]
1968
3043
  };
1969
3044
  });
1970
3045
  }
@@ -1975,12 +3050,13 @@ async function analyze(model, backend, spinner, context, { animateProgress = tru
1975
3050
  systemPrompt: BROWSER_ORCHESTRATOR_PROMPT,
1976
3051
  backend,
1977
3052
  subagents,
3053
+ skills: ["skills/"],
1978
3054
  responseFormat: toolStrategy(FindingsSchema)
1979
3055
  });
1980
3056
  const userMessage = context ? buildBrowserUserMessage(context) : [
1981
3057
  "Analyze the frontend performance data in this workspace.",
1982
3058
  "",
1983
- "Start by reading /heap/summary.json, /trace/summary.json, and /trace/runtime/summary.json",
3059
+ "Start by reading heap/summary.json, trace/summary.json, and trace/runtime/summary.json",
1984
3060
  "to understand the overall picture, then explore source files to verify root causes."
1985
3061
  ].join(`
1986
3062
  `);
@@ -2822,7 +3898,9 @@ async function createWorkspace(options) {
2822
3898
  }
2823
3899
  }
2824
3900
  }
2825
- const result = createWorkspaceFromFiles(files, "zeitzeuge-browser-workspace-");
3901
+ Object.assign(files, DATA_SCRIPTING_SKILL_FILES);
3902
+ Object.assign(files, BROWSER_ANALYSIS_SKILL_FILES);
3903
+ const result = await createWorkspaceFromFiles(files);
2826
3904
  return {
2827
3905
  backend: result.backend,
2828
3906
  cleanup: result.cleanup,