# L-006 Enumeration-Divergence Sweep (T-1109 Phase 7)

## Summary

**Scan window:** 20 min · **New instances:** 7 confirmed + 1 candidate for demotion
**Scope:** `bin/`, `lib/`, `web/blueprints/`, `web/templates/`, `web/watchtower/`, `agents/context/`, `agents/audit/`, `agents/task-create/`, `.claude/settings.json`, `status-transitions.yaml`, `.context/cron-registry.yaml`
**Canonicals already in place:** `status-transitions.yaml` (loaded by `lib/enums.sh`); `.fabric/subsystems.yaml` (loaded by `fabric.py`); `.context/cron-registry.yaml` (loaded by `cron.py`).
**Dominant failure mode:** the web tier (Python blueprints + Jinja templates) reimplements every enum that already lives in `status-transitions.yaml`, producing a **Python/Jinja/Bash triple mirror** that is the biggest cluster found.

## Known instances (confirmed before this sweep)

- **G-024** — `lib/upgrade.sh:388` agent-directory list (`task-create handover git healing fabric dispatch resume audit session-capture`) vs `bin/fw:181-190` `do_vendor()` includes list. Already drifts — missing `capture`, `docgen`, `mcp`, `observe`, `onboarding-test`, `termlink` out of 16 agent dirs under `agents/`.
- **G-037** — `bin/fw:193-202` `do_vendor()` excludes (9 entries) vs `lib/update.sh:194-198` `do_update()` excludes (3 entries). Already drifting — `lib/update.sh` is missing all 5 `lib/ts/*` excludes.
- **G-038** — `web/blueprints/tasks.py:439,511` hardcoded `workflow_types` + `448,481` horizons + `545` statuses vs `status-transitions.yaml` canonical.
- **G-039** — `lib/config.sh:130-148` `FW_CONFIG_REGISTRY` vs `web/blueprints/config.py:14-32` `SETTINGS` vs `CLAUDE.md:501-519` config table (triple mirror, 17 rows each).

## New instances discovered

### L-006-NEW-001: owner enum (`human` / `claude-code`) has 7 parallel sites, no canonical

- **Canonical:** *none* — no `VALID_OWNERS` in `status-transitions.yaml` or `lib/enums.sh`. `agents/task-create/create-task.sh` accepts free-form via `--owner` argument.
- **Mirror A:** `web/blueprints/tasks.py:443` — `allowed_owners = ["human", "claude-code"]`
- **Mirror B:** `web/blueprints/tasks.py:496` — inline tuple `("human", "claude-code")` (duplicate in same file)
- **Mirror C:** `web/templates/tasks.html:496` — Jinja `['human','claude-code']` kanban card
- **Mirror D:** `web/templates/tasks.html:711` — Jinja `['human','claude-code']` list view
- **Mirror E:** `web/templates/task_detail.html:166` — Jinja `['human','claude-code']` detail view
- **Agent audit:** `agents/audit/audit.sh:2010` — `owner != "human"` check (treats anything ≠ human as agent)
- **Currently matches:** yes (all 5 list sites match)
- **Asymmetry risk:** bash free-form allows any owner string; Python strictly rejects non-`{human,claude-code}` inputs. A task with `owner: orchestrator` created via `fw task create --owner orchestrator` will load in bash CLI but render broken in Watchtower and be unsettable via the inline dropdown.
- **Proposed chokepoint:** add `owners:` section to `status-transitions.yaml`; export `VALID_OWNERS` from `lib/enums.sh`; expose via a new `/api/enums` endpoint (or `fw_enums.py` helper that reads the YAML) consumed by both blueprints and templates via `render_page` context.
- **Severity:** **medium** (silent rejection, user-visible UI bug; asymmetry already exists between bash and web).

### L-006-NEW-002: workflow_type triple mirror — Python blueprint, Jinja templates, bash enum

- **Canonical:** `status-transitions.yaml:17-24` → `lib/enums.sh:VALID_TYPES`
- **Mirror A:** `web/blueprints/tasks.py:439` — `allowed_types = ["build", "test", "refactor", "specification", "design", "decommission", "inception"]`
- **Mirror B:** `web/blueprints/tasks.py:511` — same list, second copy
- **Mirror C:** `web/templates/tasks.html:494` — kanban card inline_select
- **Mirror D:** `web/templates/tasks.html:707` — list view inline_select
- **Mirror E:** `web/templates/task_detail.html:174` — detail view inline_select
- **Secondary anchor:** `agents/audit/audit.sh:2209` — `FAST_TYPES = {"test", "specification", "decommission"}` (subset, transformation)
- **Currently matches:** yes (7 entries across all 5 mirror sites)
- **Proposed chokepoint:** same `/api/enums` endpoint + `get_workflow_types()` Python helper that shells out to `lib/enums.sh` or reads `status-transitions.yaml` once at startup. Alternative: Flask context processor injects `ENUMS` dict into every template.
- **Severity:** **high** (G-038 explicitly names tasks.py but omits the Jinja mirrors; adding a new type like `governance` requires 5 synchronized edits with no gate).

### L-006-NEW-003: horizon triple mirror (same topology as NEW-002)

- **Canonical:** `status-transitions.yaml:26-29` → `lib/enums.sh:VALID_HORIZONS`
- **Mirror A:** `web/blueprints/tasks.py:448` — `if horizon not in ("now", "next", "later")`
- **Mirror B:** `web/blueprints/tasks.py:481` — same tuple, second copy
- **Mirror C:** `web/templates/tasks.html:495` — Jinja `['now','next','later']`
- **Currently matches:** yes
- **Proposed chokepoint:** same as NEW-002
- **Severity:** **medium** (3 entries, smaller blast radius than types, but identical structural flaw)

### L-006-NEW-004: active-status quadruple mirror (Python, Jinja, CSS, watchtower prioritizer)

- **Canonical:** `status-transitions.yaml:8-12` → `lib/enums.sh:VALID_STATUSES`
- **Mirror A:** `web/blueprints/tasks.py:393` — `status_options = [...]`
- **Mirror B:** `web/blueprints/tasks.py:545` — `allowed = [...]` (duplicate in same file)
- **Mirror C:** `web/templates/tasks.html:489,699` — Jinja inline_select lists (2 copies)
- **Mirror D:** `web/templates/tasks.html:652` — Jinja for-loop `{% for s in [...] %}`
- **Mirror E:** `web/templates/tasks.html:149,152,155,158,174,177,180,191,502,514,526,538` — kanban CSS + HTML column selectors (each status named separately in CSS and HTML)
- **Mirror F:** `web/watchtower/prioritizer.py:16-18` — priority dict `{"issues": 0, "started-work": 1, "captured": 2}` (subset/transformation, but any new status silently sorts as default)
- **Mirror G:** `web/blueprints/core.py:190` — `status in ("started-work", "captured")` (subset check)
- **Currently matches:** yes for the top 5 sites; prioritizer and core.py are transformations that would silently miss a new status
- **Proposed chokepoint:** macro-based template partial driven by a Flask context processor; `prioritizer.py` loads from `lib/enums.sh` output via subprocess or direct YAML read; kanban CSS generated from Jinja loop instead of 12 hand-written selectors.
- **Severity:** **high** (most-copied enum in the codebase — 7+ mirror sites; kanban CSS is the worst: each status is hand-named and a new status wouldn't just "not render", it would be invisible because there's no column for it).

### L-006-NEW-005: `fw hook` usage-message list drifts from canonical filesystem

- **Canonical:** `agents/context/*.sh` — actual hook scripts (filesystem is source of truth; `fw hook <name>` loads `$AGENTS_DIR/context/${name}.sh`)
- **Mirror A:** `bin/fw:3370-3372` — hardcoded "Available hooks:" error message listing 15 hook names
- **Mirror B:** `.claude/settings.json:1-146` — runtime hook registration (10 distinct hook invocations)
- **Mirror C:** `bin/fw:876-884` — doctor's `expected` dict of 6 hooks (subset; tier-based required-hook checker)
- **Mirror D:** `web/blueprints/enforcement.py:20` — `hooks = {"tier0": False, "tier1": False, "checkpoint": False}` (subset; enforcement UI panel)
- **Currently matches:** Mirror A lists 15 hooks matching filesystem; Mirror C only asserts 6; Mirror D only checks 3. No drift *yet*, but four independent lists where the usage message (Mirror A) and the settings.json reality (Mirror B) have already diverged once — a new hook added to settings.json won't automatically update either the usage message or the enforcement panel.
- **Proposed chokepoint:** generate the usage list from `ls $AGENTS_DIR/context/*.sh`; doctor's `expected` map becomes `{script: event}` computed by parsing `.claude/settings.json` from the project itself; enforcement.py queries the same settings.json (it already does — but then applies a 3-entry whitelist, which is the actual bug).
- **Severity:** **medium** (user-facing usage list will rot; enforcement UI already drops 7+ real hooks from its status panel, misleading the human about coverage).

### L-006-NEW-006: `fw` subcommand list — case statement vs `show_help()` prose

- **Canonical:** top-level `case "$command" in` block in `bin/fw` (~102 case entries from line 1901 onward)
- **Mirror:** `bin/fw:433-532` — `show_help()` function with ~50 hand-written `echo -e` command descriptions
- **Currently matches:** partial — spot-check shows several case entries that are not in show_help (`ask`, `plugin-audit`, `fix-learned`, `traceability`, `enforcement`, `preflight`, `validate-init`, `mcp`, `consolidate`, `upstream`, `cron`) plus deprecated/hidden aliases. Conversely, help mentions `setup` (deprecated alias) and `tier0` (which the case statement actually handles via subcommand parsing).
- **Proposed chokepoint:** introduce a lightweight registry file (e.g., `lib/commands.yaml`) mapping command → description → category, loaded by both the router and `show_help`. Or at minimum generate help output by parsing the case block with a static analyzer at release time.
- **Severity:** **medium** (documentation drift is user-facing: new commands are routinely added without a help entry — the user can't discover `fw consolidate`, `fw upstream`, `fw mcp`, etc., from `fw help`).

### L-006-NEW-007: agent sync list in `lib/upgrade.sh` vs on-disk `agents/` directory

- **Canonical:** `agents/*/` directory listing (16 subdirs)
- **Mirror A:** `lib/upgrade.sh:388` — `agent_dirs="task-create handover git healing fabric dispatch resume audit session-capture"` (9 entries)
- **Mirror B:** `lib/upgrade.sh:326-357` — `agents/context/*.sh` handled separately via glob (10th category, implicit)
- **Mirror C:** `bin/fw:181-190` — `do_vendor()` copies `agents/` wholesale (does not enumerate; glob-based)
- **Currently matches:** **no — already drifting.** Missing from the upgrade path: `capture`, `docgen`, `mcp`, `observe`, `onboarding-test`, `termlink`. Consumer projects upgrading via `fw upgrade` will silently NOT receive any of these 6 agents' bugfixes or features.
- **Proposed chokepoint:** replace the explicit `agent_dirs` string with `for d in "$FRAMEWORK_ROOT/agents/"*/; do` (same pattern `do_vendor` already uses) OR make `lib/upgrade.sh` call into `do_vendor` for the agents copy step.
- **Severity:** **high** (already causing silent drift in every consumer that runs `fw upgrade` — the framework's own upgrade path is blind to 37% of its agent directories). This is arguably part of the existing G-024 scope but is a **distinct enumeration** from `do_upgrade()` step 4b; both should be fixed in the same chokepoint commit.

## Non-instances (considered and rejected)

- **`web/blueprints/fabric.py:_load_subsystems()`** — reads `.fabric/subsystems.yaml`, no hardcoded mirror. ✓
- **`web/blueprints/cron.py`** — reads `.context/cron-registry.yaml`, no hardcoded mirror. ✓
- **`web/blueprints/risks.py:27`** — `{"urgent": 0, "high": 0, "medium": 0, "low": 0}` is a counter initializer for a 5×5 risk matrix; the 4 ranking levels are defined by the matrix math (score → band) in `risks.html:125`, not duplicated. Single source (matrix) is arithmetic; no drift risk.
- **`lib/init.sh:354-386`** — provider enum (`claude|cursor|generic`) appears only in one case statement; no mirror found.
- **`agents/task-create/create-task.sh`** — stdin/arg parsing for `--type`, `--owner`, `--horizon` flows through `lib/enums.sh`. ✓
- **`status-transitions.yaml:legacy`** — `refined`, `blocked` appear in `lib/enums.sh:LEGACY_STATUSES` and nowhere else. ✓

## Recommendations

### Immediate gap registration (keep them distinct from G-024/G-037/G-038/G-039)

Register **4 new gaps**, all in the `L-006` family:

1. **G-040** — NEW-001 (owner enum mirror × 5 sites, no canonical). Medium severity. Add `owners:` to `status-transitions.yaml` as part of the fix.
2. **G-041** — NEW-004 (active-status × 7 mirror sites, including kanban CSS). High severity. Flag kanban CSS as the most structurally fragile site (invisibility of new statuses).
3. **G-042** — NEW-005 (`fw hook` list × 4 mirror sites). Medium severity. Fix path: enumerate `agents/context/*.sh` at runtime.
4. **G-043** — NEW-006 (`fw` subcommand × 2 mirror sites in the same file). Medium severity. Fix path: command registry YAML or case-block parser.

NEW-002 and NEW-003 should be **folded into G-038** as an expansion note — same Python+Jinja+bash triple-mirror pattern, same chokepoint (enums injected into templates via context processor).

NEW-007 should be **folded into G-024** as a secondary site — it's the same `lib/upgrade.sh` file that G-024 already names, but a different enumeration on a different line (line 388 vs. step 4b), and it's *already* drifting.

### Sweep-level build task

**T-1110 (proposed): Collapse all framework enums into a single source of truth loaded at startup.**

Scope:
- Extend `status-transitions.yaml` with `owners` and (optionally) `ranking` sections.
- Add `fw_enums.py` helper that reads the YAML once and exposes `VALID_STATUSES`, `VALID_TYPES`, `VALID_HORIZONS`, `VALID_OWNERS` dicts.
- Flask context processor injects `ENUMS` into every template.
- Replace all 20+ hardcoded Python/Jinja mirror sites with `ENUMS.types`, `ENUMS.statuses`, etc.
- Replace kanban CSS hand-written selectors with a Jinja `{% for status in ENUMS.statuses %}` loop.
- Replace `lib/upgrade.sh:388` and `lib/upgrade.sh` 4b with a glob over `agents/*/`.
- Replace `bin/fw` usage hook list with a glob over `agents/context/*.sh`.
- Replace `bin/fw:876-884` doctor `expected` dict with a parse of `.claude/settings.json` (the actual contract).

**Estimated effort:** 200–300 LOC removed, 80–120 LOC added (net negative).
- `status-transitions.yaml`: +6 LOC (owners section)
- `lib/enums.sh`: +8 LOC (owners export)
- `fw_enums.py` (new): +40 LOC
- `web/app.py` context processor: +15 LOC
- `web/blueprints/tasks.py`: -25 LOC (remove 4 hardcoded lists)
- `web/templates/tasks.html`: -35 LOC (replace hardcoded lists with loops; replace kanban CSS)
- `web/templates/task_detail.html`: -10 LOC
- `web/watchtower/prioritizer.py`: -5 / +8 LOC (load from enum instead of dict literal)
- `web/blueprints/enforcement.py`: -5 / +15 LOC (compute hook coverage from settings.json, not 3-entry whitelist)
- `lib/upgrade.sh`: -15 / +10 LOC (replace enumerations with globs)
- `bin/fw` (hook usage msg + doctor expected): -15 / +20 LOC

Net: ~115 LOC saved; 4 confirmed gaps closed; 2 existing gaps (G-024, G-038) fully retired.

## Method

1. Grepped for Python list/tuple/set literals with `allowed_`, `valid_`, `VALID_`, `[Ll]egal`, `STATUS_`, `TYPE_` prefixes in `web/blueprints/` → found tasks.py (4 sites), discovery.py (scope filter — not an enum), enforcement.py (hook dict).
2. Grepped for status tokens (`captured|started-work|issues|work-completed`) and type tokens (`specification design build test refactor decommission`) across the whole tree, filtered to `*.py`, `*.html`, `*.sh` → found the Python+Jinja+bash triple mirror.
3. Read `bin/fw:show_help()` (100 lines) + counted case-statement entries (102) → confirmed help drift.
4. Inspected `lib/upgrade.sh` lines 325–405 and compared `agent_dirs` string against `ls agents/` → confirmed 6 agents missing from the upgrade path.
5. Compared `bin/fw:876-884` doctor expected-hooks dict against `.claude/settings.json` hook list against `bin/fw:3370-3372` usage message against `agents/context/*.sh` filesystem listing.
6. Inspected `web/blueprints/enforcement.py:_hook_status()` → found 3-entry hardcoded whitelist.
7. Verified non-instances: `fabric.py`, `cron.py`, `risks.py`, `init.sh` providers, `create-task.sh`.

**Files inspected (direct reads, ≥20 lines each):** `lib/enums.sh`, `status-transitions.yaml`, `lib/config.sh`, `lib/update.sh`, `lib/upgrade.sh`, `lib/init.sh`, `bin/fw` (6 regions), `web/blueprints/tasks.py`, `web/blueprints/config.py`, `web/blueprints/enforcement.py`, `web/blueprints/fabric.py`, `web/templates/tasks.html`, `agents/audit/audit.sh` (4 regions), `agents/context/bus-handler.sh`, `agents/task-create/create-task.sh`, `.claude/settings.json`, `.context/cron-registry.yaml`, `CLAUDE.md` (config table region).
