Position: Reiterating — How AI Turns SDLC Into a Loop, Not a Line


AI-assisted developmentSDLCCI/CDdeveloper workflowscode quality
11 min read
Table of Contents
Position: Reiterating — How AI Turns SDLC Into a Loop, Not a Line

A few weeks ago I wrote about Feature MD — a markdown file that sits next to a feature and keeps the AI honest about what it’s actually supposed to be building. The pitch was simple: without something anchoring intent, agents drift. They start solving the problem you described in message three, not the one you meant in message one. A file that captures intent up front, and gets checked against as the work progresses, turned out to be a cheap fix for an expensive problem.

The post held up. What didn’t hold up was the assumption baked into it — that I’d still be the one maintaining the file. That’s fine for one feature on one afternoon. It stops being fine the moment you’re running a few of these in parallel, or the moment the agent’s output gets good enough that you quietly stop opening the diff yourself. And that second part is the part worth designing for now, before it becomes the default rather than the exception.

One question came back from readers more than any other: who’s writing the feature.md, and who’s checking it’s telling the truth?

That’s the right question. Here’s what I’ve built since.

Traditional SDLC vs the reiterating loop

Left: linear pipeline ending at “done.” Right: the same stages with a loop-back when the spec changes — the thesis in one image.

The watcher is watching itself

Try automating feature.md instead of hand-writing it, and you run straight into this: the same agent that made the code changes is also the one summarizing what it did. That’s not a record, it’s a self-report. If the agent misread the task, the misreading shows up identically in the code and in the description of the code. Nothing catches it, because the thing that would normally catch it — a human reading the diff — is the exact step this setup exists to remove.

That doesn’t kill the idea. It just means the changelog can’t be the whole safety mechanism. It’s one half of one.

The self-graded changelog problem

One context, two outputs, and nothing standing between them. The diff and the account of the diff both come from the same pass — which is fine until the pass is wrong.

Automating the file instead of babysitting it

Cursor’s hooks system gives you lifecycle events around the agent loop — a session starting, a file getting edited, the agent stopping. That’s the plumbing feature.md was missing. The config is a single JSON file:

{
  "hooks": {
    "sessionStart": [
      { "command": ".cursor/hooks/feature-journal-session-start.sh", "timeout": 15 }
    ],
    "afterFileEdit": [
      { "command": ".cursor/hooks/feature-journal-after-edit.sh", "timeout": 10 }
    ],
    "stop": [
      { "command": ".cursor/hooks/feature-journal-stop.sh", "timeout": 15, "loop_limit": 3 }
    ]
  }
}

The session-start hook binds a feature slug for this chat, writes it to .cursor/hooks/state/active-feature, and injects the existing journal as read-only context. The after-file-edit hook marks the session dirty so no-op turns are skipped. The stop hook checks the dirty flag, appends a changelog entry to docs/features/<slug>.md, and prevents re-fire with a skip marker.

The stop hook contains the core journaling logic. After checking a dirty flag (set by afterFileEdit) and a skip marker (to prevent re-fire), it prompts the agent to append a structured changelog entry:

feature="$(read_active_feature "$session_id")"

# Skip if no code edits this turn
if [[ ! -f "$(dirty_file "$session_id")" ]]; then
  echo '{}'; exit 0
fi

# Mark skip so the follow-up write does not re-fire
date -u +%Y-%m-%dT%H:%M:%SZ > "$(skip_file "$session_id")"

followup="Append changelog to docs/features/${feature}.md:

### $(date -u +%Y-%m-%d\ %H:%M\ UTC) — <short title>
**Why:** <why this change>
**Decisions:** <decisions taken>
**Files:** <paths changed>
**Logic:** <behavior / approach>"
jq -n --arg msg "$followup" '{followup_message: $msg}'

A rule tells the agent to read the journal before feature work, turning it from passive storage into active guardrails:

When this chat has a bound feature from the feature-journal hook,
attach that journal before doing feature work.

1. Read `.cursor/hooks/state/active-feature` for the slug.
2. Read `docs/features/<slug>.md` with the Read tool.
3. Treat that file as source of truth for goal, prior decisions,
   changelog, and files already touched.

Claude Code and OpenCode have equivalent hooks, and OpenCode’s AGENTS.md serves the same purpose as Cursor rules — so the same pattern works across tools.

Hook lifecycle around feature.md

The dashed line on the right is the whole point — feature.md persists across the gap between sessions instead of getting rebuilt from memory every time.

One addition since the last version: the hook now asks for confirmation before it runs, or creates itself under the name of whoever’s invoking it. Small thing, bigger effect than it looks like it should have. It doesn’t check whether the work is correct — that’s not its job. What it does is make sure nothing runs anonymously. Every automated action has a name on it, which is usually the difference between a team that trusts a system and a team that quietly stops using it the first time something breaks and nobody can say who ran it.

The loop that actually checks

Automating the file solves “who’s writing this down.” It does nothing for “is any of it true.” For that, I ended up with a loop instead of a pipeline:

Read the Confluence doc → understand it → derive acceptance criteria from it → develop against those criteria → cross-validate the output against the criteria → validate the criteria against Confluence again → update Jira.

Steps five and six are where I’d underweighted things the first time around. Deriving acceptance criteria up front gives you something concrete to build against, instead of re-reading a ticket description and hoping the implementation matches its vibe. The real payoff, though, is checking twice: once right after development, to see if the code satisfies the criteria you set, and again at the very end — against the live document, not a snapshot cached at session start. If someone edited Confluence while the agent was working, that surfaces before Jira gets marked done, not after.

That second check is what makes it a loop instead of a straight line. If Confluence changes after a feature’s already gone through this once, nothing restarts from zero. The process re-enters at the top with the updated source and runs through again.

The AI-native SDLC loop

Seven stages, grouped by phase — teal for ingesting the source, purple for planning and building, coral for the two validation checks, gray for the tracker update. The dashed path on the right is the re-entry point.

What this actually buys you

I’m not going to call it fool proof. Closed-loop instead of self-reported is the honest pitch. The old version produced a record. This version checks the output against something explicit before it lets anyone claim the work is done — and checks it a second time against a source that might have moved since the first check ran. That’s a real upgrade, and it’s the part I’d defend in a room full of skeptical engineers.

The attribution piece matters in a quieter way. Remove the requirement that a human touch the code, and it’s tempting to also remove the requirement that a human be accountable for what ran. The hook confirmation step is a small, deliberate refusal to let that happen. It’s not a correctness gate. It’s a “someone signed off on this existing” gate, and keeping those two things distinct — in the system and in how you describe it to a team — matters more than it sounds like it should.

The map that builds itself

There’s a second-order use of this that I didn’t expect when I started, and it’s worth mentioning because it costs almost nothing extra to get.

Every feature.md is currently an island. It’s a great record of one feature, and it’s invisible to every other feature next to it. But two features touching the same module is exactly the kind of information you’d want captured somewhere, and it turns out Obsidian’s linking model captures it for free, provided the notes reference the same thing.

The shift is to stop treating feature.md as the only note type. Alongside a note per feature, you keep a note per component or module — created once, then referenced, never rewritten by hand. A feature note doesn’t describe the payments module; it just lists payments in its frontmatter as something it touched. Obsidian’s backlinks do the rest: open the payments component note and you get a live, chronological list of every feature that’s ever reached into it, generated as a side effect of the loop rather than a separate writing task.

Feature notes linking into an emergent application map

Feature notes cross-link to shared component notes. The component notes are what the map is actually mapping — the feature notes are just the evidence trail.

The same session-stop hook that already writes the changelog can write this too, since it already knows which files and modules a session touched:

---
jira: PROJ-1234
confluence: /pages/checkout-redesign
status: validated
touches: [[Payments]], [[Checkout]]
---

Frontmatter schema: which hook writes which field

The same frontmatter block, annotated by origin — each field is written by a different lifecycle hook or validation step.

No new plumbing, just one more write on an event that already fires. Six months in, the graph view — filtered down to components only, features as an overlay — becomes something close to an architecture diagram nobody had to draw, because it was assembled out of everything the loop was already recording.

It’s not free of the same problem I keep raising, though. If a feature note claims it touched payments and the diff shows it didn’t, that’s now a wrong link in the module’s history instead of a wrong line in a changelog — the self-report risk just moved up a level. And a touches field derived from the agent’s own sense of what it worked on is weaker than one derived straight from the git diff paths, for the same reason the changelog needed an outside check in the first place. Whether this map is trustworthy enough to lean on for real architecture decisions comes down to which of those two you pick.

Where it’s still soft

Better I list these than someone else finds them first.

The cross-validation step — output against acceptance criteria — is still largely a judgment call made by the same context that did the work. Better than no check, worse than independent verification. I haven’t decided yet whether it should run in a fresh context, a different model, or alongside actual test execution instead of standing in for it.

feature.md has no compaction strategy. It’s append-only, and a feature that lives across thirty sessions produces a file nobody wants to read back in — which defeats the purpose of reading it back in. The fix is probably a rolling summary: collapse older sessions into a standing “current state,” keep the last handful verbatim. Haven’t built that part yet.

And the loop checks internal consistency, not whether Confluence was right to begin with. If the source doc was ambiguous or just wrong when the acceptance criteria were derived, everything downstream will faithfully validate against a flawed baseline and call it done. Concurrent sessions on the same ticket, partial completion across multiple PRs, someone editing Confluence mid-session rather than between sessions — none of that is handled yet either.

None of that erases the point. It means I’m describing something meaningfully better at catching drift than what I had last time, not something immune to it. If you’re building anything like this, I’d rather hear where yours breaks than where it works — that’s usually the more useful conversation.


TL;DR — Feature MD solves “who’s writing this down” but not “is any of it true.” The fix is a loop, not a pipeline: derive acceptance criteria from the source doc, build against them, then validate twice — once against the criteria, once against the live source. Close the loop with session-start hooks (initializes the file), session-stop hooks (writes the changelog and touched modules), and a confirmation step that keeps every action attributed. The same infrastructure produces an emergent component map as a side effect. It still breaks when the source doc is wrong, when compaction is absent, and when independent verification is skipped — but it catches drift sooner than relying on an agent’s self-report.