agentleFS
grzmolgrzmol/vibe-force/skills/sf-technical-debt-audit/SKILL.md

sf-technical-debt-audit

Skill2 starsChanged 2 months agoMITView on GitHub

Standing technical-debt audit of a Salesforce project and org - inventory the metadata surface, measure API version drift, test and coverage debt, trigger and automation sprawl, retired Workflow Rules and Process Builder still running, hardcoded ids and endpoints, profile-based permissions, dead fields and classes, Apex volume against the 6 MB org limit, and package and dependency debt; then rank findings by impact and effort into a debt register with an owner and a remediation skill per item. Use when taking over an unfamiliar org, planning a refactor or a release-hardening sprint, answering "why is every change here slow", or producing a debt report for an architecture review. Not for reviewing a diff - that is skill sf-code-analyzer-quality and the sf-quality-gate agent.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every hit is an environment coupling: it works in the org it was written in and breaks everywhere else.

    line 123
  2. Very specific:

    Write it to .vibeforce/reports/debt-<ISO>.md so the next audit can diff against it.

    line 220

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Salesforce Technical Debt Audit

When to use

SituationUse this skill
Taking over an org or codebase you did not buildYes
Planning a hardening sprint and needing a ranked debt registerYes
Explaining why small changes take days in this orgYes
Reviewing the diff of the change you just madeSkill sf-code-analyzer-quality, agent sf-quality-gate
Security posture of the orgSkill sf-org-security-audit
Deciding how small a specific change should beSkill sf-minimal-change
A specific governor limit being hit at runtimeSkill sf-governor-limits
Deployment failures and release mechanicsSkill sf-deployment-strategies

An audit is read-only. It runs against source plus a connected org, never writes, and is safe to point at production - see Verification for the production posture.

What counts as debt here

Debt is anything that makes the next change more expensive than it should be. Six categories, each with a measurable signal:

CategorySignal you can measureWhy it costsRemediation skill
Version drift<apiVersion> spread across metadata; sourceApiVersion in sfdx-project.jsonBehaviour differs per class; upgrades become archaeologysf-project-structure
Test debtClasses without a test, coverage below the gate, SeeAllData=true, assertion-free testsNo safety net, so every change is manual QAsf-apex-testing, sf-lwc-jest-testing
Automation sprawlTriggers per object, Workflow Rules and Process Builder still active beside FlowsOrder of execution becomes unknowablesf-flow-automation, sf-process-builder-migration, sf-apex-development
Structural debtHardcoded ids and URLs, profiles instead of permission sets, no layering, duplicated selectorsEvery environment needs manual fixessf-minimal-change, sf-fflib-foundations
Dead weightFields, objects, classes and permission sets nothing references; Apex volume against the org limitSearch noise, deploy time, limit headroomsf-minimal-change
Delivery debtUntracked manual org changes, no package boundaries, no CI gateReleases are irreproduciblesf-deployment-strategies, sf-packaging-release

Core patterns

1. Fix the scope before measuring anything

sf org display --target-org vf-int
sf org list limits --target-org vf-int
sf org list metadata-types --target-org vf-int --output-file .vibeforce/reports/metadata-types.json

Record: org alias and type, API version, the package directories from sfdx-project.json, and whether the source you hold is the whole org or one package. An audit of a partial source tree that claims org-wide coverage is worse than no audit.

2. API version drift, measured from source

# every declared apiVersion, most common first
grep -rho "<apiVersion>[0-9.]*</apiVersion>" force-app | sort | uniq -c | sort -rn

# the files furthest behind the project baseline
grep -rl "<apiVersion>4[0-9]\." force-app --include="*-meta.xml" | head -50

The baseline is sourceApiVersion in sfdx-project.json and apiVersion in config/vibe-force.defaults.json (67.0). The post-edit hook already flags a mismatch on files you touch (scripts/lib/xml-lint.js); the audit is the org-wide version of the same question.

Org-side equivalent, when the source is not the whole truth:

sf data query --use-tooling-api --target-org vf-int \
  --query "SELECT Name, ApiVersion FROM ApexClass WHERE NamespacePrefix = null ORDER BY ApiVersion"

[unverified: the ApexClass.ApiVersion field name was not confirmed against the Tooling API reference in this session - run sf sobject describe --sobject ApexClass --use-tooling-api first]

3. Test debt, in the two places it hides

# structural: which classes and bundles have no test at all
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" pairing --json

# behavioural: coverage per class, from a real run
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" apex --target-org vf-int --json

# tests that read org data instead of creating it
grep -rn "SeeAllData\s*=\s*true" force-app --include="*.cls"

# tests with no assertion at all - one file per iteration, so an empty list is simply an empty loop
grep -rli "@isTest" force-app --include="*.cls" | while read -r f; do
  grep -q "Assert\.\|System.assert" "$f" || echo "no assertion: $f"
done

Coverage above the gate with assertion-free tests is not coverage; report both numbers.

4. Automation sprawl per object

# triggers per object, from source
grep -rho "trigger [A-Za-z0-9_]* on [A-Za-z0-9_]*" force-app --include="*.trigger" \
  | awk '{print $4}' | sort | uniq -c | sort -rn

# what still runs beside your Flows
sf data query --use-tooling-api --target-org vf-int \
  --query "SELECT MasterLabel, ProcessType, TriggerType, Status FROM FlowDefinitionView WHERE Status = 'Active'"

Two triggers on one object is a finding on its own: the platform does not define their order. Workflow Rules and Process Builder lost support on 31 December 2025 - an active one beside a record-triggered Flow on the same object is the single most common source of "it depends" bugs (skills sf-flow-automation for workflow rules, sf-process-builder-migration for processes).

5. Hardcoded ids, endpoints and environment assumptions

# 15 and 18 character ids in code
grep -rnE "'[a-zA-Z0-9]{15}'|'[a-zA-Z0-9]{18}'" force-app --include="*.cls" --include="*.js"

# endpoints that should be Named Credentials
grep -rnE "https?://" force-app --include="*.cls" | grep -v "schema.org\|w3.org"

Every hit is an environment coupling: it works in the org it was written in and breaks everywhere else. Fixes are Custom Metadata, Custom Settings, Schema.SObjectType describes, and Named Credentials (skill sf-integration-patterns). Turn the recurring ones into a Code Analyzer regex rule so the audit does not have to find them twice (skill sf-code-analyzer-quality, pattern 5).

6. Permission debt

ls force-app/main/default/profiles/ 2>/dev/null | wc -l
ls force-app/main/default/permissionsets/ 2>/dev/null | wc -l

sf data query --target-org vf-int \
  --query "SELECT Assignee.Username FROM PermissionSetAssignment WHERE PermissionSet.PermissionsModifyAllData = true"

Profiles carrying feature access instead of permission sets is debt: profiles cannot be composed, and a profile diff is unreviewable. The deep version of this audit - object matrices, dangerous permissions, guest access - is skill sf-org-security-audit.

7. Dead weight and limit headroom

# Apex volume against the org limit
find force-app -name "*.cls" -not -name "*Test.cls" -exec cat {} + | wc -c

# classes nothing references - skip tests and every entry point the platform calls for you
for f in force-app/main/default/classes/*.cls; do
  n=$(basename "$f" .cls)
  grep -qiE "@isTest|@AuraEnabled|@RestResource|@InvocableMethod|webService|implements +(Schedulable|Batchable|Queueable|Database\.)" "$f" && continue
  refs=$(grep -rl "\b$n\b" force-app --include="*.cls" --include="*.js" --include="*.xml" | grep -cv "/$n\.")
  [ "$refs" -eq 0 ] && echo "unreferenced: $n"
done

# custom fields on an object, to compare against what the UI and code actually use
sf data query --use-tooling-api --target-org vf-int \
  --query "SELECT QualifiedApiName, DataType FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = 'Account'"

Limits that matter for this category: 1 million characters per class or trigger, 6 MB of Apex per org (10 MB in scratch orgs, raisable by support case), 7,500 code units per deployment - all from skill sf-governor-limits. Report headroom as a percentage, not a byte count.

8. Delivery debt

sf project deploy preview --target-org vf-int          # what differs between source and org
sf project retrieve preview --target-org vf-int        # what was changed in the org by hand

Anything that shows up only in retrieve preview was changed in Setup and exists in no branch. Count those components: they are the debt that makes every deployment a negotiation.

9. Rank, do not list

An audit that returns 300 findings changes nothing. Rank each finding on two axes and report the top of the list:

ImpactMeaning
3Blocks or breaks releases, or risks data loss
2Slows every change in one area
1Local annoyance
EffortMeaning
1One PR, no behaviour change
2One sprint, needs tests first
3Needs a migration plan and a rollback path

Order by impact descending, then effort ascending. The first five items are the report; the rest is an appendix.

10. The debt register

## Debt register - <org alias> - <ISO date>

| # | Category | Finding | Evidence | Impact | Effort | Owner | Remediation skill |
| --- | --- | --- | --- | --- | --- | --- | --- |

## Measured baseline
| Metric | Value | Gate or limit |
| --- | --- | --- |
| Apex classes / triggers | | |
| Org Apex volume | | 6 MB |
| Apex coverage (org) | | gates.apexOrgCoverageMin |
| Classes with no test | | 0 |
| LWC bundles with no spec | | 0 |
| Active Workflow Rules / Process Builders | | 0 for new automation |
| Objects with more than one trigger | | 0 |
| Profiles / permission sets in source | | |
| Components changed in the org only | | 0 |

## Not measured
<what the audit could not see, and why>

Write it to .vibeforce/reports/debt-<ISO>.md so the next audit can diff against it. A debt register without a baseline cannot show progress, and progress is the only reason to run it twice.

Anti-patterns

Anti-patternConsequenceFix
Auditing source and calling it an org auditOrg-only changes stay invisibleRun sf project retrieve preview and say what you could not see
Counting findings instead of ranking themThe report is ignoredImpact x effort, top five first
"Refactor everything to fflib" as a findingRewrite proposal, not a debt itemName the cost of the current shape in changes per week
Treating coverage percentage as test qualityAssertion-free tests pass the gateReport coverage and assertion density together
Deleting a field because nothing in source references itReports, list views and Flows reference fields tooCheck FieldDefinition, reports and Flow metadata before proposing deletion
Running the audit against production with a mutating checkGuard trips, or worse, it does notOnly the read-only checks; see Verification
Filing debt with no ownerNothing movesEvery register row names a person and a remediation skill

Verification

# the local gates, on the whole tree rather than the diff
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" static --json
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" pairing --json

# org-side, read-only
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" apex --target-org vf-int --json
sf org list limits --target-org vf-int
sf project deploy preview --target-org vf-int

Production posture: the queries and previews in this skill are read-only and legitimate against a production alias, and vf-check prints a production banner rather than refusing for the read-only family. vf-check apex runs tests in the org and is not part of a production audit - take its numbers from the last release pipeline instead. Never set VF_ALLOW_PROD=1 to force a mutating check through as part of an audit.

Exit codes are the runner contract: 0 pass, 1 gate failed, 2 misconfiguration or missing tool, 3 org or network error.

References

  • references/debt-signals.md - the full signal catalogue: detection command, threshold, remediation and owning skill per signal.
  • Related skills: sf-org-security-audit (security posture), sf-code-analyzer-quality (static analysis and custom rules), sf-minimal-change (how small a change should be), sf-governor-limits (the limit tables cited here), sf-flow-automation (automation consolidation), sf-apex-testing and sf-lwc-jest-testing (test debt), sf-deployment-strategies and sf-packaging-release (delivery debt), sf-project-structure (API version baseline).
  • Official: Salesforce CLI Command Reference, org list metadata, Apex Governor Limits, Migrate to Flow.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.