Everything visual, on a real phone. I have no browser (§9). The 390px work is measured and reasoned, not seen. Open all eleven routes at 390px before trusting them.
The rule that makes a draft milestone SETTLED (§3). It is the only rule in the app that claims success, and it is mine, not the supervisor's. If "PUSHED + COMPLETED + every declared gate PASS" is too generous or too strict, it is one function — stateFromRun in src/lib/drafts.ts — and one test.
Branch-derived milestones. Twenty of the sixty-four come from branches whose merge status is not recorded, so they all read IN_PROGRESS. AKH Peptides is 9 of its 12 milestones this way, and its DRAFT 0% is therefore a statement about *unknown branches*, not about the product. If that reads wrong, either delete them per project or ask discovery for a merged flag (§10.5).
Report-section milestones. Fourteen milestones are an agent's claim about its own work, and they are all NEEDS_REVIEW so they weigh on the denominator without ever helping the numerator. That is deliberate, and it is arguable.
The tier defaults on the Estate map. Each cites the manifest phrase behind it, but "Experimental" for Raglub and Bizi comes from "Phase 0 architecture" and "architecture for 25 products", which is my reading of your words, not your judgement.
The lint gate change (next lint → eslint .) and the two toolchain changes in §9. Confirm the rule set is the one you want.
The review inclusion rule. A run enters the inbox if it moved a branch, if it pushed with an unrecorded head, or if it wrote a report. That last clause is what puts the three reported runs in front of you even when their branch head equals their base.
The manifest says 14 repositories, the brief says 13 (§1). Something upstream disagrees with something else, and it is not for me to reconcile.
The unreadable-source screen. If BULGAR_ROOT is wrong on the worker, Dev Control now says so instead of reporting an empty estate. Worth seeing once deliberately, by starting it with a bad root, so the failure mode is familiar before it happens for real.
The eight SUPERVISOR_LOST runs between 14:01 and 16:52 on 2026-08-25 are still a real signal, as the previous agent noted. Dev Control now shows them as never-clean finishes, two of them having already pushed — but nothing has explained what killed those supervisors.
Exposure. Dev Control has no login. Confirm the tailnet boundary is actually enforced before anyone opens it from a phone on a mobile network. ---
Branch: agent/bulgar-dev-control-web/20260825T233251Z-6b20 Starting SHA: b401bdc ("Refresh fixtures: full estate, 28 runs, 3 reports, discovered branch facts") Developed against: BULGAR_ROOT=./fixtures — the real 17-component manifest, the real 14-repository branch discovery, 28 real run records and 3 real agent reports. The container cannot see /srv/bulgar, as expected and as designed.
V1's `src/app/` is not in this checkout. The brief says "src/app/ has Home, Estate, Agents, Review, Stats". At b401bdc on main the repository contains exactly one source file — src/lib/estate.ts (89 lines) — plus fixtures and config. V1's five surfaces exist on the agent branch agent/bulgar-dev-control-web/20260825T192754Z-0ed7, which was pushed but never merged into main, and its own report (fixtures/reports/20260825T192754Z-0ed7.md) is in the fixtures.
So "extend, do not rewrite" was applied to the only V1 artefact actually present:
src/lib/estate.ts — every original export is byte-for-byte unchanged: ROOT, GateStatus, ExitReason, RemoteCheckpoint, RunMeta, gate(), awaitsReview(), ageSeconds(). V2 additions are appended below a marked line and only narrow fields the fixtures already carry. A test asserts awaitsReview() still behaves exactly as V1 defined it, over all 28 real runs.src/app/ and src/components/ is new, because there was nothing there.I did not fetch or merge the V1 branch: the supervisor does all git, and I have no remote access.
| count | note | |
| Projects (one per repository) | 14 | |
| Runnable components | 17 | |
| Repositories | 14 | |
| Runs read | 28 | |
| Agent reports read | 3 | 25 of 28 runs have no report — that is real |
| Branches discovered | 57 across 14 repos | from discovered-estate.json |
The brief says "17 components across 13 repositories". The manifest contains 14 distinct repositories, and I did not reconcile that by dropping one. Counted from estate-manifest.json.components[].repository: sarafski7979/{raglub, bizi, bulgar-dev-control, bulgar-dna, bulgar-showcase, raw-podcast, louie-doherty, superfit-gym-app, stella-h, sponsorme, shopify-design-louie, bulgar-base} (12), kristiyanpts/bulgar-studio, diannescotland/akh-peptides. A test pins the app's count to the manifest's, so if the manifest changes the interface follows it rather than a number I typed.
| project | tier (default) | status | product | milestones | engineering readiness | runs |
| Bulgar Dev Control | INFRASTRUCTURE | NEEDS REVIEW | DRAFT 0% | 1 | 5 of 6 | 3 |
| Bulgar Studio | CORE BULGAR | NEEDS REVIEW | DRAFT 0% | 17 | 3 of 9 | 3 |
| Bulgar DNA | CORE BULGAR | NEEDS REVIEW | DRAFT 22% | 18 | 6 of 6 | 8 |
| Bulgar Showcase | CORE BULGAR | NEEDS REVIEW | DRAFT 33% | 3 | 3 of 5 | 2 |
| RAW Podcast | CLIENT | SETTLED ON A DRAFT ROADMAP | DRAFT 100% | 1 | 4 of 5 | 10 |
| Louie Doherty | CLIENT | SETTLED ON A DRAFT ROADMAP | DRAFT 100% | 1 | UNKNOWN | 2 |
| Raglub | EXPERIMENTAL | IN PROGRESS | DRAFT 0% | 1 | UNKNOWN | 0 |
| Bizi | EXPERIMENTAL | IN PROGRESS | DRAFT 33% | 3 | UNKNOWN | 0 |
| AKH Peptides | CLIENT | IN PROGRESS | DRAFT 0% | 12 | UNKNOWN | 0 |
| Superfit Gym App (standalone) | CORE BULGAR | NEEDS REVIEW | DRAFT 0% | 2 | UNKNOWN | 0 |
| Stella H | CLIENT | NOT STARTED | DRAFT 0% | 1 | UNKNOWN | 0 |
| Sponsorme | CLIENT | SETTLED ON A DRAFT ROADMAP | DRAFT 100% | 1 | UNKNOWN | 0 |
| Shopify Design Louie | ARCHIVED | IN PROGRESS | DRAFT 0% | 2 | UNKNOWN | 0 |
| Bulgar Base | INFRASTRUCTURE | NOT STARTED | DRAFT 0% | 1 | UNKNOWN | 0 |
Nine of fourteen projects have UNKNOWN engineering readiness. That is the honest answer: eight have never been run at all, and Louie Doherty's only two runs both ended in INFRASTRUCTURE_FAILURE, so no gate value on them describes the product.
13 of 17 components declare a way to be previewed; 4 have none.
| state | count | which, and why |
| LIVE PREVIEW | 0 | nothing is running: no run has exit_reason: RUNNING |
| LOCAL / PRIVATE PREVIEW | 1 | bulgar-dev-control/web — the manifest note says "tailnet access only" |
| AVAILABLE ON DEMAND | 11 | a start_command exists. Only bulgar-dna/web is proven to start (run 20260825T121535Z-794e recorded preview: RUNNING); the other 10 say "no run has ever started it, so it is not proven" |
| PREVIEW LIMITED — SAFE DATA ONLY | 1 | akh-peptides/backend — services_required: [postgres, api]; a throwaway stack only, never a hosted database |
| NO PREVIEW | 4 | raglub/core, bizi/platform, bulgar-base/docs (no start_command); shopify-design-louie/theme (needs a store token the worker will not hold — the manifest's own PREVIEW_LIMITED_NO_SAFE_DATA) |
| BROKEN PREVIEW | 0 | no run has recorded a failed preview |
A preview link is only ever rendered when a run is `RUNNING` and its `preview` field is `RUNNING`. A declared start command is not a running app. A test asserts that no component outside LIVE_PREVIEW exposes a port.
louie-doherty is recorded as a LIVE SITE in its manifest note, and that fact is shown as a settled milestone — but the manifest carries no URL field, so no link is offered. I did not invent one. If you want that link, the manifest needs a live_url (see §10).
PRODUCT PROGRESS = settled milestone weight / total milestone weight. Nothing else. There is no code path anywhere in this app that turns a commit count, a file count, a line count or an elapsed time into a percentage, and a test proves it: adding 25 synthetic passing runs to DNA does not move its number by one point.
If a project has no milestones there is no percentage — it renders UNKNOWN, not 0%.
14 draft roadmaps, 64 draft milestones. 0 confirmed — confirming is Ivan's action and I did not perform it. Every draft is labelled DRAFT; the percentage renders as DRAFT 62%, never bare.
Where the 64 came from, and the exact rule for each:
| source | count | rule | state it produces |
branch | 20 | every branch in discovered-estate.json that is not the default and not agent/, backup/ or rescue/ | IN_PROGRESS, with the caveat "whether it is merged is not recorded" |
manifest-note | 15 | 12 quoted phrase rules (LIVE SITE, Proven on Debian 12, SCAFFOLD ONLY, Parked., INTEGRATION-ONLY, NAME MISMATCH, THIRD GitHub owner, COLLABORATOR REPO, …) plus one for services_required | as stated per rule |
report-section | 14 | the deliverable sections of an agent report (process sections such as "Approaches that FAILED" are excluded) | NEEDS_REVIEW if the run pushed, else IN_PROGRESS — never SETTLED |
run-task | 11 | runs grouped by task, attempts merged (SPRINT A and SPRINT A (attempt 1) are one milestone) | best outcome across attempts, see below |
manifest-finding | 4 | each findings.* entry that names the repository or application | SETTLED if status: RESOLVED, else NEEDS_REVIEW ("a decision for a human") |
Runs whose task is "started via bulgarctl" produce no milestone — 13 of the 28 runs carry that non-descriptive task, and inventing a milestone from them would be inventing a denominator.
A run-task milestone is only SETTLED when all four of these are true: exit_reason: COMPLETED, agent is not AGENT_FAILED, remote_checkpoint: PUSHED, and every gate the manifest declares reported PASS. A gate that did not run is not a pass. INFRASTRUCTURE_FAILURE yields BLOCKED; SUPERVISOR_LOST yields NEEDS_REVIEW, because the record is incomplete.
Result across the estate: 9 SETTLED, 23 NEEDS_REVIEW, 22 IN_PROGRESS, 5 BLOCKED, 5 NOT_STARTED.
A second, separate number: checks passing / checks declared, from install, test, typecheck, lint, build, preview.
*_command: null) is NOT_PRESENT and is excluded from the denominator — it is never counted as passing. Shown as "N not present, excluded".INFRASTRUCTURE_FAILURE, or whose install failed, are excluded entirely — their gate values describe the harness.Three projects read DRAFT 100% off a single manifest-note milestone. Rather than let that pass as a finished product, the card says "thin denominator: too few milestones for this number to mean much", the status reads SETTLED ON A DRAFT ROADMAP (visually dashed), and the home counter shows SETTLED 0 with "3 more settled on a draft roadmap" underneath. Nothing counts as settled until Ivan confirms the roadmap it was measured against.
Human state lives in `${BULGAR_ROOT}/dev-control/human-state.json` — Dev Control's own file. It never touches a product repository or a product database. Writes are atomic (temp file + rename), so a crash cannot half-write a decision.
It holds: milestone states, titles, weights and tombstones; Ivan's own milestones; per-project priority, tier, note and roadmap confirmation; review marks; and an event log (last 500) that feeds the Activity page.
Generated drafts can never overwrite it. mergeMilestones() lets a draft fill only an id human state does not carry; a milestone Ivan removed stays removed; a milestone Ivan added exists with no evidence behind it and says so. Two tests cover this, one of them by regenerating the whole snapshot twice against the same human state.
Ivan can, from the UI, with no JavaScript: set a milestone state, set a weight (1–5), remove or add a milestone, confirm or reopen a roadmap, set priority, set tier, save a note, mark or unmark a review. All of it verified over real HTTP against the running server.
Nothing is rendered that is not in this table. /run/<id> prints the raw meta.json so any value can be checked against its source.
runs/<id>/meta.json| displayed | field |
| task / activity title / milestone title | task |
| project, component, working dir | repository, component, working_directory |
| exit chip | exit_reason |
| agent chip | agent |
| install chip | install |
| four gate chips | gates.test / .typecheck / .lint / .build |
| "a gate could not run" blocker | gate_note |
| checkpoint chips | local_checkpoint, remote_checkpoint |
| preview chip and link | preview, preview_port (link only when exit_reason: RUNNING) |
| branch, base SHA, head SHA | agent_branch, base_sha, remote_sha |
| "the branch moved" | remote_sha !== base_sha, both required, else UNKNOWN |
| started / ended / "N ago" | started, ended |
| cpus, memory, image, container, pid, runtime set | cpus, memory, image, container, supervisor_pid, runtime_set |
| OPEN BRANCH | https://github.com/{repository}/tree/{agent_branch} |
| VIEW CHANGES | https://github.com/{repository}/compare/{base_sha}...{remote_sha} |
estate-manifest.json| displayed | field |
| project name, display name | repository (display name is typographic only — dashes to spaces, a small acronym list for DNA/AKH/OS/AI/API/UI/RAW) |
| one-line description | opening sentences of notes, verbatim, never rewritten |
| family chips | project_family |
| check declared / NOT_PRESENT | install_command, test_command, typecheck_command, lint_command, build_command, start_command |
| preview port, start command | preview_port, start_command |
| PREVIEW LIMITED — SAFE DATA ONLY | services_required, credential_requirement |
| NO PREVIEW (store token) | credential_requirement: STORE_TOKEN_REQUIRED |
| LOCAL / PRIVATE PREVIEW | notes matching /tailnet/ |
| estate tier default | quoted notes phrase, else project_family, else UNASSIGNED |
| findings, "authoritative copy — needs decision" | findings.* including canonical, evidence, action_taken |
| "deliberately not in the estate" | not_in_estate[] |
| footer "manifest generated" | generated |
discovered-estate.json| displayed | field |
| branch list, branch count, default head | branches, branch_count, default_head |
| agent / wip branch counts | agent_branches, wip_branches |
| branch milestones | branches minus default/agent/backup/rescue |
reports/<id>.md or runs/<id>/report.md| displayed | source |
| "what to look at" on a review item | the report's own "What a human must review" list |
| NEEDS IVAN items of kind REPORT | the same list |
| blockers of kind report | "Remaining blockers and open questions" |
| "What changed" on Activity | the report's deliverable section titles |
| report-section milestones | the same section titles |
| full report page | the file, rendered by a renderer that builds React elements only — no innerHTML anywhere in the app |
dev-control/human-state.jsonMilestone overrides and additions, priority, tier, notes, roadmap confirmation, review marks, and the "your decision" entries in Activity.
| where | when | why it is not a zero |
| Engineering readiness | no run, or every run failed at install / infrastructure | "0 of 6" would read as six broken checks; the truth is nothing was measured |
| Product progress | no milestones | no denominator means no percentage |
| A gate chip | the gate is declared but the run recorded nothing | UNKNOWN and NOT_PRESENT are different facts and look different |
| "What changed" on a review item | PUSHED with no remote_sha (4 real runs) | a push with an unrecorded head is not the same as a push that changed nothing |
| "Last update" | no run and no decision (Raglub, Bizi, AKH, Stella H, Sponsorme, Shopify, Base, Superfit) | |
| "Ended" on a run | ended missing though the run is not RUNNING (3 real runs) | the review card falls back to "started N ago · no end time was recorded" |
| Agent liveness | no signal file (all 28 runs today) | claiming an agent is working is a stronger claim than admitting we cannot tell |
| Estate tier | no manifest evidence | shown as UNASSIGNED — needs your decision |
| The whole home page | estate-manifest.json cannot be read at all | "The estate cannot be read" replaces every count, because "0 projects" and "the source is unreadable" are different facts and must not print the same |
| Priority | Ivan has not set one | "/next" says so instead of ranking by an opinion |
ACTIVE_WORK_CONFIRMED requires at least two of four independent signals of change: checkpoint TREE change, workspace writes, child process activity, CPU progression. NO_ACTIVE_WORK is only stated when all four are known and none is changing. Otherwise: UNKNOWN.
A checkpoint COUNT is not a signal and there is no field for one — an agent that checkpoints on a timer produces checkpoints forever without doing work. A test feeds five identical checkpoint tree SHAs and asserts the signal reads STATIC and the verdict stays UNKNOWN.
Signals are read from an optional runs/<id>/liveness.json. The supervisor does not write that file today. Until it does, every run shows UNKNOWN and the Agents page explains why in plain words. That is the honest state, not a bug — and it is the one place where a small supervisor change would light up a whole surface (§10).
Active agents right now: 0. No run in the record has exit_reason: RUNNING.
INFRASTRUCTURE_FAILURE, 1 gate_note (test NOT_RUN: needs a local DATABASE_URL…), and 6 from the two reports that recorded blockers.kristiyanpts/bulgar-studio :: superfit-gym-app and sarafski7979/superfit-gym-app :: web carry an AUTHORITATIVE COPY — NEEDS DECISION banner listing both copies and quoting findings.duplicate_application ("NEITHER COPY DELETED, MERGED OR MIGRATED"). A test fails if either copy stops being flagged or if the decision milestone ever leaves NEEDS_REVIEW by itself.| gate | command | status |
| install | npm ci | PASS (npm ci --dry-run validates against the unchanged lockfile) |
| test | npm test | PASS — 58 tests, 0 failures |
| typecheck | npx tsc --noEmit | PASS |
| lint | npm run lint | PASS |
| build | npm run build | PASS — 11 routes, all ƒ dynamic |
Tests are TypeScript executed by node --test with type stripping, and run against the real fixtures, not mocks. They are honesty tests: the vocabulary is never widened; SUPERVISOR_LOST is never a clean finish; an infrastructure failure never supplies product evidence; a component with no run reads UNKNOWN; progress cannot be moved by adding runs; a report claim is never SETTLED; human state always wins; the operation set contains nothing that merges, pushes, deploys or executes; no identifier can escape BULGAR_ROOT; and every one of the 28 real run ids resolves.
Two changes to the repository's own tooling (both needed for the gates to be real):
"lint": "next lint" → "lint": "eslint ." with a new flat eslint.config.mjs. Next 16 removed `next lint` — npx next lint now reports "no such directory: /work/lint". The previous Dev Control agent reached the same conclusion (report …0ed7.md §11.7). The rule set is eslint-config-next flat, and it is yours to confirm."type": "module" in package.json and allowImportingTsExtensions in tsconfig.json, so node --test can run the TypeScript library modules directly. next build is green with both.next build also rewrote tsconfig.json itself — jsx: preserve to react-jsx, plus the .next/types includes and the next plugin. Those three edits are the tool's, not mine, and are committed as it made them. The previous Dev Control agent hit the same thing.npm test pins BULGAR_ROOT=./fixtures so the gate is deterministic wherever the supervisor runs it, rather than depending on an environment variable being set for it./run/<id> — fixedThe brief reports /run/<id> returning not-found for a real run id. There was no /run route in this checkout, so it is implemented correctly rather than patched: the id is validated as a path segment, the run directory is read directly, and notFound() is only called when the file truly is not there. A test walks all 28 real ids and asserts each resolves and agrees with its own directory name; hostile ids (../../etc/passwd, /etc/shadow, embedded newlines) resolve to null.
Mobile is the primary device and the home order is the required one: NEEDS IVAN · ACTIVE AGENTS · PROJECTS · NEEDS REVIEW · BLOCKED · RECENT ACTIVITY, with the counts strip above it. Cards everywhere; the only tables in the app are inside agent reports, and those are wrapped in a horizontal scroller.
Checks performed at a 390px budget (390 − 32 shell − 32 card = 326px of content):
width in the codebase is the viewport meta.overflow-wrap: break-word globally, overflow-wrap: anywhere on every monospace element, and a .chip.wrap variant for branch names and repository paths.kristiyanpts/bulgar-studio :: superfit-gym-app at ~358px on the Estate page. Re-run afterwards: nothing over 300px.<form> + server action: the whole app works with JavaScript off, which was verified by driving the real HTTP endpoints with curl.There is no browser in this container. chromium, chromium-browser, google-chrome and firefox are all absent, and neither Playwright nor Puppeteer is installed. Installing one would mean pulling a browser into an agent image, which is not my call. So the mobile work above is reasoned and measured, not seen — the same limitation the two other agents recorded this week. A human with a phone should open all eleven routes before this is trusted visually. This is the first item in §12.
BULGAR_ROOT. There is no token, no key, no credential, no .env read, and nothing from the manifest's credential fields is rendered beyond the *requirement* label (STORE_TOKEN_REQUIRED, DEV_DATABASE_REQUIRED).child_process import, no exec, no spawn, no Docker client, and no remote terminal of any kind. A grep for those is in the report trail and comes back empty.BULGAR_ROOT, via one server action, behind a fixed enumerated operation set (13 operations). Anything else is rejected before it reaches the reducer. Free text exists only as stored data (a milestone title, a note): control characters are stripped, length is capped at 200, and it is never used as a path, a command or an argument... is rejected, and the resolved path is asserted to stay under BULGAR_ROOT.Rather than invent them:
louie-doherty is a LIVE SITE with no address, so Dev Control cannot offer the one link an owner would most want.discovered-estate.json. Twenty milestones are drafted from branches that may already be merged; today the caveat says so on every one of them.test, while a run fifteen minutes earlier had proved four gates. Replaced with per-check "most recent run that recorded *this* check", each citing its own run id on the page.SETTLED ON A DRAFT ROADMAP (dashed, distinct), a "thin denominator" warning under any percentage with fewer than three milestones, and a home counter that keeps confirmed and draft-settled apart.force-dynamic; a reload is the refresh.git push.Ranked by how much I want a second pair of eyes.
stateFromRun in src/lib/drafts.ts — and one test.DRAFT 0% is therefore a statement about *unknown branches*, not about the product. If that reads wrong, either delete them per project or ask discovery for a merged flag (§10.5).next lint → eslint .) and the two toolchain changes in §9. Confirm the rule set is the one you want.BULGAR_ROOT is wrong on the worker, Dev Control now says so instead of reporting an empty estate. Worth seeing once deliberately, by starting it with a bad root, so the failure mode is familiar before it happens for real.New: src/lib/{manifest,read,human-state,ops,report-parse,drafts,derive,load}.ts; src/app/{layout,page,actions,globals.css} and the ten route folders; src/components/{ui,project-card,milestone-list,review-card,markdown}.tsx; test/{fixtures,vocabulary,readiness,progress,human-state,review,liveness,runs-and-paths,previews,estate,description}.ts; eslint.config.mjs.
Changed: src/lib/estate.ts (appended only, V1 exports untouched), package.json (type, scripts), tsconfig.json (allowImportingTsExtensions), .gitignore, README.md.
Not changed: every file under fixtures/, and every other repository on this machine.