version-branches
Skill0 starsChanged 3 months agoMITView on GitHub
This repo ships TWO branches — main for Frappe v16, version-15 for Frappe v15. Use whenever you fix a bug, merge a PR, cut a release, touch pyproject.toml or install.py, or are asked whether a change needs porting. Read BEFORE writing the fix, not after.
In this repository6 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/agents.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- dev-benchSkill
- releaseSkill
- semantic-commitSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 4 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Distinctive: line 33
When unsure, port it. A needless port is cheap.
- Distinctive: line 52
If the cherry-pick conflicts, resolve it toward *what v15 can actually do*, not toward what main says.
- Distinctive: line 114
Before using any framework API that feels new, check it against v15:
- Distinctive: line 147
Never resolve a merge conflict there by taking main's copy.
- Very specific: line 149
Never release
version-15with ports outstanding: Run the drift check first, below.
≈ “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.
Files
Two branches: main (v16) and version-15 (v15)
If you fix something on main, it is not done until it is on version-15 too.
That sentence is the whole skill. Everything below is how.
| Branch | Frappe | Python | Releases as |
main | v16 | 3.14 | v0.X.Y |
version-15 | v15 | 3.11 | v0.X.Y-v15 |
This was a deliberate choice (Malik, 2026-08-16) over a single branch serving both. The known cost is drift: the two copies grow apart unless every fix is carried across. Fighting that drift is your job in every session.
The rule
A change on main falls into exactly one of three buckets. Decide which
before you write it, and say which in the PR:
- Port it. Bug fixes, security fixes, docs corrections, test fixes. Anything a v15 user would also suffer. This is the default.
- Do not port it. A feature that depends on something v15 does not have. Say why in the commit or PR body so nobody re-litigates it later.
- Port it differently. The bug exists on both but the fix cannot be identical, because v15 lacks the API. Write it twice, deliberately.
When unsure, port it. A needless port is cheap; a missed security fix is not.
Porting a change
git checkout version-15
git cherry-pick <sha> # or -n to inspect before committingThen, before you commit:
python3.11 -m compileall -q -x '(tests|__pycache__)' frappe_agentsThat compile is not optional. It is the cheapest possible check that you have not dragged v16-only syntax onto the v15 branch, and it takes a second. Silence means it passed.
If the cherry-pick conflicts, resolve it toward what v15 can actually do, not toward what main says.
What version-15 may not contain
Syntax
version-15 targets Python 3.11. Two things main uses freely are syntax
errors there:
| Do not write | Write instead |
type X = int | str (PEP 695) | X: TypeAlias = int | str |
type X = ... that references itself | TypeAliasType("X", ...) — see below |
except A, B: (PEP 758) | except (A, B): |
The recursive case is a trap, and it bites silently. frappe_agents/harness/
types.py defines JSONValue in terms of itself. Rewriting that one as a plain
TypeAlias still compiles — and then pydantic expands it eagerly and recurses
until the interpreter dies, at import time. Use the lazy named alias instead:
from typing_extensions import TypeAliasType
JSONValue = TypeAliasType("JSONValue", Union[JSONPrimitive, list["JSONValue"], dict[str, "JSONValue"]])typing_extensions is a direct frappe dependency on both v15 and v16, so this
adds nothing to the bench. Use Union[...] rather than | there: the recursive
arm has to stay resolvable by name when pydantic builds the schema.
Compiling is not enough to catch this. If you touch the harness types, import
them under 3.11 with pydantic present and build a TypeAdapter before you push.
ruff on that branch is set to target-version = "py311" and will catch these,
but the compile above catches them faster.
ruff format on main will undo your fix. main targets py314, and its
formatter rewrites an inline except (A, B): into PEP 758 except A, B: — which
version-15 cannot parse. Do not fight it file by file: name the tuple once and
catch the name.
SIGNATURE_ERRORS = (TypeError, ValueError)
...
except SIGNATURE_ERRORS:A name is not a tuple literal, so neither branch's formatter touches it and the same source works on both.
The vendored harness in frappe_agents/harness/ is the usual offender —
upstream writes for modern Python, so every re-vendor reintroduces PEP 695.
Convert it again each time.
Framework APIs missing in v15
| API | Status on version-15 |
Workspace Sidebar doctype | Does not exist. Guard on frappe.db.exists("DocType", "Workspace Sidebar") before creating or renaming one. |
meta.get_masked_fields() | Does not exist. Already guarded everywhere — do not add an unguarded call. |
Before using any framework API that feels new, check it against v15:
curl -s -o /dev/null -w "%{http_code}\n" \
https://raw.githubusercontent.com/frappe/frappe/version-15/frappe/<path>404 means v15 does not have it, and your change needs bucket 2 or 3.
The masking gap is a security difference, not a cosmetic one
v16 feeds get_masked_fields() into two protections: what gets [redacted] in
an audit row, and the extraction gate's master-value comparison. On v15 that
source is empty, so a field an admin masked is not automatically protected.
docs/admin.md says so on the v15 branch. If you change anything in that area,
keep that note true.
Releasing
Two streams, released separately.
main→ normalreleaseskill, normal version numbers.version-15→ same flow. Its own.releasercnamesversion-15as the release branch and setstagFormattov${version}-v15, so the two streams never collide.
The first v15 release needs a dry run. No v*-v15 tag exists yet, so
semantic-release has no previous release to count from and will pick a version
off the whole history. Dry-run it exactly as the release skill describes and
read the version it proposes before dispatching anything.
.releaserc is one of the files that legitimately differs between the branches.
Never resolve a merge conflict there by taking main's copy.
Never release version-15 with ports outstanding. Run the drift check
first, below.
Checking for drift
CI runs this nightly and opens an issue when the two diverge, but run it by hand any time you are about to release or are asked "are we in sync?":
git fetch origin
git log --oneline origin/version-15..origin/main -- \
frappe_agents/ pyproject.tomlEmpty output means everything is carried across. Anything listed is a change on
main that has not been ported — triage each into one of the three buckets.
Docs and workflow files are excluded from that command on purpose: they diverge legitimately.
When someone asks "can we support v14 too?"
The answer on record is no — v14 is end-of-life under Frappe's last-two-majors policy. Do not add a third branch without Malik saying so explicitly.
Last verified: 2026-08-16 (branch created, CI green on both).
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
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.

