Identify computational bottlenecks, analyze parallel scaling, estimate memory requirements, and generate optimization recommendations for materials simulations — parse timing logs to find dominant phases (solver, assembly, I/O), evaluate strong and weak scaling efficiency, profile memory from mesh and field parameters, and detect bottlenecks with actionable fix suggestions. Use when a simulation is running slower than expected, investigating MPI scaling efficiency, planning HPC resource allocation, deciding whether to tune the preconditioner or reduce I/O frequency, or estimating if a problem fits in available RAM, even if the user only says "my simulation is too slow" or "how many nodes do I need."
Provide tools to analyze simulation performance, identify bottlenecks, and recommend optimization strategies for computational materials science simulations.
Requirements
Python 3.10+
No external dependencies (uses Python standard library only)
Works on Linux, macOS, and Windows
Inputs to Gather
Before running profiling scripts, collect from the user:
Input
Description
Example
Simulation log
Log file with timing information
simulation.log
Scaling data
JSON with multi-run performance data
scaling_data.json
Simulation parameters
JSON with mesh, fields, solver config
params.json
Available memory
System memory in GB (optional)
16.0
Decision Guidance
When to Use Each Script
Need to identify slow phases?
├── YES → Use timing_analyzer.py
│ └── Parse simulation logs for timing data
│
Need to understand parallel performance?
├── YES → Use scaling_analyzer.py
│ └── Analyze strong or weak scaling efficiency
│
Need to estimate memory requirements?
├── YES → Use memory_profiler.py
│ └── Estimate memory from problem parameters
│
Need optimization recommendations?
└── YES → Use bottleneck_detector.py
└── Combine analyses and get actionable advice
Choosing Analysis Thresholds
Metric
Good
Acceptable
Poor
Phase dominance
<30%
30-50%
>50%
Parallel efficiency
>0.80
0.70-0.80
<0.70
Memory usage
<60%
60-80%
>80%
Script Outputs (JSON Fields)
All scripts wrap their payload in a top-level object with two keys: inputs and results. The fields below live under results.
The detector applies per-type dominance thresholds: solver/assembly/general phases are flagged above 50% of runtime; I/O phases above 30%. Any flagged phase above 70% is reported as high severity.
Scenario
Meaning
Action
Solver >50% (high >70%)
Solver-dominated
Tune preconditioner, check tolerance
Assembly >50%
Assembly-dominated
Cache matrices, vectorize, parallelize
I/O >30%
I/O-dominated
Reduce frequency, use parallel I/O
Balanced (below thresholds)
Well-balanced
Look for algorithmic improvements
Scaling Analysis
Efficiency
Meaning
Action
>0.80
Excellent scaling
Continue scaling up
0.70-0.80
Good scaling
Monitor at larger scales
0.50-0.70
Poor scaling
Investigate communication/load balance
<0.50
Very poor scaling
Reduce processor count or redesign
Memory Profile
Usage
Meaning
Action
<60% available
Safe
No action needed
60-80% available
Moderate
Monitor, consider optimization
>80% available
High
Reduce resolution or increase processors
>100% available
Exceeds capacity
Must reduce problem size
The estimate follows the three-term formula Total = Field + Solver Workspace + Matrix Storage (see references/profiling_guide.md). Matrix storage and solver workspace depend on solver.type:
iterative (default): sparse matrix (default 7-point stencil, override via solver.stencil_nnz) plus workspace vectors.
direct: sparse matrix scaled by a conservative fill-in factor (solver.fillin_factor, default 10) to reflect factorization fill-in — a direct solver estimates far more memory than an iterative one for the same mesh.
matrix-free: no assembled matrix; workspace vectors only.
The estimate is intentionally conservative so a "will it fit in RAM?" decision does not silently under-estimate.
Error Handling
Error
Cause
Resolution
Log file not found
Invalid path
Verify log file path
No timing data found
Pattern mismatch
Provide custom pattern with --pattern
At least 2 runs required
Insufficient data
Provide more scaling runs
Missing required parameters
Incomplete params
Add mesh and fields to params file
Optimization Strategies by Bottleneck Type
Solver Bottlenecks
Use algebraic multigrid (AMG) preconditioner
Tighten solver tolerance if over-solving
Consider direct solver for small problems
Profile matrix assembly vs solve time
Assembly Bottlenecks
Cache element matrices if geometry is static
Use vectorized assembly routines
Consider matrix-free methods
Parallelize assembly with coloring
I/O Bottlenecks
Reduce output frequency
Use parallel I/O (HDF5, MPI-IO)
Write to fast scratch storage
Compress output data
Scaling Bottlenecks
Investigate communication overhead
Check for load imbalance
Reduce synchronization points
Use asynchronous communication
Consider hybrid MPI+OpenMP
Memory Bottlenecks
Reduce mesh resolution
Use iterative solver (lower memory than direct)
Enable out-of-core computation
Increase number of processors
Use single precision where appropriate
Verification checklist
Before trusting a profiling result or acting on a recommendation, record the concrete evidence below:
Confirmed timing_analyzer.py actually matched entries — results.phases is non-empty and results.total_time > 0; if a custom --pattern was used and results.message/suggested_patterns appeared, the pattern was fixed and re-run (an empty phases list silently looks like a fast simulation).
Cross-checked that the sum of phases[].percentage is ~100% and that named phases cover the wall-clock time — unaccounted-for time means missing log lines, not a balanced run.
For scaling claims, used >=2 runs spanning a real processor range and recorded results.average_efficiency and results.efficiency_threshold_processors from scaling_analyzer.py; verified the --type (strong vs weak) matches how the runs were generated (fixed total size vs fixed work-per-rank).
Recorded the memory breakdown from memory_profiler.py (field_memory_gb, solver_workspace_gb, matrix_storage_gb, total_memory_gb) and confirmed solver.type (iterative / direct / matrix-free) matches the real solver — a direct solve carries the ~10x fill-in factor and a wrong type makes the "fits in RAM?" answer unsafe.
Checked results.warnings and compared total_memory_gb (and per_process_gb) against the actual --available-gb; treated >80% as the documented "high" band, not a pass.
For each bottleneck_detector.py recommendation, confirmed the driving bottleneck (its category, value, and threshold) is consistent with the timing/scaling/memory inputs that were actually supplied — recommendations only reflect the JSON files passed via --timing/--scaling/--memory.
After implementing an optimization, re-ran the relevant analyzer and recorded the before/after value to confirm the bottleneck actually moved (re-profile step of the workflow).
Common pitfalls & rationalizations
Tempting shortcut
Why it's wrong / what to do
"timing_analyzer.py returned no bottlenecks, so the run is balanced."
An empty/low result is often a pattern mismatch — phases may be empty or partial. Verify total_time matches wall-clock and that phase percentages sum to ~100% before concluding "balanced".
"Two runs scaled fine, so it scales."
Two points only give an average efficiency; they cannot reveal where efficiency falls off. Add more processor counts and check efficiency_threshold_processors, and confirm you used the correct --type (strong vs weak).
"Iterative vs direct is just a flag; memory is about the same."
memory_profiler.py applies a conservative ~10x fill-in factor for direct and stores no matrix for matrix-free. Setting the wrong solver.type can under-estimate RAM by an order of magnitude — set it to the real solver.
"It fits in --available-gb total, so we're fine."
The relevant number for an MPI run is per_process_gb against per-node/per-rank RAM, and >80% of total already triggers a warning. Check the per-process figure and the warnings list, not just the total.
"I/O is under 50%, so I/O isn't the bottleneck."
I/O is flagged at the lower 30% threshold, not 50%. A 30-50% I/O phase is a real bottleneck the detector reports — reduce output frequency or use parallel I/O.
"The recommendation says tune the preconditioner, so the solver is the problem."
Recommendations are only as complete as the JSON you passed in. If --scaling/--memory were omitted, those bottlenecks are simply invisible — feed all available analyses before trusting the priority ranking.
Security
Input Validation
User-supplied --pattern regex values are validated for length (500 chars max) and rejected if they contain constructs prone to catastrophic backtracking (ReDoS)
Scaling data entries are validated for finite time values, integer processor counts, and bounded run count (10,000 max)
available_gb is validated as a positive finite number; mesh dimensions and field parameters are validated as positive integers
--type (scaling type) is validated against a fixed allowlist (strong, weak)
All loaded JSON files must have an object (dict) as root element
File Access
timing_analyzer.py reads a single log file specified by --log; log files are capped at 500 MB and rejected before parsing
scaling_analyzer.py, memory_profiler.py, and bottleneck_detector.py read JSON files capped at 100 MB
Phase names extracted from log files are truncated to 200 characters and stripped of control characters to prevent prompt-injection payloads from propagating into agent context
No scripts write to the filesystem; all output goes to stdout
Tool Restrictions
Read: Used to inspect script source, references, simulation logs, and result files
Write: Used to save profiling reports or optimization recommendations; writes are scoped to the user's working directory
Grep/Glob: Used to locate log files, result files, and search references
The skill's allowed-tools excludes Bash to prevent the agent from executing arbitrary commands when processing untrusted simulation logs or result files
Safety Measures
No eval(), exec(), or dynamic code generation
All subprocess calls use explicit argument lists (no shell=True)
Reduced tool surface (no Bash) limits the agent to read/write operations only
Phase names and diagnostic strings are sanitized before inclusion in output to prevent injection
Limitations
Log parsing: Depends on pattern matching; may miss unusual formats
Scaling analysis: Requires at least 2 runs for meaningful results
Memory estimation: Approximate; actual usage may vary
Recommendations: General guidance; may need domain-specific tuning
References
references/profiling_guide.md - Profiling concepts and interpretation
See CHANGELOG.md for the authoritative, dated release history.
1---2name: performance-profiling3description: Identify computational bottlenecks, analyze parallel scaling, estimate memory requirements, and generate optimization recommendations for materials simulations — parse timing logs to find dominant phases (solver, assembly, I/O), evaluate strong and weak scaling efficiency, profile memory from mesh and field parameters, and detect bottlenecks with actionable fix suggestions. Use when a simulation is running slower than expected, investigating MPI scaling efficiency, planning HPC resource allocation, deciding whether to tune the preconditioner or reduce I/O frequency, or estimating if a problem fits in available RAM, even if the user only says "my simulation is too slow" or "how many nodes do I need."4---56# Performance Profiling78## Goal910Provide tools to analyze simulation performance, identify bottlenecks, and recommend optimization strategies for computational materials science simulations.1112## Requirements1314- Python 3.10+15- No external dependencies (uses Python standard library only)16- Works on Linux, macOS, and Windows1718## Inputs to Gather1920Before running profiling scripts, collect from the user:2122| Input | Description | Example |23|-------|-------------|---------|24| Simulation log | Log file with timing information | `simulation.log` |25| Scaling data | JSON with multi-run performance data | `scaling_data.json` |26| Simulation parameters | JSON with mesh, fields, solver config | `params.json` |27| Available memory | System memory in GB (optional) | `16.0` |2829## Decision Guidance3031### When to Use Each Script3233```34Need to identify slow phases?35├── YES → Use timing_analyzer.py36│ └── Parse simulation logs for timing data37│38Need to understand parallel performance?39├── YES → Use scaling_analyzer.py40│ └── Analyze strong or weak scaling efficiency41│42Need to estimate memory requirements?43├── YES → Use memory_profiler.py44│ └── Estimate memory from problem parameters45│46Need optimization recommendations?47└── YES → Use bottleneck_detector.py48 └── Combine analyses and get actionable advice49```5051### Choosing Analysis Thresholds5253| Metric | Good | Acceptable | Poor |54|--------|------|------------|------|55| Phase dominance | <30% | 30-50% | >50% |56| Parallel efficiency | >0.80 | 0.70-0.80 | <0.70 |57| Memory usage | <60% | 60-80% | >80% |5859## Script Outputs (JSON Fields)6061All scripts wrap their payload in a top-level object with two keys: `inputs` and `results`. The fields below live under `results`.6263| Script | Key Outputs (under `results`) |64|--------|-------------|65| `timing_analyzer.py` | `results.phases`, `results.slowest_phase`, `results.total_time` |66| `scaling_analyzer.py` | `results.results`, `results.efficiency_threshold_processors`, `results.average_efficiency`, `results.baseline` |67| `memory_profiler.py` | `results.total_memory_gb`, `results.per_process_gb`, `results.field_memory_gb`, `results.solver_workspace_gb`, `results.matrix_storage_gb`, `results.warnings` |68| `bottleneck_detector.py` | `results.bottlenecks`, `results.recommendations` |6970## Workflow7172### Complete Profiling Workflow73741. **Analyze timing** from simulation logs752. **Analyze scaling** from multi-run data (if available)763. **Profile memory** from simulation parameters774. **Detect bottlenecks** and get recommendations785. **Implement optimizations** based on recommendations796. **Re-profile** to verify improvements8081### Quick Profiling (Timing Only)82831. **Run timing analyzer** on simulation log842. **Identify dominant phases** (>50% of runtime)853. **Apply targeted optimizations** to dominant phases8687## CLI Examples8889### Timing Analysis9091```bash92# Basic timing analysis93python3 scripts/timing_analyzer.py \94 --log simulation.log \95 --json9697# Custom timing pattern98python3 scripts/timing_analyzer.py \99 --log simulation.log \100 --pattern 'Step\s+(\w+)\s+took\s+([\d.]+)s' \101 --json102```103104### Scaling Analysis105106```bash107# Strong scaling (fixed problem size)108python3 scripts/scaling_analyzer.py \109 --data scaling_data.json \110 --type strong \111 --json112113# Weak scaling (constant work per processor)114python3 scripts/scaling_analyzer.py \115 --data scaling_data.json \116 --type weak \117 --json118```119120### Memory Profiling121122```bash123# Estimate memory requirements124python3 scripts/memory_profiler.py \125 --params simulation_params.json \126 --available-gb 16.0 \127 --json128```129130### Bottleneck Detection131132```bash133# Detect bottlenecks from timing only134python3 scripts/bottleneck_detector.py \135 --timing timing_results.json \136 --json137138# Comprehensive analysis with all inputs139python3 scripts/bottleneck_detector.py \140 --timing timing_results.json \141 --scaling scaling_results.json \142 --memory memory_results.json \143 --json144```145146## Conversational Workflow Example147148**User**: My simulation is taking too long. Can you help me identify what's slow?149150**Agent workflow**:1511. Ask for simulation log file1522. Run timing analyzer:153 ```bash154 python3 scripts/timing_analyzer.py --log simulation.log --json155 ```1563. Interpret results (the detector flags solver/assembly phases above 50% and I/O phases above 30%; >70% is high severity):157 - If solver dominates (>50%, high above 70%): Recommend preconditioner tuning158 - If assembly dominates (>50%): Recommend caching or vectorization159 - If I/O dominates (>30%): Recommend reducing output frequency1604. If user has multi-run data, analyze scaling:161 ```bash162 python3 scripts/scaling_analyzer.py --data scaling.json --type strong --json163 ```1645. Generate comprehensive recommendations:165 ```bash166 python3 scripts/bottleneck_detector.py --timing timing.json --scaling scaling.json --json167 ```168169## Interpretation Guidance170171### Timing Analysis172173The detector applies per-type dominance thresholds: solver/assembly/general phases are flagged above **50%** of runtime; I/O phases above **30%**. Any flagged phase above **70%** is reported as high severity.174175| Scenario | Meaning | Action |176|----------|---------|--------|177| Solver >50% (high >70%) | Solver-dominated | Tune preconditioner, check tolerance |178| Assembly >50% | Assembly-dominated | Cache matrices, vectorize, parallelize |179| I/O >30% | I/O-dominated | Reduce frequency, use parallel I/O |180| Balanced (below thresholds) | Well-balanced | Look for algorithmic improvements |181182### Scaling Analysis183184| Efficiency | Meaning | Action |185|------------|---------|--------|186| >0.80 | Excellent scaling | Continue scaling up |187| 0.70-0.80 | Good scaling | Monitor at larger scales |188| 0.50-0.70 | Poor scaling | Investigate communication/load balance |189| <0.50 | Very poor scaling | Reduce processor count or redesign |190191### Memory Profile192193| Usage | Meaning | Action |194|-------|---------|--------|195| <60% available | Safe | No action needed |196| 60-80% available | Moderate | Monitor, consider optimization |197| >80% available | High | Reduce resolution or increase processors |198| >100% available | Exceeds capacity | Must reduce problem size |199200The estimate follows the three-term formula `Total = Field + Solver Workspace + Matrix Storage` (see `references/profiling_guide.md`). Matrix storage and solver workspace depend on `solver.type`:201202- `iterative` (default): sparse matrix (default 7-point stencil, override via `solver.stencil_nnz`) plus workspace vectors.203- `direct`: sparse matrix scaled by a conservative fill-in factor (`solver.fillin_factor`, default 10) to reflect factorization fill-in — a direct solver estimates far more memory than an iterative one for the same mesh.204- `matrix-free`: no assembled matrix; workspace vectors only.205206The estimate is intentionally conservative so a "will it fit in RAM?" decision does not silently under-estimate.207208## Error Handling209210| Error | Cause | Resolution |211|-------|-------|------------|212| `Log file not found` | Invalid path | Verify log file path |213| `No timing data found` | Pattern mismatch | Provide custom pattern with --pattern |214| `At least 2 runs required` | Insufficient data | Provide more scaling runs |215| `Missing required parameters` | Incomplete params | Add mesh and fields to params file |216217## Optimization Strategies by Bottleneck Type218219### Solver Bottlenecks220- Use algebraic multigrid (AMG) preconditioner221- Tighten solver tolerance if over-solving222- Consider direct solver for small problems223- Profile matrix assembly vs solve time224225### Assembly Bottlenecks226- Cache element matrices if geometry is static227- Use vectorized assembly routines228- Consider matrix-free methods229- Parallelize assembly with coloring230231### I/O Bottlenecks232- Reduce output frequency233- Use parallel I/O (HDF5, MPI-IO)234- Write to fast scratch storage235- Compress output data236237### Scaling Bottlenecks238- Investigate communication overhead239- Check for load imbalance240- Reduce synchronization points241- Use asynchronous communication242- Consider hybrid MPI+OpenMP243244### Memory Bottlenecks245- Reduce mesh resolution246- Use iterative solver (lower memory than direct)247- Enable out-of-core computation248- Increase number of processors249- Use single precision where appropriate250251## Verification checklist252253Before trusting a profiling result or acting on a recommendation, record the concrete evidence below:254255- [ ] Confirmed `timing_analyzer.py` actually matched entries — `results.phases` is non-empty and `results.total_time` > 0; if a custom `--pattern` was used and `results.message`/`suggested_patterns` appeared, the pattern was fixed and re-run (an empty `phases` list silently looks like a fast simulation).256- [ ] Cross-checked that the sum of `phases[].percentage` is ~100% and that named phases cover the wall-clock time — unaccounted-for time means missing log lines, not a balanced run.257- [ ] For scaling claims, used >=2 runs spanning a real processor range and recorded `results.average_efficiency` and `results.efficiency_threshold_processors` from `scaling_analyzer.py`; verified the `--type` (strong vs weak) matches how the runs were generated (fixed total size vs fixed work-per-rank).258- [ ] Recorded the memory breakdown from `memory_profiler.py` (`field_memory_gb`, `solver_workspace_gb`, `matrix_storage_gb`, `total_memory_gb`) and confirmed `solver.type` (iterative / direct / matrix-free) matches the real solver — a direct solve carries the ~10x fill-in factor and a wrong type makes the "fits in RAM?" answer unsafe.259- [ ] Checked `results.warnings` and compared `total_memory_gb` (and `per_process_gb`) against the actual `--available-gb`; treated >80% as the documented "high" band, not a pass.260- [ ] For each `bottleneck_detector.py` recommendation, confirmed the driving `bottleneck` (its `category`, `value`, and `threshold`) is consistent with the timing/scaling/memory inputs that were actually supplied — recommendations only reflect the JSON files passed via `--timing`/`--scaling`/`--memory`.261- [ ] After implementing an optimization, re-ran the relevant analyzer and recorded the before/after `value` to confirm the bottleneck actually moved (re-profile step of the workflow).262263## Common pitfalls & rationalizations264265| Tempting shortcut | Why it's wrong / what to do |266|-------------------|------------------------------|267| "`timing_analyzer.py` returned no bottlenecks, so the run is balanced." | An empty/low result is often a pattern mismatch — `phases` may be empty or partial. Verify `total_time` matches wall-clock and that phase percentages sum to ~100% before concluding "balanced". |268| "Two runs scaled fine, so it scales." | Two points only give an average efficiency; they cannot reveal where efficiency falls off. Add more processor counts and check `efficiency_threshold_processors`, and confirm you used the correct `--type` (strong vs weak). |269| "Iterative vs direct is just a flag; memory is about the same." | `memory_profiler.py` applies a conservative ~10x fill-in factor for `direct` and stores no matrix for `matrix-free`. Setting the wrong `solver.type` can under-estimate RAM by an order of magnitude — set it to the real solver. |270| "It fits in `--available-gb` total, so we're fine." | The relevant number for an MPI run is `per_process_gb` against per-node/per-rank RAM, and >80% of total already triggers a warning. Check the per-process figure and the `warnings` list, not just the total. |271| "I/O is under 50%, so I/O isn't the bottleneck." | I/O is flagged at the lower **30%** threshold, not 50%. A 30-50% I/O phase is a real bottleneck the detector reports — reduce output frequency or use parallel I/O. |272| "The recommendation says tune the preconditioner, so the solver is the problem." | Recommendations are only as complete as the JSON you passed in. If `--scaling`/`--memory` were omitted, those bottlenecks are simply invisible — feed all available analyses before trusting the priority ranking. |273274## Security275276### Input Validation277- User-supplied `--pattern` regex values are validated for length (500 chars max) and rejected if they contain constructs prone to catastrophic backtracking (ReDoS)278- Scaling data entries are validated for finite time values, integer processor counts, and bounded run count (10,000 max)279- `available_gb` is validated as a positive finite number; mesh dimensions and field parameters are validated as positive integers280- `--type` (scaling type) is validated against a fixed allowlist (`strong`, `weak`)281- All loaded JSON files must have an object (dict) as root element282283### File Access284- `timing_analyzer.py` reads a single log file specified by `--log`; log files are capped at 500 MB and rejected before parsing285- `scaling_analyzer.py`, `memory_profiler.py`, and `bottleneck_detector.py` read JSON files capped at 100 MB286- Phase names extracted from log files are truncated to 200 characters and stripped of control characters to prevent prompt-injection payloads from propagating into agent context287- No scripts write to the filesystem; all output goes to stdout288289### Tool Restrictions290- **Read**: Used to inspect script source, references, simulation logs, and result files291- **Write**: Used to save profiling reports or optimization recommendations; writes are scoped to the user's working directory292- **Grep/Glob**: Used to locate log files, result files, and search references293- The skill's `allowed-tools` excludes `Bash` to prevent the agent from executing arbitrary commands when processing untrusted simulation logs or result files294295### Safety Measures296- No `eval()`, `exec()`, or dynamic code generation297- All subprocess calls use explicit argument lists (no `shell=True`)298- Reduced tool surface (no Bash) limits the agent to read/write operations only299- Phase names and diagnostic strings are sanitized before inclusion in output to prevent injection300301## Limitations302303- **Log parsing**: Depends on pattern matching; may miss unusual formats304- **Scaling analysis**: Requires at least 2 runs for meaningful results305- **Memory estimation**: Approximate; actual usage may vary306- **Recommendations**: General guidance; may need domain-specific tuning307308## References309310- `references/profiling_guide.md` - Profiling concepts and interpretation311- `references/optimization_strategies.md` - Detailed optimization approaches312313## Version History314315See `CHANGELOG.md` for the authoritative, dated release history.
Run npx skillmds@latest add heshamfs/performance-profiling in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Identify computational bottlenecks, analyze parallel scaling, estimate memory requirements, and generate optimization recommendations for materials simulations — parse timing logs to find dominant phases (solver, assembly, I/O), evaluate strong and weak scaling efficiency, profile memory from mesh and field parameters, and detect bottlenecks with actionable fix suggestions. Use when a simulation is running slower than expected, investigating MPI scaling efficiency, planning HPC resource allocation, deciding whether to tune the preconditioner or reduce I/O frequency, or estimating if a problem fits in available RAM, even if the user only says "my simulation is too slow" or "how many nodes do I need." It is listed under Productivity on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: executes scripts. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
HeshamFS (@heshamfs) published this skill. Their other Agent Skills are listed on their SkillMD profile.