On May 4 the dashboard said seventeen active tasks. Four were epic shells with no children attached, three were sessions I had closed mid-work without reconciling the manifest, and two more were sub-sessions Claude had opened under a ticket and never linked back to a parent. Nine out of seventeen were not active in any useful sense, and the problem had nothing to do with bad data entry.
The vault, the task model, and the session pipeline were each using a slightly different definition of “active,” and nothing was forcing them to agree.
the layout was teaching me something I ignored
Tasks lived as flat files. Epics I had not promoted yet sat in an Unknown staging folder as loose files structurally identical to task files unless you opened them and checked the issuetype field inside. Session manifests tracked area: and task:, but neither was validated against anything real, so sessions drifted into free-float after the work closed. When I wrote the dashboard regenerator for the production app, it needed a separate code path for every layout variant it encountered.
That is the tell. An automation that accumulates a new special case for every layout it touches is not a routing problem waiting to be solved. The layouts are wrong.
five peer areas with one internal shape
I flattened the vault to five peer areas, each with the same internal structure: a dashboard folder, an epics folder, a references folder, and an archive. The area definition lives in a single README.md so every classifier, every session hook, and every dashboard script reads from the same source of truth.
Before this, area classification ran by string-matching against folder names that had drifted out of sync with the actual area definitions. After, a script can read the area key and signal list from one file and stop guessing, because the surface it was reading had finally become consistent. The automation did not get smarter.
the organizer was the canary
The organize skill reads the vault’s area structure, the current session manifest, and active task state, then prompts the agent to surface misclassified or unlinked work. Before the reorg, it crashed on shape mismatches roughly half the time, because it assumed a standard epic folder structure and the unknown bucket was anything but. After the reorg, the first run flowed from start to finish without a single special case firing.
That is a more useful signal than a unit test. A tool that was previously brittle because the underlying layout was incoherent became reliable the moment the layout became predictable.
the unknown bucket was the problem
The unknown staging folder is where agent systems go to rot. Any work without a clean epic home lands there, and once it is there, classifiers stop touching it because the bucket name signals ambiguity. I had 47 files in that bucket, each a real epic that had shipped or was in progress but had never been promoted to a named folder.
A small promotion script walks the unknown bucket, reads each file’s issuetype and epic_link fields, and moves it into the right named directory, creating the folder if needed. The whole thing is fifty lines:
for f in UNKNOWN.glob("*.md"): text = f.read_text(encoding="utf-8") if fm(text, "issuetype") != "Epic": continue # not an epic; leave for the task router epic = fm(text, "epic_link") or f.stem dest = EPICS / epic dest.mkdir(parents=True, exist_ok=True) shutil.move(str(f), str(dest / f.name))Running it cleared the bucket to zero and fixed the dashboard’s active count in one shot.
The promotion logic itself was straightforward. The real issue was that the bucket existed as a permanent home rather than a temporary staging area. A system that treats “unknown” as a valid steady state for categorized work will slowly accumulate everything that does not fit neatly, which is most real work.
sessions had to follow the same rule
The session pipeline had its own version of the same problem. Each session file carries a status: field that can be active, recent, or closed. But “active” was not enforced anywhere, so sessions that had been functionally closed for weeks still showed up in the count. Epics were masquerading as live tasks because a session manifest had referenced an epic ID rather than a specific task ID, and the dashboard was counting the reference as active work.
The fix had two parts. First, a root rule: session manifests must reference a task ID, not an epic ID. Epics are groupings, not units of work. Second, a daily sweep that marks sessions recent if last activity was more than 24 hours ago and closed past 72 hours, triggered automatically at session start. After both changes, the dashboard dropped from seventeen active tasks to eight, every one of them genuinely in progress.
state has to converge at the filesystem first
None of these four fixes involved Claude doing something smarter. No classifier got a better prompt, and none of the new code crossed fifty lines.
What changed was that every tool was now reading from surfaces that agreed with each other: canonical area shapes, real epic folder locations, session manifests pointing at tasks instead of epics. The dashboard could regenerate from the filesystem and produce a count that matched reality.
The vault work took maybe three hours, the same day I shipped four production tickets. It felt like housekeeping. But it was the reason the next session’s dashboard told me the truth, which is a different thing from making the system louder about its lies.
An agent system that cannot distinguish an active task from an epic shell does not need a smarter model. It needs the promotion script to run and the unknown bucket to hit zero.