Lessons
When a worker's first attempt at a pull request does not stick and the second one does, the fix is usually something the next worker in that role should know before it starts. Lessons carry that forward, with the brakes on: a wrong lesson spreads exactly as easily as a right one, so nothing here learns by itself.
$ worktender lessons propose --write # find failed-then-fixed dispatches, draft them
$ $EDITOR ~/.config/worktender/lessons/engineer.proposed.md # you write the lesson
$ worktender lessons approve worktender-227 # you approve it
$ worktender start 42 --repo . # the next engineer reads it
The files
Lessons live beside the role configuration, one pair of files per
role, in ~/.config/worktender/lessons/ (or under $XDG_CONFIG_HOME):
| File | Who writes it | Who reads it |
|---|---|---|
<role>.proposed.md |
lessons propose --write, then you |
you, and lessons approve. Never a worker. |
<role>.md |
lessons approve, or you by hand |
start, for every worker in that role |
Both are yours. No repository can ship one, for the reason there is no per-repository role configuration: a file a checkout could supply would be a repository writing into its own workers' prompts.
Each lesson is a block:
## worktender-227
provenance: https://github.com/steig/worktender/pull/227
motivated-by: ledger: #204 on 204-stats-escapes started 2026-10-01T10:00:00Z; 2 done reports, 1 pr-feedback round
added: 2026-10-16
expires: 2026-12-15
approved-by: steig
Run `go vet ./...` as well as the tests before reporting done; CI runs both.
| Key | Meaning |
|---|---|
## <id> |
Lowercase letters, digits and -. propose names it <repository>-<pr>. |
provenance |
The pull request the lesson came from. Required, and an https:// URL. |
motivated-by |
The ledger cycle that made it a candidate. |
added, expires |
Dates. A lesson is last read the day before expires. |
approved-by |
Who approved it. |
Unknown keys are refused, naming the file and line: a misspelt expire: that
silently did nothing would be a lesson that never lapses. Lines beginning |
are evidence from GitHub, never part of the lesson.
Proposing
worktender lessons propose reads the dispatch ledger and finds the
dispatches that failed and were then fixed: ones that reported done more than
once, or reported done after a pr-feedback round. For each, it asks
gh pr view for the pull request and its commits, and prints a draft with the
commits added after the first attempt ended.
worktender lessons propose [--role <role>] [--since 720h] [--write]
The draft's text is a TODO. Only a person can say what a cycle taught —
the commits say what changed, not why. --write appends the drafts to each
role's .proposed.md, skipping any id already proposed or live; without it,
nothing is written. A dispatch started with no role has no role's lessons to
join, and is counted rather than drafted.
The evidence lines are commit headlines, written by whoever pushed: untrusted,
escaped, and quoted with |. They stay in the proposals file, which no worker
ever reads, and approve drops them.
Approving
worktender lessons approve <id> [--role <role>] [--days 60] [--yes]
Prints the lesson exactly as it will be read, says which role's workers will
read it and until when, and asks you to type yes — the same act as
worktender approve for an issue. It then moves the lesson into
<role>.md, stamped with today's date, an expiry --days from now (60 by
default), and approved-by: your gh login, or git config user.name when gh
cannot say.
It refuses a lesson still carrying the TODO, one without a provenance URL,
and one whose id is already live.
Editing <role>.md by hand is fine: it is your file, and you are the approval.
That is also how a lesson is renewed — move its expires forward.
What a worker reads
start, briefing a worker in a role, reads that role's <role>.md and takes
the lessons that are complete, unexpired, and among the first five in the file.
Each is cut to one line of at most 400 characters. They are written to
worktender/lessons.md in the worktree's git directory — never committed, gone
with the worktree — framed as guidance a person approved, each with its pull
request, approver and expiry.
The lessons are not typed into the brief. The brief has to arrive in a
single pane read, and five lessons would be several times that. So the brief
gains one sentence pointing at the file. The longest briefs — an analysis role
with directories, a six-digit issue and a long branch — have no room even for
that; start then says so and leaves the file pinned for you to point the
worker at. A factory:ready issue's worker is pointed at the lessons from the
foot of its approved copy instead, below the hashed payload.
start prints what it skipped and why:
lessons: skipped 1 expired, 2 past the cap of 5 for role engineer; `worktender lessons list` says which
lessons: 5 lessons for role engineer pinned to /repo/.git/worktrees/42-fix/worktender/lessons.md
A role with no lessons file starts exactly as it did before lessons existed.
Listing, removing, expiring
worktender lessons list [--role <role>]
worktender lessons remove <id> [--role <role>]
list shows every lesson, live or proposed, with its state: live,
expired, invalid (and why), past the cap of five, or proposed.
remove deletes the lesson from the live file — from every role's that holds
it, unless --role narrows it. Removal is traceable by construction. A
lesson has one home, and start reads it at brief time with nothing cached
anywhere, so a removed lesson reaches no worker started afterwards. A worker
already running keeps the copy it was briefed with.
worktender doctor warns about an expired or invalid lesson, by role and id.
start already skips it; the warning is the prompt to renew or remove it,
rather than letting dead rules accumulate into ceremony.