Portfolio case study
I build the systems that make unclear requirements impossible.
NoPM is a lite project management tool your agent operates over REST, CLI or MCP. Governance (immutable audit, scoped tokens, four-eyes approval) is built into the data layer. Built solo in eight waves with AI agents.
The thesis
Governance belongs in the data layer.
Every mutation calls writeAudit() once, fail-closed. Postgres triggers keep the log append-only, so even a service key cannot rewrite history. The UI uses the same API your agent does.
“One surface for humans and agents: 121 documented endpoints, OpenAPI,
llms.txt, scoped tokens. No bypass.”
The wedge
Three things most PM tools can't claim together.
- agent-native
- Every action is a REST endpoint: scoped
resource:actiontokens, OpenAPI, anllms.txtindex, anopmCLI and MCP. Any agent drives it the same way the console does. - governed
- Every mutation writes one append-only audit row. Deletes need four-eyes: a different checker approves, enforced by a Postgres
CHECK. Even a service key can't rewrite history. - governed triangle
- Timeline, resource and budget changes pass one audit and sign-off gate. NoPM records who changed what and who approved it. It is not a scheduler, time tracker or ERP.
What was built
Governance spine, minimal registers.
The governed record beside Jira, Slack and Teams.
$ nopm tasks rm task_8f3a \
--reason "superseded by revised cutover plan"
⏸ delete requested — pending a checker's
approval (four-eyes)
$ nopm approve req_2c91
✓ approved & committed
# a different person's token: task:delete:approve
# one immutable audit row written
$ nopm audit --limit 1
actor_label action target
checker (agent) task.delete task_8f3acreated_at actor_label action target
2026-06-08T… checker (agent) task.delete task_8f3a- Governed Kanban (drag = audited) and Gantt with rebaseline gate
- Trust Center: immutable audit log, provenance, tokens
- Change control, notifications, deck export (PDF / PPTX / HTML)
- Link invites: role-capped, revocable, audited
- Workspace, Project, Milestone, Task (maker-checker delete)
- Risk and Issue registers with scoring
- Clashes: schedule, resource and dependency conflicts
- Resources, allocation, cost rate, capacity
- Budget: planned / committed / actual / EVM (CPI, SPI, EAC, VAC)
- Sprints: burndown and velocity
- BYOK assistant (your own API key, AES-256-GCM at rest, ephemeral scoped token). NoPM ships no model.
- Versioned documents and charters
Proof metrics
Proof metrics.
These counts are generated from the RTM, the test run and the API route tree. CI fails if they drift. Tests ask first: can a maker self-approve?
Invariants (stated, not counted)
- One immutable audit row per mutation.
- No bundled LLM. No model key ships.
Reviewed, then closed
Ten defects found, then closed.
A 2026-09-06 review confirmed ten defects. Two sprints closed or narrowed them the same day.
All ten defects
- Finding (morning)
- Eleven mutating routes wrote no audit row, including BYOK key set and delete.
- State (evening)
- Closed. A coverage test fails the build on any unaudited write. The key lifecycle is audited without the key.
- Finding (morning)
- Ordinary writes persist before the audit row; only two flows are transactional.
- State (evening)
- Open by decision. The Trust Center says so.
- Finding (morning)
- One scope both raised and decided change requests. A token requester made the DB check vacuous.
- State (evening)
- Closed. change:decide is a separate scope agents never hold. Requester and decider are typed identities, checked in the API and the database.
- Finding (morning)
- Unlocking a frozen baseline needed no reason.
- State (evening)
- Partly closed. A reason is required in the API, dialog and CLI. The same person can still unlock.
- Finding (morning)
- Audit rows had no client origin or request id.
- State (evening)
- Partly closed. Every row records the declared client and a request id, including rows written inside database functions. No hash chain yet.
- Finding (morning)
- No record could point at a Jira key or Slack thread.
- State (evening)
- Closed. Tasks, projects, risks, issues and decisions carry source, external_ref and external_url.
- Finding (morning)
- No public page said NoPM complements Jira, Slack or Teams.
- State (evening)
- Closed. README, landing, this page and the product doc say it.
- Finding (morning)
- Lock and rebaseline had no navigation home.
- State (evening)
- Closed. Baselines lives under Governance. The demo page moved to My Work.
- Finding (morning)
- The audit immutability triggers had no regression test.
- State (evening)
- Closed. A migration-text test fails if any migration drops, disables or bypasses them.
- Finding (morning)
- Tool and metric counts drifted between surfaces.
- State (evening)
- Closed. Counts are generated and CI-checked.
Review, sprint receipts and re-run are in the repo under docs/research and sprint-log.
Stack & method
Production stack, built with AI agents.
Technology
- Next.js 16 App Router, TypeScript, Tailwind v4
- Supabase Postgres, RLS, Auth, Storage
- Vercel deploy, Cloudflare DNS
- vitest 7547 tests, governance first
- jsPDF / pptxgenjs board-pack exports
Discipline
- Requirement traceability matrix (RTM), checked in CI
- Architecture decision records (ADRs)
- Sprint log per wave
- AI multi-agent workflow, solo build
See the governance live.
Every endpoint is documented. Every mutation is audited.