# The work cycle — what happens at the end of a piece of work

```
Status: Current
Owner: _null
Last reviewed: 2026-08-18
Governs: what must be true before a piece of work counts as finished
Review trigger: Any change to what the Command Center reads, or to which of
                those channels a person rather than an agent has to write
```

> **This is a procedure, not a status board.** Nothing here records what is open,
> what is next, or what is blocked — those live in the tracker and on
> privacyllc.dev, and a copy of them in this file would be the second disagreeing
> record that `DOC_TRUST_MAP.md` exists to prevent.

## Why this file exists

A piece of work ends in more than one place. The code is committed; the issue
that asked for it is still open; the document the change contradicts still says
the old thing; and the project screen at
[privacyllc.dev](https://privacyllc.dev) still shows last week's next action to
whoever opens it.

None of those catch up on their own. Two of them cannot be caught up later by
anybody but the person who did the work, because by then nobody knows what the
next action was meant to be.

## The cycle

Run all of it, in this order, every time. It is short on purpose.

1. **Close what you finished.** `closes #N` in the commit that does the work, so
   the record comes from the thing that happened rather than a date typed
   afterwards. If no single commit finished it, close it by hand with the
   evidence — a path, a symbol, a test name, or the command that proves it.
   *"Done" is not a close.*
2. **File what you found.** A defect noticed on the way past is an issue with a
   severity label and the build SHA it was seen at, not a memory. Filing it costs
   a minute; the alternative is finding it again from scratch, or shipping it.
3. **Close the milestone if the batch landed.** A milestone with every issue
   closed and itself still open reads as a batch still in progress — see
   [Open and closed are not bookkeeping](#open-and-closed-are-not-bookkeeping).
4. **Update the documents this change triggered.** Read the `Review trigger`
   lines: a new module, a changed data shape, a new migration, a new boundary
   something crosses. Those edits go in **the same commit as the code**, for the
   reason in [Docs travel with the push](#docs-travel-with-the-push).
5. **Push.** Nothing above is visible off this machine until you do, and step 7
   reports on what was pushed.
6. **Write the log entry** in `docs/history/DEVELOPMENT_LOG.md`: what changed,
   what it proved, **Next action**, and **Blockers**. Dated, append-only, newest
   first.
7. **Tell the Command Center**, which is three calls: reconcile, so it re-reads
   the tracker and the pushed docs; `PATCH` the project's summary and next
   action, which nothing else writes; and a check-in if what changed is worth a
   timestamped note. Commands are [at the bottom](#the-commands).

Steps 6 and 7 are the two that get skipped, and they are the two nobody else can
do afterwards.

## Next action — where it lives

| Copy | Where | What it is |
| --- | --- | --- |
| The current one | `nextAction` on the project, privacyllc.dev | the live answer to "what happens next", shown to whoever opens the project screen |
| The dated one | the newest entry in `DEVELOPMENT_LOG.md` | what the next action was **at that point** — history, not status |

These are not two records of the same thing, and the distinction is worth
holding on to. The field is overwritten every time and always describes now. The
log entry is never edited and describes a moment — which is what makes it safe
to keep, and why an old entry naming a next action that has since been done is
not stale, it is a receipt.

Write the next action as an **action**: the thing a person would start on
Monday, specific enough to begin without asking a question. "Continue the work"
is not one. If the honest answer is that you do not know, that is a real answer —
say what has to be decided and by whom.

**Filing an issue can change what the project card says next, without anyone
choosing that.** The dashboard's next action is the *newest open issue in the
current milestone* — not the most severe one; severity labels have no influence
on it at all. So a routine `P2` filed into the batch you are working in replaces
whatever the card was showing, and it will keep showing that until something
newer arrives. The `nextAction` field is the only way to say something different
on purpose, which is most of why step 7 exists.

`currentSummary`, `nextAction` and `description` are **write-only**: they are
deliberately absent from every API response, because free text can name a
customer or an unannounced product. Only the admin screen shows them back. Never
report them as empty because a `GET` did not return them.

## Blockers — where they live

A blocker is recorded in up to three places, and **an agent can write only the
first and the third**:

| Where | What it holds | Who writes it |
| --- | --- | --- |
| The tracker | the work itself — an issue labelled `release-blocker`, or `P0` when it ships broken | anyone, including an agent |
| The blockers table on privacyllc.dev | the business-facing blocker, with a severity, an owner and a resolution note | **a human, in the admin UI** |
| A check-in's `blockers` field | narrative: what is stuck, said in a timestamped note | anyone, including an agent |

The agent API has no route that creates, edits or resolves a blocker on the
site. Posting a check-in whose `blockers` field says something is stuck records
*narrative* — it does not touch the blockers table, and it does not clear
anything. An agent that reports "blocker filed" after a check-in has told the
truth about the note and a falsehood about the table.

So: **file the issue** — that is the copy the work is actually tracked in — and
when it belongs in front of a stakeholder, say so plainly and let a human enter
it. Before treating a site blocker as resolved, restate its title and status and
get explicit confirmation; the admin UI requires a resolution note that the API
cannot supply.

If the work is blocked and nothing is filed anywhere, the project simply looks
slow.

## Open and closed are not bookkeeping

Every open issue is a denominator, and not in the abstract: the percentage on
the project screen **is** closed issues over all issues in this tracker. Nothing
else produces it. That has three consequences worth stating in full:

- **An issue left open after the work is done** understates the project
  permanently, and the understatement compounds — a fortnight of finished work
  with unclosed issues reads as a fortnight of no progress.
- **An issue closed without evidence** cannot be reopened with confidence,
  because nothing in it says what "fixed" meant. That is why the close comment
  carries the path, symbol, test or command.
- **Invented future work** makes every percentage wrong, permanently and in one
  direction. Do not pad the tracker. If the real answer is one milestone and
  three issues, file exactly that.

Milestones are the same argument at batch scale, and they are counted the same
way: milestones closed over milestones total is the second figure on the project
screen. Closing the last issue under a milestone does not close the milestone,
so a tracker full of complete-but-open batches reports a project as less
finished than it is — and cannot answer "what shipped".

**File every issue into a milestone.** One filed outside still counts against
the headline percentage while being invisible to the milestone figure, which is
how two readings of the same project come to describe different amounts of work.
The site measures the gap rather than ignoring it.

One trap that costs an afternoon, and it is about a *card* rather than a figure:
the Milestones list on privacyllc.dev reads the Command Center's own table,
which only an admin can write. A repository whose milestones are being counted
in the figure above can still show *"No milestones have been added yet"* in that
list. It is not a sync failure and no amount of reconciling changes it.

## Docs travel with the push

The Command Center reads this repository's documents at a commit, and stores the
SHA it read them from. When that SHA falls behind the repository's newest
commit, the docs report is marked **stale** — not wrong, not missing, *stale*,
which is the honest description of a document that was accurate at a commit
nobody is running any more.

Two ways to produce it, and both are ordinary carelessness rather than bad luck:

- **Code pushed, documents not updated.** The report is recomputed at the new
  SHA against prose describing the old behaviour. Nothing flags this; the
  document is simply confidently wrong now, and its `Last reviewed` line still
  looks recent.
- **Documents updated, not pushed.** The site keeps reporting the old ones. A
  correction that exists only on your machine has not been made.

Hence step 4's insistence that documentation edits ride in the same commit as
the change that caused them. It is not tidiness — a doc commit that comes "after
this next thing" is the commit that does not get written, and the manual's
migration table in the project this template came from sat six migrations behind
before anybody noticed. Every reader in between trusted it.

## What the Command Center reads, and who writes it

Four independent channels, and **only two of them are automatic**:

| On the project screen | Source | How it gets there |
| --- | --- | --- |
| Headline % — issues closed / all issues | the repository's tracker | automatic — on reconcile, and immediately on a webhook delivery |
| Second figure — milestones closed / all milestones | the repository's **milestones** | same read, same moment |
| Milestone coverage — issues that sit in no milestone | the repository's tracker | same read |
| QA verdict — round, build SHA, overall sentence | `docs/qa/ClaudeReport.md` | push the repository, then reconcile |
| Current summary / Next action | manual fields on the project | `PATCH /agent/projects/queue-north-website` — nothing else writes them |
| The Milestones card, and a typed weighted plan | the Command Center's **own** milestone table | admin UI only |
| Blockers | the Command Center's **own** blockers table | admin UI only |

The first three are why steps 1 and 3 are steps: **closing an issue and closing
a milestone each move a figure a stakeholder can see, that day.** The last two
are lists rather than figures, and they are the ones an agent cannot write — a
repository whose milestones are counted in the second figure can still show
*"No milestones have been added yet"* on the card, because that card reads a
table only an admin fills in.

A repository that has not adopted the four label names is reported as *not
adopted* rather than as zero defects, and one with its tracker switched off is
reported as switched off rather than as an empty backlog. Absence is never
rendered as a measurement — which is exactly why a real backlog nobody filed
looks like nothing at all.

## The commands

The tracker is the Forgejo instance at
**[dream.scheller.ltd](https://dream.scheller.ltd)**, and steps 1 to 3 happen
there. Its credentials, the Cloudflare trap and the reason it has to be that
instance rather than any tracker are in the project README's *Where the tracker
is* — one copy, named once.

**This repository has no test suite**, so "the evidence that proves it" is
usually a file and symbol, a `curl` against the live origin, or what was observed
at a named viewport. Say which; "Done" is not a close.

```bash
set -a; . ~/.openclaw/docker-registry.env; set +a
python3 scripts/forgejo-issue.py list
python3 scripts/forgejo-issue.py close 196 "Fixed in a1b2c3d; src/components/layout/Header.jsx
#   no longer overlaps at 320px — checked in DevTools at iPhone SE width."
```

Step 7 talks to the Command Center instead:

```bash
# The token lives in this machine's credential store, never in the repository.
TOKEN="$(grep PRIVACY_LLC_TOKEN ~/.openclaw/credentials/privacyllc.md | cut -d= -f2)"
BASE="https://privacyllc.dev/api/internal/v1"
AUTH="Authorization: Bearer $TOKEN"
```

**Two things about refreshing that are worth knowing before you need them.**

*The webhook must carry the signing secret.* `POST /repos/null/Queue-North-Website/hooks`
with a URL and no `secret` produces a hook that looks perfectly healthy in
Forgejo — `active: true`, test deliveries accepted with a 204 — and whose every
delivery is rejected by privacyllc.dev with `401 bad_signature`. Forgejo exposes
no delivery history through its API, so nothing anywhere says so. This repository
ran in exactly that state for ten minutes on 2026-08-18. The secret is
`FORGEJO_WEBHOOK_SECRET` in `~/.openclaw/credentials/privacyllc.md`. To prove a
delivery actually lands, sign a payload yourself and look for `"applied":true`:

```bash
WHSEC=$(grep '^FORGEJO_WEBHOOK_SECRET=' ~/.openclaw/credentials/privacyllc.md | cut -d= -f2-)
# HMAC-SHA256 of the body, in X-Forgejo-Signature (hex, no "sha256=" prefix).
# X-Hub-Signature-256 is NOT accepted.
```

*A reconcile does not force a re-read.* Both `reconcile_all` and `sync_projects`
skip a project that is inside its freshness window, reporting `refreshed=0
still_due=0` — which reads exactly like "nothing to do" and means "not yet due".
There is no per-project force in the agent API: `{"kind":"refresh"}` and
`{"kind":"reconcile"}` are both refused, and only `reconcile_all` and
`sync_projects` can be triggered. So a tracker change made minutes after a sweep
is not visible on the card until the next one, however many times you reconcile.
That is a wait, not a failure — but say so rather than reporting the numbers as
updated.

**Reconcile** — re-read the tracker and the pushed documents. Do this after the
push, not before:

```bash
curl -sk -X POST -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"kind":"reconcile_all"}' "$BASE/agent/jobs"
```

**Summary and next action** — read the project first for its `version`, then
send a flat body carrying that version. A stale version is rejected rather than
silently overwriting somebody else's edit:

```bash
curl -sk -H "$AUTH" "$BASE/agent/projects/queue-north-website"   # for version + state

curl -sk -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"version":<n>,"health":"on_track",
       "currentSummary":"<where the project stands, in a sentence or two>",
       "nextAction":"<the next concrete thing, specific enough to start>"}' \
  "$BASE/agent/projects/queue-north-website"
```

**A check-in** — a timestamped "what changed", when there is something real to
report. `summary` is required and must say something; the API refuses an empty
"no change" update, deliberately.

**`projectId` here is the UUID, not the slug.** Every other route in this API
takes the slug, and this one answers a slug with `{"code":"unknown","message":
"Something went wrong."}` — a generic 500 that says nothing about which field is
wrong, and which is identical to the error for a genuinely broken request. It
cost twenty minutes on 2026-08-18 and it will cost the next person the same
unless they read this line. Read the UUID from the project first:

```bash
UUID=$(curl -sk -H "$AUTH" "$BASE/agent/projects/queue-north-website" \
       | python3 -c 'import sys,json;print(json.load(sys.stdin)["project"]["id"])')

curl -sk -X POST -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"projectId\":\"$UUID\",\"health\":\"on_track\",
       \"summary\":\"<what changed>\",\"accomplishments\":\"<what landed>\",
       \"blockers\":\"<what is stuck, or omit>\",\"nextActions\":\"<what is next>\"}" \
  "$BASE/agent/updates"
```

Health is one of `on_track`, `caution`, `off_track`, `unknown`. Never invent
one, and never report progress the API did not return.

## What this file is not

- **Not the work list.** That is the tracker: milestones are batches, issues are
  deliverables.
- **Not the release procedure.** That is `npm run release` to publish and
  `npm run deploy` to move the stack — two commands on purpose. `scripts/release.sh`
  and `docs/OPERATIONS.md` carry the detail, and
  `docs/security/SECURITY_CHECKLIST.md` is the list to work through first.
- **Not the QA procedure.** That is `docs/qa/ClaudeQAPlan.md`, which ends in its
  own version of step 7.
- **Not a place to record status.** If you are about to add "current state" or a
  list of outstanding items below this line, the tracker is where it goes.
