The product is real; the enforcement is not yet. nablr's core — 18 personas, a phase state machine, 12 scan targets built on genuine AST analysis (tree-sitter), per-story git-worktree isolation, licensing with trial flow — all exists and runs. The architecture direction is sound and several subsystems are genuinely well-crafted.
But the audit's central finding is systemic: the governance layer is largely advisory in practice. Nearly every gate that is supposed to block an LLM agent fails open, is self-certifiable, or is unreachable. A second systemic finding: nablr does not pass its own bar. Its own health scan reports 93 critical findings; 73 files exceed complexity 30 while the product enforces ≤15 on users; CI runs coverage with --cov-fail-under=0 and only on main, while the shipped protocol demands ≥85% on changed files.
None of this is fatal — the failure modes are specific, enumerable, and mostly cheap to fix. The P0 list below is 7 items.
scripts/airgap_proof.py, security scan) is a real differentiator — worth keeping honest (see Finding 4).ref:allow) with justification comments.utils/worktree.py + merge_back with conflict-safe git reset --merge; three live worktrees confirm real use.transitions.py:366, guard_tools.py:102) — deliberate anti-drift design. Report freshness (2h health, signed full-suite markers) is enforced.*_deferred tokens write TECH_DEBT rows; force=true logs an L2 escalation and cannot bypass impact_reviewed_*.validate(role="__health__")), retry counter keyed on artifact hash.full_suite_runner.py — the codebase's only atomic write, locked signer, correct double-checked singleton, subprocess timeouts. This is the pattern the rest of the state layer should copy.noqa carries a TECH_DEBT: + story ID; TECH_DEBT.md logs real, reproduced flaws against the project itself (e.g. DEBT-1a5fe7ac, DEBT-ec74396b).Literal action unions, pydantic report models, only 18 type: ignore in ~40k LOC.tmp_path; test_e2e_sdlc_flow.py drives the actual MCP tools through all 8 phases and asserts real gate refusals; several tests are named regression locks for specific prior bugs.The product's promise is enforcement. Today most enforcement paths can be skipped, self-certified, or silently pass when their inputs are missing.
workflow(action="validate_and_advance") accepts any next_phase with no adjacency or artifact check (state_machine.py:582-695). PHASE_0 → PHASE_7 in one call. Already bit you: DEBT-ec74396b. Fixed STORY-GATE-PHASESKIP-001 (c1bf456): validate_and_advance enforces legal phase transitions._unresolved returns allowed=True (validators/__init__.py:240-248) — and every validator returns exactly that when it can't find its file. No artifact ⇒ no validation ⇒ pass. The strictest-looking layer is the weakest. Fixed STORY-GATE-FAILCLOSED-001 (220db52): _unresolved gaps now block with remediation text; only no_project_root stays fail-open; gate exceptions in log(event="phase") also fail closed.docs/requirements/{id}.md, but create_story writes to active/ (hierarchy_manager.py:25-31). Every story created through the supported path silently skips validation via the fail-open above. resolve_story_file_path exists but has only 3 call sites. Fixed e09f005 + STORY-GATE-FAILCLOSED-001: validators route through the shared resolver (flat/active/done/archive), locked by both-paths parity tests; platform_engineer now reads the real report path and timestamp key.log(event="artifact") sets any token true with no whitelist, no verification — including impact_reviewed_provisional, a HARD_REQUIRED gate (misc_tools.py:158-183). And the early return at server.py:428-431 means these grants never reach the handoff log: 0 artifact events in 1,027 log lines.impact_reviewed_verified is both HARD_REQUIRED and a PHASE_4 auto-signal — set true unconditionally on developer phase-close with no evidence check (phase_signals.py:44-50).completed_phases is a permanent bypass. Once a phase is in the list, all its artifact checks short-circuit true forever (transitions.py:479-482); nothing removes entries, and log(event="phase", outcome="failed") has zero state effect. Rework loops auto-pass health/test/full-suite gates.add_blocker appends to state; the only consumer is can_transition, which production never calls. L1/L2/L3 write identical log rows; "HALT" is display text — nothing prevents the next tool call. HALT flags live in an in-memory set, gone on restart (session.py:86-92).start_fix_flow pre-grants 5 gate artifacts including impact review, omits impact_architect from its injected sequence, and permanently hijacks active.json (never restored — live state shows FIX-611BC079 as active). AGENTS.md's claim that skipping impact review "is blocked at PR review" has no enforcing code.log(event="phase", phase="implementation") (name instead of key) resolves no phase → skips produce-gates and validators, reports success (server.py:438-449).close_workflow sets story_closed but never advances to PHASE_7 — closed stories report in-flight forever; 55 of 77 active/ stories still say Draft for shipped work.migration_planner/parity_tester/cutover_engineer exist as personas but have no PHASE_CONFIG entries → activation with zero artifact requirements.next_agent.py:189-191).agent(action="activate") injects no persona. It returns a generic "EXECUTION CONTRACT" string and never calls any get_*_prompt() (server.py:859-962). Personas exist only as MCP prompt endpoints, which AGENTS.md never tells the client to invoke. Following your own documented protocol activates a role with zero rule content. The only path that injects a persona is fix-flow. Fixed STORY-PERSONA-INJECT-001 (8f27090, PR #10): agent(activate) now delivers the persona._detect_tech_stack() uses Path.cwd() — the MCP client's launch dir, not the project root — then strips rule sections based on it (start_agent.py:124-140). Frontend/backend rules are dropped or kept essentially at random. Fixed (same story): tech-stack detection uses the project root; orphan rule files shipped into personas.core_protocol.md missing from 8/18 personas (incl. architect, scrum_master, PM); 3 rule files loaded by nobody — including artifact_tokens.md, the doc that explains how to satisfy every gate token.test_designer). Phase numbering disagrees across three sources (QA_GATE is PHASE_2 but runs after PHASE_3)."🔴 Critical"); the health scanner maps severity.lower() against plain names → never matches → every debt finding downgrades to MEDIUM (health_scanner.py:504-510). Confirmed live: all 659 debt findings in your own report are medium, including 45 bare-excepts configured critical. Debt can never trigger auto-remediation. Fixed STORY-SEVERITY-PIPELINE-001 (45e21f5): one severity vocabulary end-to-end; emoji-vs-name mapping bug removed in health + debt scanners.scan(target="complexity") uses 80/150 and reports the same files OK. Per-function complexity for Python "isn't tracked yet" (complexity.py:259) despite being the advertised metric.dead_code_scanner.py:429-468); TS ternary regex matches every optional property; docs scanner flags Markdown headings like "# Get started" as code-echo comments; arg-count rule counts self → every method "missing 1 type hint".PROJECT_SOURCE_PACKAGES=("nablr",) means UUT-mock detection can never fire in any customer project; import-order treats only nablr.* as local; validators hard-require structlog imports and literal FR-\d+ tokens in arbitrary user repos; branch name dev hardcoded in developer validator.tier1_fixer.py:78-99); no backup/dirty-check/post-fix test run; completion always logs outcome="passed".build/dist/target; only 2 scanners consult the gitignore matcher.ast_node/condition parsed and never consumed; config/ai_patterns.yaml never read; framework merge silently resets complexity thresholds to hardcoded values and drops exclusions (config_models.py:141-167).src/nablr wrote reports and TECH_DEBT into src/nablr/nablr-reports/ and src/nablr/docs/ — inside the shipped package tree. Multiple stray nablr-reports//codemap/ dirs already litter src/.scan/remediate/full_suite accept arbitrary absolute paths. A prompt-injected assistant can point the secret scanner at ~/.ssh (masked prefixes written to a report), run tier-1 fixes (file mutation) anywhere, or run full_suite — which executes npm test/gradlew/mvn/cargo in any directory, with repo-supplied nablr-config.yaml extra_args appended verbatim to argv. Fixed STORY-PATH-CONFINE-001 (6675c74, PR #5): MCP tool inputs confined to project root (utils/path_confine.py), full_suite argv hardened.story_id et al. interpolate straight into file paths (f"{story_id}.json"); pydantic fields have no pattern=. A ../-bearing ID writes outside the state dir. Fixed (same story): IDs validated.NABLR_DEV_MODE=1 returns paid status before any check (license_client.py:420-421); transport auth defaults to a verifier accepting every token (auth.py:108-116). Key comparison non-constant-time; paid asset cache never expires offline.dependencies = [ line (reported 0 deps for this very repo), has no CVE check; file audit's "subprocess" detection can never match. Report titles itself "nablr Security Audit" for any project.get_nablr_state_dir() edits the user's .vscode/settings.json and .cursorignore; a health scan mutates the scanned repo (writes TECH_DEBT.md, relocates docs).demo_security_scan.sh claims "no network calls" then instructs the customer to run the exact grep that disproves it (urllib in license_client.py). README's "exactly one category of outbound call" is the honest framing — use it everywhere.None = "no such story", and re-init dead-ends in FileExistsError → story permanently unopenable (state_machine.py:239-265, init.py:303). Worse: a corrupted story file makes the scrum-master close-out gate pass (requirements fall back to []). Fixed STORY-STATE-ATOMIC-001 (PR #4, 06ed98e): atomic write/read for state layer.tester/security_owner completions lose updates (parallel.py:165); story_lock exists but guards 2 of ~9 mutation sites, and its own dict insertion races.merge_back deletes the audit trail. Worktree removal takes .nablr_state/ and handoff_log.jsonl with it — phase history vanishes at merge with no export (merge_back.py:206).DEBT-1d576de9 — cross-worktree corruption already logged).touched_files_since_story_init by the UTC offset; second-resolution graph node IDs collide; git subprocesses have no timeouts (a credential prompt hangs the MCP handler)..nablr_state/; 15MB unignored sef-reports/.ci.yml triggers on main only; the active branch is dev; story-branch PRs into dev match nothing. Tests first run at the batched dev→main PR, long after stories are declared Done. Fixed STORY-CI-DEV-BRANCH-001 (c452af5): push+PR triggers on [main, dev]; paths-ignore narrowed to pure prose; lint/security/test parallel with build gating on all three.--cov-fail-under=0 in a job named "Test & Coverage" (ci.yml:93), against a stated bar of ≥85% on changed files. No coverage config exists anywhere; coverage.xml is produced and discarded. The 85%-on-changed-files bar is currently uncomputable (no diff-cover). Fixed CI-DEV-BRANCH-001 uploads coverage.xml every run; STORY-COVERAGE-GATE-001 (97c4670, PR #11) sets a real floor (--cov-fail-under=75, ratchet-only policy) and adds diff-cover for the changed-files bar.pass_summary total/passed/failed are hardcoded 0 literals (full_suite_runner.py:689-693); evaluate_gate checks signature/freshness/exit codes only. A run collecting nothing passes. The shape-lock test asserts only that the key exists.tests/generated/ are assert symbol is not None (6 of 7 for the same symbol), regenerated by design, untracked in git and invisible to CI — yet counted in local green runs and already indexed in codemap.scripts/propagate.sh, which doesn't exist — the "17 skipped" your status reports record as green. Two real red tests sit in .pytest_cache/lastfailed.patch.object blind spot — the strongest UUT-mock violation in your own suite (test_brownfield_populator.py:31-75) is invisible to it.bandit --exit-zero discards MEDIUM/LOW Fixed (CI-DEV-BRANCH-001: scripts/ci_bandit_gate.py — HIGH/CRITICAL fail, MEDIUM annotated). No branch protection; provenance check is a warning, not a block.| Claim | Reality | Verdict |
|---|---|---|
| 19 expert personas | 18 (AVAILABLE_AGENTS); the 19th prompt is the entry point | Inflated |
| 40+ local scanners | 12 scan targets, ~17 real engines; own docs page says both "40+" and "10" | Inflated |
| Rules/configs "Pro only" | All 34 rule files + 11 YAMLs ship in the free wheel; free tier reads them locally | Not enforced |
| Free entry: "activate nablr" | activate_nablr prompt is behind _require_paid() — free users get an upsell | Broken |
| Jira sync "Shipped" | Reverted; doc still says Shipped; 306-LOC export module survives with zero callers | Stale |
| Parallel orchestration | Gate only, hardcoded 2 agents at PHASE_6; never dispatches (uncommitted epic plans the generalization) | Partial |
| Codemap generation | Runs, but output has 176 [Desc] placeholders → FEATURES.md degenerates to "2 features" for a 34-tool product | Hollow output |
scan(target="compliance") | Advertised in docstring + AGENTS.md; not in the dispatch — invalid target; 246-LOC dead implementation | Phantom |
| 14-day trial / licensing | Implemented properly (mint, device dedup, offline grace) | Real |
| Worktree isolation, browser E2E stepper, air-gap proof | Implemented (browser fixed at bcf0fcd; was hardcoded-success before) | Real |
public-docs/tools/overview.mdx: all 18 documented tool names are pre-consolidation — none exist (scan_health → scan(target="health"), activate_key → set_license_key, …). remediate, agent, workflow, validate, browser etc. publicly undocumented.release.toml changelog mode=update with no insertion flag and no marker in CHANGELOG.md → semantic-release silently never updates it.INDEX.md: claims 83 stories (actual 104), 6 epics (actual 11); status buckets include **|:** 11 from the fallback regex matching legacy metadata tables (requirements_serializer.py:137); 14 stories render "(untitled)"; TD/REF/FIX docs invisible to the generator.docs/requirements/TD-011…TD-015 belong to a different product (education app: mindmaps, tutors, EPIC-19/20) — cross-project write consistent with the stale-root bug in DEBT-1d576de9.active/ (STORY-011×3, BRANCH-RULESET, CI-MINUTES); archive/ never created despite the Done story specifying it; 22 md/json companion pairs split across directories; all 21 new uncommitted docs written to flat root, ignoring the subfolder convention.docs/requirements/README.md 7 months stale; USER_GUIDE.md references renamed tools; CHANGELOG headed "SEF Agents"; README badge says Python 3.13+ vs requires-python >=3.12; quickstart points to nablr.nablrco.com while everything else says nablr.dev; dist/ holds builds from two identities (nablr, ref_agents).| corrupts its own columns (the exact bug the row describes); manual registry severities invisible in the auto-rollup below it.Ran scan(target="health") and scan(target="security") on src/nablr as part of this audit:
| Metric | Result | Note |
|---|---|---|
| Critical / High / Medium | 93 / 107 / 1,638 | Product enforces "block on critical" for users |
| Files over complexity 30 (Tier-3 "full redesign") | 73 | Product mandates ≤15 per function; own worst: server.py CC 211, 1,793 LOC |
except Exception without re-raise | 34 critical hits | Own protocol forbids it; 29 handlers are body = pass |
| Security self-scan | FAIL (network imports) | Fails on its own allowlisted license client — allowlist matching bug |
| Dependency audit | "0 dependencies" | False — 10 runtime deps; parser missed its own pyproject format |
| Report placement | Wrote into src/nablr/docs/ & src/nablr/nablr-reports/ | Path-resolution bug pollutes shipped package tree |
Other structural items: 19 latent import cycles broken by 166 function-local imports (33 in server.py); utils/ depends upward on session/workflow/tools; a leaf tool imports nablr.server and spawns a daemon thread from inside a getter (context_graph.py:830); orchestration decisions made by regex-parsing scanner markdown (server.py:321-324, :477 — if "❌" in result); 148 logger.info calls unreachable at the configured WARNING level; dead dispatch enums duplicating the Literal unions; stale hardcoded validator_version="3.13.2"; PyYAML used on a core path but declared dev-only.
scan(target="regression") is filename-keyword matching wired to no gate.test_designer validator; conceptual-test plan sets tests_written three phases before executable tests exist (misc_tools.py:301).parallel.py:197-219) — contradicts the tester persona's "will be BLOCKED".guardian-ci.mdx markets the provenance story before the git-trailer bridge exists.Question examined: how efficient is nablr in token terms — measured not as raw count but as outcome per token. Answer: token cost is moderate and bounded; token efficiency is poor, and the waste is caused by reliability failures, not verbose prompts. Fixing the Finding 1 gates saves more tokens than any prompt trimming.
North star: FPY — % of phases (or stories) that pass their gate on the first attempt with no retry, no rework loop, no fix-flow traced back later. Efficiency = FPY ÷ cost (nablr-controlled tokens + cycle time); concretely, cost-per-first-pass-phase. Token figures are an honest lower-bound proxy — an MCP server cannot see client-side reasoning tokens.
| Surface | Cost | Note |
|---|---|---|
| AGENTS.md (always-on) | ~3.5k tok | Reasonable |
| Persona payloads | 2.0k–8.9k (developer max, mean ~5k) | Lean — but currently ~0 delivered (Finding 2) |
| Rule library | 49.2k across 34 files | Loaded per-role only — good design |
| Health report / TECH_DEBT / INDEX | 3.4k / 7.1k / 3.9k | Fine |
scan(debt, summary=false) | Unbounded (659 rows) | Worst single offender |
A full 10-phase story ≈ 30–60k nablr-controlled tokens once personas are actually delivered — the intended, acceptable price of governance.
Synthesis: tokens-per-completed-story is effectively unbounded — the completions denominator is zero in current state. The leak is rework multiplying how many times the same tokens get spent. Efficiency-ordered levers: (1) Wave 0–1 gate fixes — the biggest token saver; (2) deliver personas on activation; (3) cap scan(debt) output; (4) prompt trimming — smallest lever.
Tracked by STORY-FPY-METRICS-001 (EPIC-TEAM-SCALE-001, commit 2c5e160; Jira NABLR-5 under epic NABLR-4): enum outcomes + persisted retries, per-payload token ledger, generate_efficiency_report → nablr-reports/efficiency/, today's numbers committed as the day-zero baseline.
*_unresolved gaps must block, not pass (validators/__init__.py:240-248); route all story-path lookups through resolve_story_file_path.validate_and_advance gets adjacency + artifact checks; remove or gate log(event="artifact") self-certification behind a verified whitelist, and log every grant.pattern= on pydantic fields), resolve every directory/file_path arg against the project root, refuse escapes; stop honoring repo-supplied extra_args without an allowlist.dev + PRs, set a real --cov-fail-under, add diff-cover for the 85%-changed-files bar, add pyright, wire the test-smell scanner in, and parse real pytest counts into pass_summary (require total > 0 in evaluate_gate).full_suite_runner pattern (tmp + os.replace, lock) into state_machine._save_state and friends; treat JSONDecodeError as corruption, not absence.agent(activate) should return the persona prompt (or AGENTS.md should mandate the prompt endpoint); fix _detect_tech_stack to use the session root..nablr_state/ + handoff log before merge_back removes the worktree; restore the active-story pointer after fix-flow.suggest_next_agent); add revert_phase so failed phases can re-gate.activate_nablr (it's your funnel), delete or ship jira_confluence_export, remove the phantom compliance target, fix demo_security_scan.sh.public-docs/tools/overview.mdx against the real tool surface; fix INDEX serializer regexes; repair the release chain (changelog insertion marker; re-point tags at reachable commits).server.py (registration vs orchestration vs watcher lifecycle) and give scanners a structured result type — stop regex-parsing your own markdown for control flow.src/nablr/codemap/, stray report dirs); gitignore sef-reports/ or delete it.test_designer validator.active/ statuses (the close protocol works — Finding 1's closure bug is why they're stale).IgnorePatternMatcher.server.py, health_scanner.py, transitions.py resolve_artifact → token-resolver registry).artifact_tokens.md into the personas that need it — it's the manual for your own gates and nobody reads it.Done marks stories already implemented. Wave 0 complete (2026-07-28): WORKTREE-VENV-001 (ed48e8e), GATE-FAILCLOSED-001 (220db52), CI-DEV-BRANCH-001 (c452af5). Wave 1 complete (verified 2026-08-02): SEVERITY-PIPELINE-001 (45e21f5), GATE-PHASESKIP-001 (c1bf456), STATE-ATOMIC-001 (PR #4, 06ed98e), PATH-CONFINE-001 (6675c74, PR #5), COVERAGE-GATE-001 (97c4670, PR #11), PERSONA-INJECT-001 (8f27090, PR #10).
Principle: fix the tools you'll use to build everything else first, then correctness, then polish. Backlog committed as 5 new epics + stories linked into 7 existing epics (a8de8d4, 2c5e160).
| Wave | Theme | Stories (order) |
|---|---|---|
| 0 | Meta-blockers — the workflow itself is broken for dogfooding | WORKTREE-VENV-001 Done · GATE-FAILCLOSED-001 Done · CI-DEV-BRANCH-001 Done |
| 1 | P0 correctness (parallelizable) | SEVERITY-PIPELINE-001 Done · GATE-PHASESKIP-001 Done · STATE-ATOMIC-001 Done · PATH-CONFINE-001 Done · COVERAGE-GATE-001 Done · PERSONA-INJECT-001 Done |
| 2 | Gate + state hardening | GATE-TOKEN-AUDIT-001 → FIXFLOW-INTEGRITY-001 · STATE-LOCKING-001 · MERGEBACK-STATE-EXPORT-001 · FULLSUITE-COUNTS-001 · GATE-PHASE-REVERT-001 · GATE-BLOCKERS-001 · CODEMAP-DEDUPE-001 |
| 3 | Scanner correctness | SCAN-REPORT-PATHS-001 → REMEDIATION-BOOKKEEPING-001 · SCANNER-FP-001 · THRESHOLD-COHERENCE-001 · SCANNER-PORTABILITY-001 · TESTSMELL-CI-001 |
| 4 | Workflow completion + telemetry | CLOSE-PHASE7-001 → FPY-METRICS-001 · VALIDATOR-TESTDESIGNER-001 · SESSION-COHERENCE-001 · REQDOCS-SERIALIZER-001 + REQDOC-LIFECYCLE-001 (paired: parser/placement vs lifecycle moves; Jira NABLR-6) · MIGRATION-GATES-001 |
| 5 | Truth & release | LICENSE-INTEGRITY-001 + RELEASE-CHAIN-001 (both before next PyPI publish) · FREE-ENTRY-001 · DOCS-CLAIMS-001 · PUBLICDOCS-TOOLS-001 · SCAN-RESULTS-STRUCTURED-001 (last — pure refactor, safest under waves 0–2 protection) |
FPY-METRICS-001 placement rationale: Wave 4, immediately after CLOSE-PHASE7-001 — its outcome data is only meaningful once gates fail closed (Wave 0) and closures actually reach PHASE_7. The day-zero baseline is already captured, so nothing is lost by waiting. Overrides: pull LICENSE-INTEGRITY + RELEASE-CHAIN into Wave 1 if a release is imminent; Wave 3 has zero file overlap with Waves 1–2, so a second contributor can run it in parallel.
Five parallel deep-read audits (architecture/core, scanners/validators, workflow engine/personas, test suite/CI, docs-vs-claims) over the full source tree, plus dogfooding nablr's own MCP scan(health) and scan(security) against src/nablr. Read-only — no code changed; the auto-started remediation session REM-8332E323 was left untouched.
Caveats: dev HEAD moved during the audit (e1a21d3 → bcf0fcd, a concurrent session's browser-stepper fix — findings reflect bcf0fcd where noted). Three story worktrees are live under .nablr-worktrees/. Line numbers are accurate as of scan time and may drift. Dogfood scan wrote reports into src/nablr/… (itself Finding 3's path bug) — those files are untracked and safe to delete.