---
title: "My agents needed a filing system before they needed more intelligence"
canonical: https://dxdev.com/blog/2026-05-04_filing-system/
datePublished: 2026-05-04
---
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 `ITEM-XXXX.md` files. Epics I had not promoted yet sat in `00_Epic_Unknown/` 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 HTO, 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: `hto`, `agent_system`, `blog`, `business_ops`, `personal`. Each lives under `vault/01_Areas/<NN_slug>/` and mirrors the same internal structure: `00_Dashboard/`, `01_Epics/`, `02_References/`, `03_Archive/`. The area slug and signal list live in the area's `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 `area_key` and `area_signals` from one file and stop guessing, because the surface it was reading had finally become consistent. The automation did not get smarter.

## /organize was the canary

The `/organize` skill reads the vault's area structure, the current session manifest, and active task state, then prompts Claude 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 `00_Epic_Unknown` 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

`00_Epic_Unknown` 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.

`promote_epic.py` walks the unknown bucket, reads each file's `issuetype` and `epic_link` fields, matches against the area's `01_Epics/` folder, and moves the file into the right named directory, creating the folder if it does not exist. It is fifty lines. 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. Sessions lived in `vault/04_Sessions/YYYY/MM/DD/NNN_<sid>.md` with a `status:` field that could 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 at session start via the SessionStart hook in `~/.claude/settings.json`. After both changes, the dashboard dropped from seventeen active tasks to eight, all actually 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.

Same day I landed these changes, I also shipped four HTO tickets, including a stat-label fix on ITEM-6881 and a 3.347.22 hotfix release for ITEM-6901. The vault work took maybe three hours. 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 `promote_epic.py` to run and `00_Epic_Unknown` to hit zero.
