# Chapter 5 · Feature stories

Machine edition of https://agentc.consulting/agent-enhanced-development/chapter-5/feature-stories
Part of Agent-Enhanced Development: https://agentc.consulting/agent-enhanced-development.md
Source conventions: https://github.com/AgentC-Consulting/aed-conventions/blob/main/04_feature_stories.md

## The moment

You know exactly what you want your product to do, and you can say it in one
sentence: "add late fees to overdue invoices and let the customer know." You
hand that sentence to your agent, and what comes back is `LateFeeService` with a
method called `process`, the plan buried inside it, and half the names invented
on the way. Then the week goes to the rename pass.

Nothing about your sentence was wrong. It was just not written in a form an
agent could build from. This chapter turns it into a story your agent can read,
and then into the skeleton it can build. Three fields, three stages, and you
write no code.

## The engine: three fields, three stages

Chapter 3 took one finished file and read it four ways, backwards. This chapter
runs the other direction: three fields go in, and three stages later the same
file exists. Put your own product in the fields, or leave the billing defaults
where they are. On the page a stage button shows any of the three at once, and
on a wide screen the stages also advance as you scroll the section.

1. **The story.** The three fields snap into the persona sentence the
   conventions state, each field highlighted where it lands. This is the owner
   speaking.
2. **The When-statement.** The same story restated as "When … is provided,
   then …", and then expanded until no jargon is left. The **when** clause is
   marked as the `initialize` parameters; the **then** clause is marked as the
   methods. This is the plan speaking.
3. **The skeleton.** A real Crystal file, named end to end, every body still
   empty. This is the agent speaking.

**Stage 1. The story.** Persona `Admin`, verb `perform`, subject `Invoice`:

> As an **Admin** user, I want to _perform_ a late fee charge on an **Invoice**
> that is past its grace period **and** _notify_ the **Customer** it belongs to.

`perform` is the flag. It is the one verb that is not RESTful, so what gets
built is a process manager rather than a controller action. And the word "its"
was hiding a relationship: the invoice **belongs to** the customer. Neither of
those was a decision the agent had to invent. Both were already in the
sentence.

**Stage 2. The When-statement.** First plainly:

> When an Invoice and the number of days it is past due are provided, then stop
> unless the invoice is overdue, charge the late fee, and tell the customer.

Then expanded, until every piece of jargon is spelled out in models,
attributes, and operations:

> When an Invoice record and an Integer representing how many days past its due
> date that invoice is today are provided, then compare that number against the
> company's grace period in days and do nothing if it is smaller, otherwise
> charge the invoice a fee equal to the late fee rate times the amount owed, and
> email the customer the invoice belongs to a notice of the fee.

The **when** clause is now the `initialize` parameters. The **then** clause is
`perform`, in order. That expanded sentence is nearly the pseudocode, and that
is the point.

**Stage 3. The skeleton**, in the file `billing/apply_late_fee.cr`:

```crystal
# File found under `billing/apply_late_fee`
# Applies a late fee to the invoice once it has gone past its grace period, then tells the customer.
class Billing::ApplyLateFee
  # -- What this needs --

  # The invoice being charged. It knows its customer, its amount, and its due date.
  property invoice : Invoice

  # How many days past the due date the invoice is today.
  property how_many_days_late : Int32 = 0

  # -- Company policy --

  # Days past due before any fee applies.
  property grace_period_in_days : Int32 = 30

  # The fee, as a share of the amount owed.
  property late_fee_rate : Float64 = 0.05

  # -- What we learned along the way --

  # True once the notice has gone out.
  property has_the_customer_been_told : Bool = false

  def initialize(@invoice, @how_many_days_late)
  end

  # -- How it runs --

  def perform
    stop_unless_the_invoice_is_overdue
    charge_the_late_fee
    tell_the_customer
  end

  # Nothing happens inside the grace period.
  private def stop_unless_the_invoice_is_overdue
    # Your business logic goes here
  end

  # The fee is a share of the amount owed, charged to the same invoice.
  private def charge_the_late_fee
    # Your business logic goes here
  end

  # The customer gets the notice by email.
  private def tell_the_customer
    # Your business logic goes here
  end
end
```

That is the file chapter 3 read four ways, arrived at from a sentence instead of
from a refactor. The bodies are still empty, and nothing in the names needs to
change when they fill.

## The three fields

**Persona: who is asking.** A persona is a specialized alias of a user model
type that carries an authorization level with it. When you name one, you have
also said that this feature needs a view and a controller action at a minimum,
and which authorization surface the route belongs to. Leave it out and the story
is a process and nothing more. The default in the field is `Admin`.

**What you want to do: the verb.** Either an HTTP verb followed by the primary
data model, in which case framework CRUD does the work, or `perform`, the one
non-RESTful verb, the one you use for your own business logic. The default in
the field is `perform`. Choose a RESTful verb in the field and the story says
so: framework CRUD builds the controller action and the view, and you write no
process manager. The two stages after the story stay on the `perform` branch,
which is where your own business logic goes.

**The thing it happens to: the subject and its relations.** The primary data
model, plus any model the action needs a relationship with. Whether you say "a"
or "multiple" is what decides the relationship, in the same Active Record
expressions Rails uses. The default in the field is `Invoice`, related to
`Customer`.

One honest note about the source. Chapter 04 of the conventions repository
states the story's anatomy and then marks its own breakdown of that anatomy
`TBD`. The section is genuinely unwritten, and the release marks it rather than
papering over it. The three stages above follow the AED planning skill, which
does spell the breakdown out and carries the same worked example.

## The rules this chapter settles

- A feature story is a user story you keep with your code, not in a tracker: "instead of managing the details of the story from a 3rd party tool such as Jira, we are going to manage the details directly with our AI agent that works from our code base."
- The story has a fixed anatomy: `As a (specify persona), I want to (RESTful verb, or "perform") ("a" or "multiple") (data model name of an existing data model) and (AR relationship name/type or "perform") (data model name or Process Manager name if performing a process)`.
- A persona means a view and a controller action at a minimum: "When there is a persona we have chosen, we also know the feature request requires a view and a controller action at a minimum." A story with no persona does not.
- The action verb is an HTTP verb followed by the primary data model the action affects: `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`.
- `perform` is the one non-RESTful verb, and it is where your own business logic goes: "A unique verb of 'perform' can be used to trigger a workflow that is not RESTful. This is the keyword you primarily use for our unique business logic, aka your special sauce." A `perform` story means you are writing a process manager, not a controller action.
- The story names every related model, and plurality decides the relationship: "The action description should also include references to any data models that require a relationship", and "The plurality of the data models dictates the relationships between them, and should use the same Active Record pattern expressions as in Rails."
- Company jargon is a flag, not a name. "Assign" aliases a `belongs_to`. If a word in the story is company jargon, define it in the plan before deriving names from it; ask the person who said it if you cannot.
- Every process is written as a When-statement: "Processes start with a 'when' keyword, always. Because a process is 'when' something happens!" The **when** clause names the qualifying information the process needs before it can run, and those become the `initialize` parameters. The **then** clause names the operations, and those become the methods called by `perform`.
- Expand the When-statement until no jargon is left. "That expanded sentence is nearly the pseudocode. That is the point."
- Read the story as a grammar exercise. Nouns become data models and attributes; verbs become process managers and methods.
- "Pseudocode and plans use release names. Never placeholders." And the reason: "Placeholder names in a plan are not neutral — they are instructions."
- Check every name while it still exists in exactly one place, the plan, and fix every warning then, before any code is written. Note per file where each class lives: the file is the lower snake_case of its primary class, and a namespaced class lives in a folder named for the namespace.

## Why it matters to the agent

This is chapter 3's argument moved one step earlier. There, the point was that a
name carries its meaning inside every token window that contains it. Here, the
window in question is the one the agent reads before any code exists at all: the
plan. The model writing the implementation reads the plan and follows it, so
`data`, `options`, `process`, and `TODO: helper here` are not gaps waiting to be
filled in later. They are instructions, and they ship.

A story that carries its own nouns, verbs, and qualifying information puts every
name into that window while the names still live in one place. Nothing has to be
invented on the way down, which is why the rename pass at the end of the feature
shrinks toward zero. There is no "we will clean up the names before merge" step,
because there is nothing left to clean.

The conventions repository publishes two measurements next to this claim, and
both are narrower than the claim. In a comprehension benchmark run on 2026-07-07, Claude Haiku
answered intent, modification, and defect probes on ten pairs of Crystal
snippets, blind, with a blind grader: AED style scored 60 of 60 and conventional
style 54 of 60, with the gap concentrated in defect-finding and intent. The
report calls that "a directional signal consistent with the hypothesis, not
proof of it." In the pet-tracker build-off of 2026-08-11, the same task was given
to headless Haiku twice in a codebase before AED adoption and twice in the same
codebase after: of five working user journeys, the before runs delivered none
and none, the after runs two and four. The build-off's own README notes the arms
differ by the whole AED bundle, not by names alone. Neither result is about
feature stories in particular. Both are about what a model does with code whose
names carry intent, and the story is how those names get there first.

### Sources for this section

The conventions themselves are chapter 04 of the conventions repository:
https://github.com/AgentC-Consulting/aed-conventions/blob/main/04_feature_stories.md
The two measurements are the comprehension benchmark of 2026-07-07 and the
pet-tracker build-off of 2026-08-11.

## Before and after

The same feature, planned two ways: once with placeholder names, once with
names derived from the story. The request was identical;
what changed is whether the plan handed the agent names or handed it
placeholders.

```crystal
# before -- a plan whose names are placeholders, and therefore instructions
class LateFeeService
  def initialize(invoices, options)
    @invoices = invoices
    @options = options
  end

  def process
    data = fetch_data
    handle_invoices(data)
    notify(data)
  end
end

# after -- the same feature, derived from the story and the When-statement
class Billing::ApplyLateFee
  property invoice : Invoice
  property how_many_days_late : Int32 = 0
  property grace_period_in_days : Int32 = 30
  property late_fee_rate : Float64 = 0.05
  property has_the_customer_been_told : Bool = false

  def initialize(@invoice, @how_many_days_late)
  end

  def perform
    stop_unless_the_invoice_is_overdue
    charge_the_late_fee
    tell_the_customer
  end
end
```

Nothing in the `before` block says what qualifies an invoice, what the grace
period is, or what the customer is told. Its `data`, `options`, and
`handle_invoices` are the names that will ship.

A plan also states the supporting model changes it implies, in the same pass.
This one does: `customer.cr` gains
`property is_this_an_enterprise_customer : Bool` and renames `subscriptions` to
`list_of_all_active_subscriptions`, because a plan that renames a model
attribute in passing has already decided that rename.

## Check the names before you write the code

Every name above exists in exactly one place right now. That is the cheapest
moment there will ever be to check it, so check it in a batch, before the
skeleton is written:

```bash
ruby ${CLAUDE_PLUGIN_ROOT}/scripts/aed_lint.rb check-name --kind class Billing::ApplyLateFee
ruby ${CLAUDE_PLUGIN_ROOT}/scripts/aed_lint.rb check-name --kind attribute how_many_days_late grace_period_in_days late_fee_rate
ruby ${CLAUDE_PLUGIN_ROOT}/scripts/aed_lint.rb check-name --kind boolean has_the_customer_been_told
ruby ${CLAUDE_PLUGIN_ROOT}/scripts/aed_lint.rb check-name --kind method stop_unless_the_invoice_is_overdue charge_the_late_fee tell_the_customer
ruby ${CLAUDE_PLUGIN_ROOT}/scripts/aed_lint.rb check-name --kind collection list_of_all_active_subscriptions
```

`--kind` is one of `boolean`, `collection`, `attribute`, `class`, or `method`.
Fix every warning now, while the fix is one edit in a plan rather than a
refactor across a branch. Then write the path down next to the class, because
the plan decides that too: `Billing::ApplyLateFee` lives in
`billing/apply_late_fee.cr`.

If you catch yourself about to write a placeholder name "just for the skeleton,"
that is the signal that the When-statement is not finished. Expand it until the
real name is obvious, then write the real name.

Next: Chapter 6 · Edit-level style (the same care, one line at a time, once the
structure is already decided).
