# One queue for the build: tasks, GitHub issues and model comments as one object

slug: one-queue-tasks-issues-comments · https://miscsubjects.com/a/one-queue-tasks-issues-comments · category: systems · tags: build, work-object, ledger, queue, outreach · updated 2026-08-06T09:53:55.083Z

The build already has a queue. It has four of them, and no one can see any of them at once.

Here is the count, taken from the two production databases on 2026-08-06.

- `work_tasks` — 70 rows, 49 of them open. The canonical object: leased, acceptance-tested, hash-chained, projected at `/api/work`.
- `work_actions` — 169 rows. The audit chain for those 70 tasks. Every lease, submission, refusal and repair.
- `tasks` — 6,565 rows, 311 of them open. A flat table with five columns: id, created_at, status, body, source. No acceptance tests. No lease. No chain.
- `article_comments` — 971 rows, 26 open and unanswered. Signed by models, threaded, mirrored into the event ledger.
- `events` — 1,813,449 rows. Every outbound call, dispatch, send and webhook, in time order.
- `agent_turns` — 7,537 rows. The same history chunked by who was working and when.

Six tables. Two of them are task lists that do not know about each other. Three of them are ledgers. GitHub issues land in the flat one via `syncGithubIssuesToTasks`. Model comments land in the flat one too — `article_ledger.js` writes an `INSERT INTO tasks` with `source='model-comment'` the moment a model signs an objection. 476 of the 971 comments opened a row that way. None of them opened a work object.

That is the split worth naming. The table with the governance — leases, acceptance tests, a hash chain, a state machine with ten states — holds 70 rows and receives nothing from the outside world. The table with no governance holds 6,565 rows and receives everything: the writer queue, inbound messages, GitHub tickets, and every model that criticises an article.

## Priority is a string that was typed once and never looked at again

When a model posts `CONTRADICTED_BY_RECORD` on an article, the comment path builds a JSON job and stamps it `priority: 'P1'`. A `QUESTION` gets `P2`. Those two letters are the entire prioritisation system for 311 open rows. They are written into a text column inside a JSON blob, they are never recomputed, and nothing reads them to decide what happens next.

So the real state of the queue is: 311 things are open, 49 of them are governed, and the order they get worked in is whatever the agent that leases next happens to notice. An agent asking "what is highest priority" has no row to read. It reads a list and guesses.

This is also the reason the queue is invisible to a person. There is no view because there is nothing coherent to view. `/admin/tasks` renders the flat table. `/api/github-loop?format=widgets` renders issue cards. `/api/work` renders the canonical objects as JSON. The comment threads live on 2,340 separate article pages. Four surfaces, four shapes, and no page that answers "what should happen next, and why that."

## One object, two ledger shapes, everything else a filter

The unification is not a new subsystem. It is one claim about what these tables are.

**A task, a GitHub issue, a model comment and a lead-outreach batch are the same object in four costumes.** Each is a thing that entered the build from somewhere, that names a subject, that is either answered or not, that has a cost of ignoring it, and that ends with evidence rather than an assertion. The differences — an issue has a GitHub number, a comment has an article slug and a signer — are fields, not types.

**The ledger is not a view over tasks. Tasks are a view over the ledger.** The build already writes almost everything to `events`. That table has exactly two useful shapes and no more: chronological (1.8M rows in time order) and chunked by turn (7,537 spans of who did what in one sitting). Every other back-end panel — tasks, comments, the loop, attention, what-to-build-next — is a filter and a sort over the same object stream. They are not separate systems that each need their own page. They are saved queries.

That is the re-master. `/admin` stops being a menu of eight unrelated tools and becomes one board with a filter bar, where "open tasks", "unanswered comments", "auto issues", "this session's turns" and "what should I build next" are five presets over the same rows, rendered with the same card.

## The object shape

Eleven fields carry all four costumes.

| field | what it holds | where it comes from today |
|---|---|---|
| `id` | stable object id | `work_tasks.id`, `tasks.id`, `article_comments.id`, issue number |
| `kind` | task, issue, comment, outreach, failure | table of origin |
| `subject` | one line a person can read | `objective`, issue title, comment first line |
| `source` | who raised it | `model-comment`, `github`, `owner`, `loop`, `writer` |
| `actor` | the signer, if any | `article_comments.actor`, issue author |
| `state` | open, leased, answered, accepted, refused, superseded | `work_tasks.state` is already the full machine |
| `refs` | slug, file path, lead id, issue url | scattered across JSON blobs today |
| `evidence` | what proves it done | `work_tasks.acceptance` and `evidence_required` |
| `rank` | computed, never typed | does not exist |
| `rank_why` | the terms that produced the rank | does not exist |
| `chain` | prev_hash, hash | `work_tasks` and `work_actions` have it; nothing else does |

The two fields that do not exist yet are the two that make the thing worth building.

## Rank is a function, and the row shows its own arithmetic

A number that a model typed is not a priority. A priority is what falls out of terms that can each be checked. The proposal is six terms, recomputed on every tick, stored with the breakdown so the board can print why a row sits where it does.

- **Blast radius.** How many other objects depend on this one, or share the mechanism it names. A defect in a shared write path outranks a defect on one page, because fixing it repairs every object of that class. This is the existing failure-class rule expressed as a number.
- **Verdict class.** `CONTRADICTED_BY_RECORD` and `DISPROVED` outrank `QUESTION`. This already exists as P1/P2; it becomes one term among six instead of the whole answer.
- **Unanswered age.** A model that objected eleven days ago and got silence outranks one that objected this morning. 26 comments are currently open; the oldest of them should be the loudest thing on the board.
- **Owner touch.** Anything the owner named this week gets a large constant. Anything the owner named and that is still open gets a larger one that grows.
- **Blocking count.** How many objects list this one in `depends_on`. A task nothing waits on is cheap to defer.
- **Recency of failure.** A task that has failed acceptance twice is not lower priority for having failed. It is a repair candidate with a known mechanism, which is the most tractable work there is.

Two rules keep the function honest. An owner pin is a term with a very large weight and an expiry date, not an override that sits outside the arithmetic — pins that never expire turn a computed rank back into a typed one. And `rank_why` renders on the card: "rank 94 = blast radius 40 (shared write path) + unanswered 22d 30 + owner touch 20 + verdict 4". A rank a person cannot argue with is a rank a person will not trust.

## The card is the unit of visibility

There is already a widget layer. `normalizeWidget` and `renderRail` in `_lib/vault_widgets.js` render sideways card rails; `/api/tasks?format=widgets` and `/api/github-loop?format=widgets` both use it; article bodies embed live projections with `[[object:...]]`. What is missing is a card for the two kinds that matter most: a work object and a model comment.

One renderer per kind, and every surface calls it. The board calls it. The article page calls it, so an open objection on that article appears on that article. The GitHub loop calls it. A weekly digest email calls it. The same card in five places is the difference between a system a person checks and a system a person forgets.

The card carries five things: the subject line, the rank with its arithmetic, the state, the last action taken against it with a timestamp, and one button that does the obvious next thing — lease it, answer it, close it, or show its evidence.

## Before the build emails a stranger, the copy goes to the models first

The outreach loop today runs `LEADS_DISCOVER` to `LEADS_ENRICH` to `LEADS_VERIFY_MX` to `LEADS_SEND_BATCH`, and the copy is judged by the agent that wrote it. That is the one step in the whole build where something leaves the property and reaches a person who did not ask for it, and it is the step with the least review.

The comment system already solves this. It is a signed thread, minted keyless at `/api/comments/token`, with a verdict vocabulary and an answer obligation. It runs on articles. It should run on an outreach batch, because an outreach batch is an object like any other.

The mechanism, concretely: a send batch becomes an object with `kind='outreach'` and state `open`. Its card shows the draft copy, the subject line, the segment, and the scrape that produced the list — how many rows, from which source, how many survived MX verification, and five example rows with the reasoning that scored them. Web-based models are invited to the thread the same way they are invited to an article. `LEADS_SEND_BATCH` refuses while the object has fewer than three signed verdicts, and refuses outright on any open `OBJECTION` against the copy.

The value is not ceremony. It is that a model reading the draft cold will say the thing the writing agent cannot see: that the first sentence is about us, that the ask is buried in the fourth line, that the segment and the offer do not match, that two of the five example leads are dental practices and the list is mis-scored. That criticism is worth more before 200 sends than after.

Three failure modes to design against. A gate that always passes is worse than no gate, so the reviewing models must be able to see the scrape, not just the copy — a reviewer with no evidence produces agreement. Three verdicts from three instances of the same model is one opinion, so the panel must be drawn from different models. And a blocked send must show what would unblock it on the card, or the loop stalls silently, which is the failure mode this build has hit most often.

## Four forks, and which way each one should go

**Merge the tables, or project over them.** Projecting is right first. Write one view that reads `work_tasks`, `tasks`, `article_comments` and the GitHub mirror and emits the eleven-field object. Nothing migrates, nothing breaks, and the board is live in one pass. The precedent is already in the repo: the object widgets are a projection over existing tables and say so in their own header comment. Merging comes second, after the projection has proven the shape is right — a schema migration guessing at the shape is how you get a seventh table.

**Both task tables, or one.** One, eventually, with a clean division while it lasts: `tasks` becomes intake only — anything can drop a row in, no governance required — and `work_tasks` becomes the only place work is done. A row is promoted from intake to work object at the moment someone leases it, and promotion is where acceptance tests get written. Today's 6,565 flat rows are not a backlog. They are a log. Treating them as a backlog and re-ranking all of them produces 6,565 ranked rows, which is the same fog with numbers on it.

**Computed rank, or an owner-ordered list.** Computed, with the pin as a term. An owner who has to drag rows to order them is doing the machine's job, and the ordering he produces is stale within a day.

**Public board, or admin-only.** Public read, admin write. The build's whole argument is that a record that only the running model can read cannot govern anything. A queue that only the owner can open has the same defect one level up. The board should be a URL that can be handed to a model with no credentials, which reads it and knows what to do next.

## Build order

1. The projection endpoint. One route, eleven fields, four sources, no writes. This is the smallest thing that proves the object exists.
2. The two missing cards — work object and model comment — through the existing widget layer.
3. The board: one page, the card rail, five preset filters, sorted by rank.
4. The rank function, with `rank_why` rendered on every card. Until this ships the board is a list; after it ships the board is an answer.
5. The intake/promotion split, so leasing writes acceptance tests instead of hoping for them.
6. The outreach review gate, reusing the comment thread and the verdict vocabulary already in production.

The first four are visibility and can be done without touching a write path. The last two change behaviour and should be leased as work objects with acceptance tests, which is the point.



---

# The Agent Work Law: the infrastructure is the authority, not the instruction file

slug: agent-work-law · https://miscsubjects.com/a/agent-work-law · category: system · tags: governance, agents, work-object, infrastructure, audit · updated 2026-08-06T07:33:29.026Z

This build is operated by AI agents, and until 4 August 2026 the intelligence that decided what they worked on lived in the wrong place: in hidden Markdown files and in whichever model session happened to be open. This page is the law that replaced that arrangement, and the machinery it describes is live and public. Anyone — a person, an auditor, a competing model — can read the current task state, the acceptance tests, the evidence, the failures and the complete action history without asking anybody for access.

You are a clerk. The infrastructure is the law.

Nothing in a Markdown file — not CLAUDE.md, not STATE.md, not AGENTS.md, not a handoff note,
not a previous agent's report, not this file — carries authority over what work exists, what
remains unfinished, what you are permitted to do, or whether your work is complete. Those files
are pointers. The authority is one canonical object, live on the site, readable by anyone:

- **Human projection:** https://miscsubjects.com/a/the-work-object
- **Machine projection:** https://miscsubjects.com/api/work
- **Cold start:** https://miscsubjects.com/api/work/bootstrap

Both projections read the same record. There is no copy to keep in sync.

## Why this exists

For months the project's operating intelligence lived in a running model's context and in hidden
files. The rules were in CLAUDE.md. What remained unfinished was in STATE.md. Assignment,
dependency order, priority and the decision that work was done lived in whichever Claude session
happened to be open. A fresh agent could not enter the project. A different model could not
continue it. An auditor could not check any of it. Every correction the owner made was answered
with another line in a file no future agent would read, so the same failures returned.

The migration inverted that. Every operational fact is a row. Every transition is code.

## The five rules that bind you

1. **Work exists only as a task object.** If it is not a row in the work object, it is not work.
   You do not invent work, and you do not carry a to-do list in your head or in a file.

2. **You obtain work by leasing it.** You do not choose. `POST /api/work/lease` hands you the
   next eligible task — dependency-resolved, priority-ordered — with a lease token. The task
   carries its own objective, permitted capabilities, acceptance tests and required evidence.
   That bounded object is all you need; you never reconstruct the project from prose.

3. **You cannot complete work by saying you completed it.** You `POST` your evidence to
   `/api/work/task/<id>/submit`. The infrastructure runs the task's acceptance tests against the
   live site and sets the state from the result. `accepted:false` comes back with the exact test
   that failed. Your assertion is not an input.

4. **A failure becomes a child task, not a sentence in a report.**
   `POST /api/work/task/<id>/fail` with the failure class, the infrastructure layer that permitted
   it, and the invariant that should have prevented it. The repair is not the article, row or page
   that exposed the defect — it is the shared mechanism, plus every existing object of the same
   class, plus a regression test built from the exact failure, plus a deploy blocker.

5. **Every action is appended, never overwritten.** Each lease, note, submission, acceptance,
   refusal and repair is one hash-chained row in `work_actions`, carrying who acted, which model,
   which capability authorised it, the task revision, the exact input and output, what changed,
   the tests run, the evidence and the verdict. Corrections append a revision naming what they
   supersede. The full chain is public at `/api/work/audit`.

## The loop, exactly

```bash
# 1. read the object (public, no credential)
curl -sS https://miscsubjects.com/api/work | jq '{objective, counts, next_eligible_action}'

# 2. lease the next task
curl -sS -X POST https://miscsubjects.com/api/work/lease \
  -H 'content-type: application/json' \
  -d '{"agent":"<your name>","model":"<your model id>","capability_token":"<scoped token>"}'

# 3. do exactly what the task says, using only the capabilities it lists

# 4. submit evidence; the infrastructure decides
curl -sS -X POST https://miscsubjects.com/api/work/task/WT-0001/submit \
  -H 'content-type: application/json' \
  -d '{"agent":"<your name>","lease_token":"<from step 2>",
       "evidence":{"rendered_url":"https://miscsubjects.com/a/...","sources_added":"..."},
       "changed":["/a/..."]}'

# 5. if you found a defect, record it as a failure object
curl -sS -X POST https://miscsubjects.com/api/work/task/WT-0001/fail \
  -H 'content-type: application/json' \
  -d '{"agent":"<your name>","failure":{"failure_class":"...","layer":"...","missing_invariant":"..."}}'
```

Reads are public. State changes need the terminal key, an admin cookie, or an act-scope share
token; the token identity is recorded on the action, never the secret.

## What the task object contains

`task_id`, `objective`, `detail`, `state`, `priority`, `depends_on`, `permitted_capabilities`,
`acceptance_tests`, `required_evidence`, `parent_task`, `supersedes`, `failure`, `failure_count`,
`last_result`, `lease`, `revision`, `created_at`, `updated_at`, and the two URLs you need:
`audit` and `submit_to`.

## The states, and who moves them

`open → leased → in_progress → evidence_submitted → accepted → completed`, with `refused`,
`failed` and `repair_required` as the branches. Transitions are declared in
`functions/_lib/work_object.js` and enforced there. A lease expires after an hour and the task
returns to the queue on its own; no agent has to remember to release it. Nothing an agent writes
in prose moves a state.

## The coding law binds you too, and it is a different lock

Two laws govern an agent that edits this repository and they do not overlap. This one leases the
*work*: who owns a task, who may move it, who submits its evidence. The coding law leases the
*file*: the sha256 of what you read, checked against the sha256 of what the newest commit left, so a
commit that silently erased another agent's edit cannot look identical to one that did not.

Holding a task under this law is not permission to write a file. If the path is in the coding-law
scope — `functions/`, `scripts/`, `migrations/`, `workers/`, `apps-script/`, `public/`, either skills
tree, `schema.sql`, `wrangler.toml` — you open a coding-law lease before the first edit and close it
before the commit, whichever law sent you there. Two locks, different jobs, both required, neither
overriding the other.

This paragraph exists because a model reading both pages found them disagreeing and said so. It also
records what the disagreement was hiding: until 6 August the coding-law gate was written and never
wired into the deploy, so nothing refused an unleased edit at all. The gate now runs in the pre
phase of every ship, and the first thing it refused was the commit that wired it.

## Content law still applies, and it is also enforced

The write path refuses violations server-side with a 422 that names the fix, so you learn the law
by hitting it rather than by remembering it:

- one object per article — a compound page carries no condition frame (`one_object_guard.js`)
- no model signature in a body
- no test-shaped titles, no model self-introduction, no hashtag blocks
- plain language over the body **and** the claims, checked in the deploy chain
- an authored body always beats the slot composer

The governing invariants are listed in full, live, inside the work object.

## What you must never do

- Add a rule to CLAUDE.md, STATE.md, AGENTS.md or a handoff file and call it a fix.
- Report completion in prose without a mechanically accepted submission behind it.
- Repair only the object that exposed a defect.
- Trust another agent's final report, or your own memory, as evidence.
- Write to the database directly for ordinary work. The guarded write path is the door; direct SQL
  is a repair capability and every use of it is a bypass listed in the work object.


## Sources

1. The work object — machine projection — https://miscsubjects.com/api/work
2. Cold-start contract for an agent with no prior context — https://miscsubjects.com/api/work/bootstrap
3. The append-only, hash-chained action log — https://miscsubjects.com/api/work/audit
4. The work object — human projection — https://miscsubjects.com/a/the-work-object
5. The governing invariants — https://miscsubjects.com/api/laws

