jfastframework
Docs Español PyPI GitHub

Working with AI agents

The premise of this framework is that an agent writing code in your repository is now normal, and that the thing which makes it survivable is not a better prompt — it is rules the agent cannot quietly break.

Three surfaces, in increasing order of how much they help:

What it gives an agent
jfast ai context --json everything about this project, in one call
jfast next what is still unfinished, in the order it can be done
contracts.toml what it may and may not do, checked by CI
AGENTS.md + .jfast/skills/ how this project expects work to be done

The generated agent surface

jfast new service billing --agent-docs
jfast init                              # asks

Writes:

AGENTS.md                                   the rules, at the root where agents look
.jfast/skills/respect-contracts/SKILL.md    read the contract before writing
.jfast/skills/design-system/SKILL.md        only when there is a frontend

Off by default. A project nobody points an agent at owes no agent files, and every file shipped is a file that can drift from the code it describes.

Why .jfast/skills/ and not one big file

Because the value is in not loading all of it. A skill declares what it is for and when to skip it:

---
name: respect-contracts
description: Read this project's contract before writing code in it, and verify
  the code against it before calling the work done.
when_to_use: Always, before any code change in billing.
when_not_to_use: Answering a question about the project without changing it.
---

An agent reads the front matter of each, picks the one that matches the task, and loads only that. One monolithic file spends the context budget on rules that do not apply to the change in hand — and duplicates AGENTS.md, which then drifts out of sync with it.

What each file is for

AGENTS.md — the rules that apply to every change: the service-level shape, the five checked rules, what not to do, and the commands. Short on purpose.

It says nothing about the files inside a module, and that is deliberate. It is written at service-scaffold time, before any module exists, and one service may hold modules in all four layouts. Naming router.py there was a promise that came true in one of the four. What it does instead is name the two things that are written alongside the code and are therefore always true: [modules.<name>] in jfast.toml for the layout, and modules/<name>/README.md for that layout's file map.

A skill — the procedure for one kind of task: preconditions, steps with the exact commands, how to verify, and the mistakes people actually make.


The rules an agent cannot drift past

These are not advice. jfast contracts check fails the build:

modules/payment/<file>:41: cross-module: module 'payment' imports modules.invoice.service; import modules.invoice.public instead
  (only public.py is another module's API -- call a function in modules/invoice/public.py that returns DTOs (add one if it is missing), and add "invoice" to depends_on under [modules.payment] in contracts.toml)

file:line, the rule, what happened, and what to do about it. That second line matters more than it looks: an agent given a violation with no remedy tends to satisfy the checker rather than fix the design — deleting the import, inlining a copy, or turning the rule off. Naming the destination removes the ambiguity.

The full list is in Contracts. The ones that most often catch generated code:

Rule Why an agent trips it
The HTTP layer may not import sqlalchemy Querying from the handler is the shortest path to a working endpoint
A module imports another only through its public.py, declared in depends_on Reusing the neighbour's model is easier than asking its facade
No raw SQL against another module's tables A text("SELECT ...") looks like it avoids the import rule
shared/ may not import a module Fixing the above by importing backwards
No blocking calls in async def time.sleep and requests are what most examples use

Generate the module you mean, then build on it

An agent handed jfast new module invoice used to spend its first edits deleting the example -- name, description, is_active and their tests -- and the diff a reviewer read was mostly that. Give the generator the fields instead, and the first draft has nothing to delete:

jfast new module invoice --fields "customer_id:int, number:str(20), total:money, paid:bool=false" \
  --unique "number"

What it writes already passes ruff check ., ruff format --check ., mypy . (strict) and pytest, in every layout, so a red gate after an agent's edit is the agent's edit. The routes are guarded as the service is configured (current_tenant with tenancy, require_auth with auth), and the module has a tasks.py for its background work. The grammar is in Modules; --bare is the structure with no fields at all.


Waive a line, do not delete the rule

from modules.invoice.enums import Status  # contracts: allow migrating to shared

The waiver is on one line, with a reason, and jfast contracts waivers lists every one. Turning the rule off in contracts.toml removes it for everybody, silently, and the next violation goes unreported — which is how a contract stops meaning anything.


One call before the first edit

jfast ai context --json

Everything about this project in one answer: every module and how it is shaped, what main.py actually imports, the module import graph, the contract, what analyze and contracts check say right now, what is still unfinished, and the commands that return whatever was left out.

Under environment it lists the settings in jfast.toml that the environment owns, which of them the current environment overrides (masked), and the rule an agent must follow: those file values are only defaults, and a disagreement is reported to a human, never "fixed" on either side (Which wins).

It is a composition, not a reimplementation — inspect, analyze, graph, contracts and next in one payload, so it cannot disagree with the command it tells you to run. It never imports the project, so it still answers on a service whose dependencies are missing or whose code does not parse.

How big is it

There is no one number, and a single figure here was wrong for every project that was not the one it was measured on. The payload is facts about your service, so it scales with your service. Measured on generated services — jfast new service shop --with database, then jfast new module N times (0.1.0a12):

modules --json --json --brief
1 10,295 3,750
3 12,353 5,340
5 14,429 6,944

A generated service is the floor, because nothing is wrong with it yet. The same five-module service after some work in it — two modules main.py never picked up, a file outside any module, two contract violations — measures about 3.3 KB more than the floor above in full, and 0.8 KB more with --brief (measured on 0.1.0a9, when the floors were 11.5 KB and 5.4 KB). Roughly +1,000 bytes per module (0.1.0a11 added each module's facade, events and tasks), and the rest is next and checks growing with what is actually outstanding.

Where the full payload goes on that five-module service, in bytes:

next      3,052   contract  2,300   commands 1,576   checks  1,562
modules   1,499   omitted     921   project    135   plugins   130

0.1.0a12 added the environment section: about 600 bytes on a generated service, more for each setting the environment owns that jfast.toml writes.

Token figures are bytes ÷ 4 — the standard rough estimate, not a tokenizer.

jfast ai context --size is the answer for your project, and it is the command to run before you decide whether to call the other one in a loop. The table above is a scale, not a budget: do not plan a context window against it.

What it deliberately leaves out

The obvious implementation of "everything a model needs" concatenates docs/. That is wrong twice over:

So the payload carries facts about the project, and names every gap under omitted with the command that closes it:

Left out How to get it
The documentation https://jfabrizzio5.github.io/JFastFramework/latest/
File contents open the files it names — it never quotes source
Per-module file listings jfast ai context --module <name>
Resolved settings, live plugin graph, route table jfast describe --json (imports the app)
What each unapplied revision does to a database with rows jfast migration check --json, jfast migration plan
Findings or violations past the first 20 jfast analyze --json, jfast contracts check --json
History git log

Naming the gaps matters more than it looks. An agent handed a partial answer with no seam in it treats it as a complete one, and then writes code against a file it was never shown.

Narrowing

jfast ai context --json --module invoice   # one module, with its file list
jfast ai context --json --brief            # the shape, without the detail
jfast ai context --size                    # what the above two cost

--module narrows the modules, findings and violations. It does not narrow next, on purpose: you asked about one module, and the step that is blocking you may be in another one.

--brief drops the contract's rules (keeping its scope, layer names and invariants), the finding and violation lists (keeping the counts), shared/, and the prose in commands and omitted.

The commands it points at

Each entry in commands is a machine-readable entry point, with what it returns and what reading it saves you:

jfast inspect --json           # modules, layouts, routes, wiring, tables
jfast analyze --json           # structural findings, each with a remedy
jfast graph --format json      # module-to-module import edges
jfast contracts show --json    # layers, forbidden calls, interfaces, invariants
jfast contracts check --json   # violations, with file, line and rule
jfast next --json              # what is unfinished, in order
jfast describe --json          # resolved settings and plugin graph

Every one of them except describe reads the filesystem without importing the project.


jfast next — what is unfinished

The same engine as jfast analyze, turned around. analyze says what is wrong; next says what to do about it, in the order it can be done:

  next  shop

   1. module 'ghost' declares routes but main.py never imports it
      └─ edit main.py between the [jfast:imports] and [jfast:routers] markers
   2. 'helpers.py' belongs to no module                      move it into a module, or into shared/
   3. ghosts has no revision (this project has none)         alembic revision --autogenerate
   4. invoices has no revision (this project has none)       alembic revision --autogenerate
   5. contracts check fails (2 violations)                   jfast contracts check
   6. modules/order has no tests                             add modules/order/tests/
   7. contracts.toml still has its generated placeholders    edit contracts.toml
   8. modules/payment has no README                          add modules/payment/README.md

  wire -> shape -> persist -> verify -> cover -> document   (dependency order, not severity)

This is the answer to "the agent skipped a step".

The ordering is the point

Steps are sorted by stage, and a stage is a precondition of the ones after it — never by how bad the finding is:

Stage Nothing after it is worth doing until
boot the service starts at all — an enabled plugin nothing provides stops it
scaffold there is a module to wire, migrate or test
wire main.py imports it: an unwired module's tests pass while its routes 404
shape the code has stopped moving between files
persist the tables the shape settled on actually exist
verify the contract has been checked against where the files ended up
cover the code under the test is wired, placed and backed by a table
document — last: it describes what the stages above settled

Severity would give a different and worse order. In the listing above, 'helpers.py' belongs to no module is low and ghosts has no revision is medium, yet the loose file goes first: moving it changes which tables the project declares, so a revision generated before the move is one you regenerate after it. Testing an unwired module is the same mistake in a louder form — the test passes, and the endpoint 404s.

jfast next --json carries the stage and its numeric rank on every step, plus a stages block spelling out what each one is a precondition of.

One fact, one step

A contract written for another layout is reported by two commands at once: analyze files one contract-governs-nothing per empty layer, and contracts check files one layer-unmatched for each of the same layers. Four empty layers therefore arrived as four steps plus a contracts check fails (4 violations) summary of those same four — five lines about one file, filed under shape as if something had to move, each carrying jfast analyze # contract-governs-nothing, which re-prints what you are already looking at.

It is one step now, in verify, and it names the command that clears it:

   2. contracts.toml governs nothing: 4 layers match no file here
      └─ jfast contracts init --layout hexagonal --force

The layout comes from what jfast.toml records for the modules. If they are not all in one layout the slot stays <layout>, because --force overwrites the contract and a guess is not worth attaching to that. And a violation that is not layer-unmatched still gets its own contracts check fails (n) step — dropping the duplicate must not drop the rest.

When there is nothing left

  ✓ shop: nothing outstanding

  3 modules, every one registered, tested and documented.
  2 revisions cover every declared table.
  contracts.toml passes, and its placeholders are filled in.

Stated as the list of what was checked rather than as praise, because a tool that always finds something to do trains people to ignore it, and one that says "all good" without saying what it looked at is no better.

jfast next always exits 0. It is a question, not a gate — jfast analyze --fail-on high is the gate.

On a freshly generated service

It is not silent, and everything it says is true. jfast new service writes three models with __tablename__ and no revisions, and a contracts.toml whose owns and does_not_own are still TODO:

   1. invoices has no revision (this project has none)   alembic revision --autogenerate
   2. orders has no revision (this project has none)     alembic revision --autogenerate
   3. payments has no revision (this project has none)   alembic revision --autogenerate
   4. contracts.toml still has its generated placeholders: owns, does_not_own, invariants
      └─ edit contracts.toml

It does not claim the modules are unwired or untested — the generator wired and tested them, and saying otherwise is the failure that makes people stop reading the output.

This is also the one place next reports something analyze will not. analyze stays silent about migrations when a project has none at all, so a brand-new service is not greeted with a finding. next exists to name the step after the one you just took, and on that project the step is the first revision.


What this does not solve

An agent that follows every rule can still build the wrong thing. Contracts constrain structure, not intent: nothing here notices that the feature was not what you asked for, that the test asserts the bug, or that a rule you wrote in January is wrong in June.

What it buys is narrower and worth having anyway: the codebase does not decay while you are not looking, and a review can be about whether the feature is right rather than about where the file went.


The framework's own agent surface

This repository practises it: AGENTS.md at the root and seven skills under .jfast/skills/, covering module creation, plugin authoring, contracts, the frontend and the design system. Skills for agents covers writing one.