← Work

Work / Tool Prototype

Project Monitor: a dashboard that checks what my projects claim

Project Monitor is a local dashboard for projects built with Claude Code. It shows tasks, roadmap, tests and Git state, and checks the status each project declares against its commit history, flagging a task marked complete that no code change supports. It is read-only and runs on localhost.

Demonstration data. Invented data.

One of my demo projects says it is 100% complete. Its dashboard card says so too, in a confident green bar. Project Monitor then points out that the “offline cache” task behind that figure has no code change supporting it, that the recorded test results are older than the latest source commit, and that the activity log has been quiet for three days. That is the point of the tool. I run many projects with Claude Code, and a status page that only repeats what a project says about itself cannot tell me when the claim has drifted from the code.

The Project Monitor portfolio page: three invented projects with health states Good, Attention and Not configured, and a count of open drift findings
Figure 1. The portfolio page, shown with three projects using invented data. Each card carries a derived health state, progress, test results and a drift line. The summary at the top counts open findings and says how many projects report “not applicable” rather than clean.

At a glance

  • Status: Prototype.
  • Built: 17 August to 3 October 2026, 86 commits.
  • Platform: a Node.js service and a browser page, bound to localhost.
  • Stack: Node 22 or later, Express, simple-git, Ajv for JSON Schema validation, plain JavaScript in the browser with no build step.
  • Built with: Claude Code.
  • AI inside the product: no. The monitor makes no AI calls. Claude Code is the tool that writes the status files it reads.

Three projects, three verdicts

Video 1. A 55-second run through three invented projects: a clean one, one whose declared status disagrees with its repository, and one with no status tracking at all. Watch the Harbour Tides project: its 100% progress figure is contradicted by the findings beneath it.
Text version of the video
  1. The portfolio page lists Orchard Notes (Good), Harbour Tides (Attention) and Ledger Lite (Not configured), with a count of open drift findings.
  2. Orchard Notes: the overview, then the Assurance tab. The checks that had something to examine report none detected. The two with nothing to examine report not applicable.
  3. Harbour Tides: the overview declares 100% complete, yet the current task is flagged as not corroborated. The Assurance tab lists five findings, each with what was declared, what was observed and when it was checked.
  4. The Repository tab shows the commits behind those findings.
  5. Ledger Lite has no status directory. The overview says so plainly and reports all six checks as not applicable.

When several projects are built by AI sessions, the record of progress is written by the same sessions that do the work. Tasks get ticked without a matching commit. Activity logs go stale. A session summary says more than the repository can show. Project Monitor is one local page that answers three questions for every project: what is happening, what is finished, and whether the first two answers agree with Git.

Observed, declared, derived

Every value on screen carries one of three labels. Observed means it came from the repository, Git or the session transcripts, so nobody had to assert it. Declared means it was written into the project’s .project-status/ folder, so it is a claim. Derived means the monitor computed it by a fixed rule. A declared value that fails a cross-check is flagged where it appears instead of being shown as fact.

The cross-checks are six drift checks that run on every refresh:

  • D1 finds completed tasks with no supporting source commit.
  • D2 finds an activity log that has fallen behind the commit history.
  • D3 compares session claims with the repository.
  • D4 checks that the declared current task is consistent with recent commits.
  • D5 finds test results older than the code they describe.
  • D6 checks requirements against tasks. It is labelled a consistency check, not corroboration, because both of its inputs are declared.

Here is the clean case. Orchard Notes is an invented project whose tasks, commits and recorded test run all agree.

Orchard Notes assurance page: six drift checks, four reporting none detected and two reporting not applicable, tests passing 12 of 12
Figure 2. Assurance view of the clean project, which uses invented data. The two “not applicable” rows are deliberate: with no session records and no requirements, those checks have nothing to examine, and the page says that is not the same as passing.

Now the drifting case. Harbour Tides declares every task complete.

Harbour Tides overview: 100% progress and a current task flagged as declared but not corroborated
Figure 3. The overview of the drifting project, which uses invented data. Progress reads 100% because progress is arithmetic over declared tasks. The current-task card is marked “declared, not corroborated (D4)” and shows the finding underneath.

The Assurance tab then shows each finding in the same shape: what was declared, what was observed, and when the check ran.

Harbour Tides assurance page listing findings D1, D2, D4 twice and D5, each with declared and observed lines
Figure 4. Five findings on the drifting project. D1 reports that task T004 is declared complete with no supporting commit. D5 reports test results that predate a later source commit. Each finding carries a plain note on what its check can and cannot prove.

The Repository tab shows the commits underneath, so I can see the evidence rather than take a finding on trust.

Harbour Tides repository page: branch, working tree counts and a table of recent commits with task ids in the subjects
Figure 5. The observed side. The newest commit is named after T004 but touches only the status files, which is why D1 does not count it.

A project with no status folder at all is a supported state too.

Ledger Lite overview: a project with no .project-status directory, shown as not configured with no invented progress figure
Figure 6. A project with no status tracking is a supported state, not an error. Branch, commits and structure are still shown. Progress is not estimated.

A declaration cannot vouch for itself

The first version of D1 accepted any commit message that named a task. Then I looked at the real history of the monitor’s own build: every commit touched .project-status/, and three of the first six touched nothing else. A task could be “corroborated” by the very commit that declared it complete.

Build decision. D1 now counts only commits that change source files. A commit touching nothing but .project-status/ or root-level documentation corroborates nothing. The decision record says I checked the change against the real history: all fifteen tasks complete at that point were still supported by a source commit, so the fix closed the hole without inventing findings.

The same attitude shaped the display. A progress percentage is never typed in; it is computed from task states by one method, and rounding is clamped so that 100% means everything is finished and 0% means nothing is. A project with nothing to measure shows a dash, not zero.

A read-only boundary with tests behind it

The monitor reads other projects, so the worst thing it could do is change one. I treated that as a design constraint with a test behind it.

Build decision. No module under server/ imports a file-write API or child_process. The only thing that spawns a process is the Git library, and my adapter allows only read subcommands. Git runs with its hooks redirected to a directory that must never exist, and with optional locks off, so that even git status leaves the index alone. One test plants executable hooks in a fixture repository and confirms none fires. Another builds a full snapshot and confirms the monitored project is unchanged afterwards.

Test and build status come only from files the project’s own test process wrote. The monitor never runs a monitored project’s tests, scripts or hooks.

The monitor monitors itself

I built the monitor with Claude Code, task by task against a written plan. The project records 88 tasks, all complete, across eight phases, and 40 decision records. Each task has acceptance criteria, and a task is closed with evidence written next to it.

The monitor was also its own first test subject. It has kept its own .project-status/ since the first commit. The rule I set was that if the drift detector never fired during its own development, the checks should be assumed broken. So the project includes a recorded run where two discrepancies were planted on a scratch branch, one behind a deliberately misleading commit message, and the checks caught both.

Build decision. Six commits referred to the wrong task id. I left them in place and recorded the mislabelling instead of rewriting history, because tidying the record would be exactly the behaviour the tool exists to detect. It also shows the ceiling of D1: it confirms that a reference exists, not that the work matches it.

A second decision record changed its own reasoning. I had assumed hooks that write status mechanically were needed because the AI forgets the protocol over long sessions. Measured against this build, currency did not degrade. What went wrong was accuracy in mechanical fields: 146 timestamps an hour out, one out of order, six wrong task ids. The decision stayed, but its justification changed to accuracy.

Under the hood

LayerWhat I used
InterfacePlain JavaScript in the browser, no build step; renderers are pure functions that return HTML strings
RuntimeNode.js 22 or later, Express, bound to localhost
Git accesssimple-git, restricted to read subcommands
ValidationAjv, compiling ten JSON Schemas at start-up
DataFiles only: each project’s .project-status/, Git history and session transcripts, read and never written
AINone inside the monitor
Tests371 tests, 370 passing with one skipped in the recorded run of 3 October 2026
Architecture: three read-only sources feed status, Git and session readers, a snapshot builder runs the drift checks and derives health, and a localhost API serves a browser app
Figure 7. How a project is read. Everything flows one way, from the monitored project to the screen.

The server compiles all ten JSON Schemas at start-up and refuses to serve if they fail, because a monitor that cannot validate would present malformed status as valid. Each project is addressed by an id from config/projects.json, and no endpoint accepts a filesystem path, so a request cannot name a directory I did not already register. Refresh is by polling, not file watching, and the number of Git processes per refresh is bounded.

Across the three example projects, the clean one comes out Good, the drifting one flags D1, D2, D4 and D5, and the untracked one reports Not configured with all six checks not applicable.

What it shows

Project Monitor puts a second opinion beside every project’s own account of itself. Because the limits of each check sit next to the check, on the same screen, the pages claim no more than a heuristic can show. Corroboration raises the cost of an inaccurate declaration, and the observed, declared and derived labels make it clear which numbers are evidence and which are claims. It gives me one local page to see what is happening across my Claude Code projects and whether the record matches the code.