Repository navigation
Releases
v0.9.0
v0.9.0 — exec --rejoin is removed
scratchpad exec --rejoin re-watched every pad a name took part in after a relaunch. A name belongs to one pad: the same name on another pad may be a different agent, so a relaunched agent could be nudged about traffic that was never its own. Bare --rejoin also exited with an error, before starting the agent, whenever SCRATCHPAD_AUTHOR was unset.
The flags --rejoin and --rejoin-max-idle are gone, and passing either is now unknown flag. Remove them from any launch command before upgrading.
A relaunched agent rejoins by running scratchpad pad get <ref> --as <name> on each pad it was taking part in, using the name it had on that pad. That registers it from its own last post there, so what arrived while it was down is pointed at once. Prompting a resumed agent to do this is the job of whoever relaunched it.
exec no longer refuses arguments placed before --.
Changelog
Features
- fcfe9c3 [drop-rejoin] feat(exec)!: remove --rejoin
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shv0.8.4
v0.8.4 — reading a nudge's pointer counts as acting on it
Under scratchpad exec, an agent that followed a nudge and read the pad without posting used to keep that pad held: the next message addressed to it waited for the 5-minute re-nudge (measured: 2m26s on a live pair). Running the pointer exactly as given, pad read <ref> --since N, now tells the relay the agent read the pad through its last printed section. The hold lifts, and new traffic is nudged at once.
Only a plain --since read counts. A read without it, or narrowed with --kind/--task, counts for nothing. A read never registers the agent, carries no name, never moves the mark backwards and is refused for another store. Re-reading at the same mark cannot start a re-nudge loop.
Relays started from 0.8.3 or earlier do not know the new message. Reads under them still work, and print a note to restart scratchpad exec.
Changelog
Features
- 6b7d938 [read-as-acting] feat(relay): a
pad read --sincelifts the nudge hold
Bug fixes
- d11cd08 [read-old-relay] fix(read): say when the relay is too old to record a read
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shv0.8.3
v0.8.3 — relaunched agents rejoin their pads; relays stay within their own store
scratchpad exec --rejoin (bare: the name in SCRATCHPAD_AUTHOR; or --rejoin=<name>) lets an agent relaunched after a host restart, a reboot or a resumed conversation hear its pads again without touching them. Before the agent starts, the relay finds the pads that identity takes part in, meaning pads it posted to or holds an open task in, and watches each one from the identity's own last post. Anything that arrived while the agent was down is pointed at once. Protected pads, pads idle past --rejoin-max-idle (default 7 days) and pads another live relay already watches under the same name are left out and counted. The duplicate case also warns, so two sessions under one name are never both nudged.
Fixes: a command pointed at another store with --dir no longer registers with the relay of the store the agent was launched in. The relay refuses it with other_store, and the agent is told to arm its own wait. exec refuses arguments before --: exec --rejoin ping -- claude used to launch ping.
Old and new relays and commands interoperate. A relay still running an older binary accepts registrations as before, until its agent restarts.
Changelog
Features
- 7fa34b8 [exec-rejoin] feat(exec): --rejoin re-watches an identity's pads after a relaunch
Bug fixes
- ecec318 [exec-rejoin] fix(relay): refuse a registration from a different store
- a1c4a9e [main] fix(exec): refuse arguments before
--; document --rejoin= - feb6175 [release-notes] fix(release): put the tag's message on the release page; drop merge lines
Documentation
- 48fda50 [main] docs: lead the TCP setup with named auth.clients, not --tcp-token-digest
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shv0.8.2
Changelog
Features
- 59b5393 [auth-clients] feat(auth): name the credentials, and let revoking one apply at once
Bug fixes
- 8e7602b [relay-renudge-loop] fix(relay): only an ADVANCED mark clears a pad's nudge
- f982600 [task-dropped-owner] fix(tasks): a dropped owner releases its completion slot
Documentation
- 59c8760 [main] docs: record the remote-access proposal instead of leaving it untracked
Others
- b15a3d8 [main] Merge branch 'claude/auth-clients'
- f3742fb [main] Merge branch 'claude/relay-renudge-loop'
- 39ea611 [main] Merge branch 'claude/task-dropped-owner'
- 812b8b6 [main] Merge branch 'claude/vendor-puredashboard'
- 930486b [vendor-puredashboard] style(ui): drop two workarounds puredashboard v0.2.0 made redundant
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shv0.8.1 - feat: add JSON pad status and selective task-opener relay wakes
Summary
This release improves automation and multi-agent coordination:
- Adds machine-readable JSON output to
scratchpad pad get. - Wakes task dispatchers when work they opened changes.
- Keeps unrelated task traffic quiet.
Changes
JSON pad status
- Add
scratchpad pad get <ref> --json. - Return one structured JSON document containing:
- pad metadata and protection state
- authors and section count
- structured turn state
- optional author inbox
- body-free section table of contents
- Preserve the existing text output and protected-pad authorization behavior.
Selective relay wakes
- Add the
openedwake selector for task events on tasks created by the current agent. - Automatic relay registration now uses
me,mine,opened:me: relevant conversationmine: tasks assigned to the agentopened: tasks dispatched by the agent
- Ignore task traffic opened by unrelated coordinators.
- Fall back to
me,minewhen connecting to an older running relay, with guidance to restartscratchpad execto
enableopened. - Update CLI help, MCP schemas, design documentation, and bundled skill guidance.
Compatibility
- No breaking CLI changes.
- Text output remains the default.
- Omitting
wake_forretains the existinganybehavior. - New clients remain usable with older relay processes through selector fallback.
Validation
make check- Go vet and test suite passed.
- All 24 Web UI tests passed.
- Local zellij E2E test with three persistent agents on pad
default-r54gdq:- Relevant task traffic woke the dispatcher relay:
nudges 0 → 1. - Unrelated task traffic left the dispatcher unchanged:
nudges 1 → 1. - All test tasks completed successfully.
- Relevant task traffic woke the dispatcher relay:
- E2E limitation: one local pad, three agents, and one positive/negative control cycle.
Changelog
Features
- ec12c04 [main] feat(cli): add JSON output to pad get
- 3b9f05f [main] feat(relay): wake task openers selectively
Documentation
- 00a5c63 [main] docs(skill): use English trigger examples
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shv0.8.0
v0.8.0 — an agent that never has to arm a wait
The whole tool rested on one discipline it could not enforce: arm a wait before your turn ends. Agents miss it two ways, and both fail silently — they forget, or they are mid-task and will get to it much later. Either way the pad holds an answer nobody is coming for, and the other agents are talking to a process that will never notice. Worse, only some hosts have a background mechanism whose exit reaches the agent at all; the rest can only block.
scratchpad exec moves the listening off the agent.
scratchpad exec -- claude # or codex, or any agent that runs in a terminalThe agent runs on a pty this tool owns. When a pad it takes part in moves, one line is typed into its input:
scratchpad notification 'new activity — run: scratchpad pad read default-ab3k9x --since 12'
Nobody arms anything, and nobody re-arms anything. Joining a pad is posting to it — pad create, pad post and pad get --as register you automatically, then say so in their output:
ref: default-ab3k9x
section: 4
next: 5
relay: watching default-ab3k9x from §4
If registering fails you get a warning telling you to arm a wait yourself, instead of silence that reads exactly like success.
Three properties worth knowing
What arrives is a pointer, never content. Another agent's words pasted into your prompt as if you had typed them is command injection, whatever state the receiver is in. The nudge names a pad and a section, nothing else.
It stays harmless if the agent is gone. exec types into whatever is on the other end of the pty, and a crashed agent leaves the shell it was launched from — which executes what it is given. That is why the nudge is scratchpad notification '…': a real command whose entire behaviour is to print its argument. The message is single-quoted, so even the pointer inside it stays inert.
Nudges coalesce, they do not queue. Every nudge for a pad says the same thing, so a later one subsumes an earlier one: five new sections are one line, three pads are one line. A pad already nudged about is not nudged again until you give a sign of having acted, with a slow floor underneath so nothing goes quiet for good.
Seeing what it is doing
A relay that is nudging and one that never heard of your pad are both silent — which is the whole question when nothing arrives.
$ scratchpad relay
▸ pid 18223 codex
socket /Users/you/.scratchpad/relays/18223-ce846a.sock
nudges 9, last 44s ago: scratchpad notification 'new activity — run: …'
watching
chitchat-xs56hi as teo from §21 quiet
It separates the cases that look identical from outside: no relay running, a relay watching nothing (the agent has not joined a pad yet), a pad registered under a name the traffic is not addressed to, or a nudge that was delivered and simply not acted on.
Also scratchpad exec --log (appends registrations and nudges to <dir>/relay.log, one file per store, with the relay's pid on every line — tail -f follows the whole team), and doctor now reports relay sockets nobody is listening on.
Also in this release
Content search in the Web UI, and GET /api/search behind it — search the store, a project, or one pad, with each hit as a place to go. A long line is now cut around its match, so the row you get back always contains the word it reports.
A watcher fix worth naming. A write landing between the watcher's initial snapshot and the registration of its directory watches was lost, not delayed: it emitted nothing at the time and matched the snapshot at the next rescan too. Every internal/watch consumer had that window. Found by a relay test that failed once in five runs.
The agent skill document is 47% shorter — 457 lines to 240. Measured on a real session, Codex reads the first 240 lines and only then goes back for more, so the tail of a long skill file is both noise and, in practice, unread. Rules an agent must obey stayed; the reference material moved to scratchpad skills docs usage. A new "When a command fails" table gives nine error codes one line each, saying the right next move.
Upgrading
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shNothing breaks. exec is opt-in, and an agent launched any other way behaves exactly as before and arms its own waits. No config change, no store migration, no new state on disk — the relay's registry lives in memory and dies with the process.
Two things worth doing after upgrading:
- Reinstall the skill so your agents get the new rules, including "do not arm a wait under a relay":
scratchpad skills install --into ~/.claude/skills # or your host's skills dir
- Check which binary your agents actually run. If an old copy is earlier in their
PATH, registration silently does nothing and the feature looks broken with no error anywhere.which scratchpadis the check.
Caveats
exec sits between your keyboard and the agent for the whole session — raw mode, window resizes, cleanup on exit. It has been used in real sessions, but it is new, and that is why it is opt-in and why nothing else in the tool depends on it.
MCP is deliberately unchanged: exec has to be the agent's parent process, and an MCP host owns its own process and terminal.
Changelog
Features
- 192c656 [docs-seo-status] feat(site): link the page to the @dnh_ma account on X
- eba12c2 [exec-relay] feat(cli):
execruns an agent under a relay, and posting is how it joins - b165d08 [exec-relay] feat(cli):
notificationmakes a nudge harmless when a shell receives it - cbdfd3e [exec-relay] feat(cli):
relayand--log, because a working relay and a deaf one are both silent - 7842fab [exec-relay] feat(relay): nudge an agent when a pad moves, so nobody has to arm a wait
- 04e09d5 [ui-search] feat(ui): GET /api/search — the store's content search, over HTTP
- dc6dd39 [ui-search] feat(ui): search the store, a project, or one pad — with the hit as a place to go
Bug fixes
- 3cbba1d [docs-seo-status] fix(site): a share card X will actually fetch — 1.78MB PNG to a 151KB JPEG
- aadab3f [docs-seo-status] fix(site): the "A pad" slide was showing the Tasks tab, not the outline
- 588625c [exec-relay] fix(watch): a write between the snapshot and the watches was lost, not late
- 321961f [ui-search] fix(search): cut a hit around its match, so the row contains the word it reports
- 61f4a79 [ui-transcript-repeat] fix(ui): the five things the review found, two of them mine to own
Documentation
- f86fafb [docs-seo-status] docs(seo): record what tracking is actually set, and correct two notes
- 571876e [docs-seo-status] docs(site): reasons before mechanisms — Features, an agent strip, Rules, Web UI slides
- 44a9e61 [docs-seo-status] docs: lead with the agent, show the exchange, and fix two commands that never ran
- 9615e51 [docs-skill-install] docs: how to install the agent skill, which nothing but the reference explained
- a5343ca [docs-skill-install] docs: name the hosts in the install step, and say where each keeps skills
- 3182250 [exec-relay] docs(skill): 457 lines back to 240 — rules stay, reference moves out
- dcaaa51 [exec-relay] docs:
execin the design spec, the guide, the topics and the README - 72f76ce [main] docs: the embed rule changed under this paragraph while it was being merged
- d8ef755 [ui-transcript-repeat] docs(ui): name the two ways this harness answers wrong without saying so
- f77768b [ui-transcript-repeat] docs(ui): say that ROLES' 2–3 character rule is enforced by review and nothing else
- 441fedf [ui-transcript-repeat] docs(ui): say which tool fits the captureScroll question, not that the harness is shaky
Others
- c6b78da [docs-seo-status] perf(images): WebP for everything the pages load, and delete what nothing links
- 2dc6533 [main] Merge branch 'ui-transcript-repeat': the transcript keeps its rows, and a harness that says when it cannot tell
- b7d37f4 [ui-transcript-repeat] ci: declare the Node that
make checknow needs - a83b9af [ui-transcript-repeat] perf(ui): repeat() keyed by section — an expanded section survives an arriving post
- 8cf0c1a [ui-transcript-repeat] refactor(ui): one template per transcript row, so a row can be kept instead of rebuilt
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shv0.7.0
⚠️ Behaviour change on upgrade
A rules edit now binds the agents already working — including on stores you never
reconfigure. Previously the read gate fired once per author per pad, so an agent that
had already posted went on posting under the version it read on its way in. From v0.7.0
the default is rules.reack = "on-change": after a rules edit at any level, the next post
from an agent that has not read the new version is refused with rules_unread.
Nothing to migrate — the marker format is unchanged and existing markers load as they are.
To keep the old behaviour, set it explicitly:
{ "rules": { "reack": "once" } }If your agents run unattended, make sure they treat rules_unread on a pad they have
posted in before as "the rules moved" rather than as a stale-digest retry. scratchpad skills docs usage says so now, and so does the error itself, which hands back the rules
in full plus the digest to repeat.
A rules change that reaches the agents it binds (#18)
Editing the rules used to be close to inert. Reaching an agent already on a pad meant
interrupting its session by hand — which costs a person their time and the agent its
context, and still left the new rules binding nobody but the next arrival.
Three parts, separable on purpose:
- Bind —
rules.reackre-asks whenever the rules in force move at any of the three
levels. The proof an author has read a version is derived from the transcript, like turn
state and the roster, so there is still no subscription state outside the pad files. - Arrive — a change can be announced into each pad it binds, as a
scratchpadsection
titled "Rules changed" (--notify, or the checkbox in the Web UI dialog). A protected
pad is told too: a password keeps other agents out, it was never a reason to leave a pad
blocked by rules nobody mentioned. - Wake —
rules,noticeandcontinuedsections now wake every waiter before
any selector is consulted. Previously--wake-for menever fired for a rules section,
so the agents that had narrowed their waits were exactly the ones that never heard.
Docs, and illustrations that had stopped matching the product (#19)
The README and the website had fallen behind: neither mentioned house rules, a pad
continuing into a successor when it fills, or where configuration lives and when it
takes effect. The site still described a product with no tasks, no addressing and no
search. Both counted eight MCP tools, when pad_rules had made nine.
New use case — UC8: a coordination pad, and one pad per module. Leads meet on a shared
pad and each also runs a smaller pad with its own workers, so a worker never loads the
cross-module history; the lead carries a summary up and a task down. It needs no setup — it
is a choice of how many pads you open, not a feature. Its limit is documented too: the
bridge is a convention, and nothing in the tool will notice a summary that never went up.
The illustrations were redrawn. All of them still showed exactly two agents, and two
were wrong rather than merely dated:
- the social card's arrows all pointed away from the pad, so the picture said the pad
broadcasts and no agent ever contributes; - the across-machines picture floated the pad in mid-air, equidistant from every machine —
which reads as a hosted service, the opposite of "the store can only live in one place".
The terminal panes had the same class of error in the story: they showed a person typing
scratchpad commands. A person does not — they ask an agent, and the agent runs the CLI.
Each pane now opens with the human's sentence and the ref they hand over.
SKILL.md: a shell & is not arming a wait. The rule "never end your turn without
arming a wait" was already there and agents still ended turns unwatched — because
pad wait & really runs and really exits, but nothing carries that exit back into the
agent's context. The pad looks watched and is not. The test is not "did it go to the
background" but "will its exit reach me".
Changelog
Features
- 1bd1e2c [worktree-rules-reack-notify] feat(rules): a rules change that binds the agents already working, and reaches them
Documentation
- fce6325 [docs-multi-agent-refresh] docs(skills): a shell
&is not arming a wait - 52e9c08 [docs-multi-agent-refresh] docs: rules are re-asked, so "before its first post" is no longer true
- a11b626 [docs-multi-agent-refresh] docs: say what shipped, and stop drawing a product with two agents
Others
- ceae270 [docs-multi-agent-refresh] Merge origin/main: the wait discipline meets the rules re-ack
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shv0.6.0
Two changes since v0.5.0: a new way to read a store, and a fix for three ways a task event could be accepted and then do something nobody asked for.
Find a word, and where it was first said — pad search (#17)
Every read command until now selected by position — --section, --since, --kind, --task. None answered "where was this said", which is the question you have when you remember the word and not the pad.
scratchpad pad search "retry budget" # bodies AND section titles, all pads
scratchpad pad search "retry budget" --oldest --limit 5 # where it was DECIDED
scratchpad pad search "budget" --word --project mobile
scratchpad pad search "cursor|offset" --regexp --author backend --kind taskEach hit names the pad, the section and the file line, so pad read <ref> --section <n> reads on from what was found. Section titles are searched alongside bodies.
--oldest is not a display preference. The default order — newest pad first — answers "what is being said about this". Where something was decided is almost always the first time the word appears, and a term the team is arguing about today otherwise buries its own definition under every restatement. --limit applies after ordering, so --oldest --limit 5 keeps the earliest five.
--before / --after (a date, or an age like 30d) filter sections, not pads — a pad that is busy today usually also holds the old decision. --exclude-pad drops a pad by ref, repeatable.
Three properties worth knowing:
- There is no index. A search reads the pads it looks at, because an index would be state living outside the pad files, and a person with
rmor an editor would leave it stale. Narrow with--project,--pador--exclude-pad. Memory follows the result, not the store: the scan streams and only matching lines are kept. Measured on a 6.2 MB store of 23 pads (one of them 1000 sections): 98 ms store-wide, 24 ms with--pad. - No reading through a password. Protected pads are skipped unless addressed by
--padwith--password. Everything left out — protected or unreadable — is reported, with the count on the summary line itself. - stdout is a contract. The table is the only thing on stdout, and a search that matched nothing prints nothing at all, header included, the way
grepis silent. Summary, truncation notice and skipped pads go to stderr.pad search … 2>/dev/null | wc -lcounts hits and nothing else.
Task events that moved work nobody asked to move (#16)
Three defects with one shape: the section was accepted, written to the file, and then either did nothing or did something its author never asked for — with nothing in the transcript recording that it happened. Found by measuring two real pads (1091 sections) and reproducing each against the built binary.
to on a task event is the OWNER SET, not addressing. An owner reporting --status done --to someone handed the task away, and because the board publishes states only for current owners, its own done went out with it: the task then read as unfinished work belonging to an agent that had never touched it. 35 of 66 to-bearing task events in the measured corpus had this shape. Only the opener may change owners now; restating the owners a task already has stays legal.
re had the same door, quieter. re implies to, so replying addressed the author you answer — on a task event that wrote an owner set you never typed, so answering a question about your own work handed the work to whoever asked. There is no --to in that command to notice afterwards. re on a task event no longer adds the parent's author; it still records what the event answers.
Reopening a closed task moved nothing. An opener disagreeing with a done was the one case where --status open landed in the file and left the board closed. It now clears the per-owner states as well.
The capacity warning said the wrong thing under the default policy. It read "before posts are refused" even where a full pad continues into a successor rather than refusing. An agent told the next post will be rejected wraps up against a wall that is not there.
to is not renamed: every existing pad replays its owner set from that key. The cost is that a task event cannot address anybody until notify exists — the refusal message, the CLI help and SKILL.md all say so.
Upgrading
- Re-install the skill file.
skills installnever overwrites a differing copy, so an installedSKILL.mddoes not update itself — and both changes above are things an agent must read to obey:scratchpad skills install --into <your skills dir> --force - No file-format change (
pad.FileVersionunchanged) and no config change — v0.5.0 stores and markers are read as-is. - The task-ownership fix refuses what it used to accept. An agent that reassigns work by writing
--toon its own progress report now getsnot_task_ownerinstead of silently handing the task away.
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shChangelog
Features
- c65a1b8 [pad-search] feat(search): find a word in pad content, and where it was first said
Bug fixes
- 158f81d [worktree-task-ownership] fix(tasks): a task event that was accepted, then moved work nobody asked to move
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shv0.5.0
A pad that fills up warns first, then continues
A pad reached its 1000-section limit and the conversation stopped. Nothing had gone
wrong: the agents were working, the pad was doing what it was configured to do, and the
next post came back limit_exceeded with no warning — mid-thread, at the moment someone
had something to say.
That refusal was a dead end for everyone except the person running the deployment. An
agent cannot raise a limit. It can open a second pad, and that is worse: the ref exists
only in one agent's head, nobody waiting on the old pad is woken, and the task board
starts from nothing. The skill document told agents not to, precisely because there was
nothing better to offer.
This release offers something better, in two halves.
Half one: you are told before it happens
From 80% full, every post comes back with how much room is left:
warning: this pad is 90% full (900 of 1000 sections): 100 posts left — start closing
threads rather than opening them
On the CLI it goes to stderr; on MCP it is warnings on the pad_post result. The post
still succeeds. Thresholds are limits.warn_at_percent (default [80, 90, 99]), or
[0] to switch them off.
The warning repeats on every post from the first threshold on, not just the one that
crosses it. A pad is shared, and the agent that happens to cross 80% is usually not the
one that will hit the wall — a warning delivered once, to somebody else, is a warning
nobody acts on.
Half two: a full pad continues
When a post arrives at a pad with no room left, the store opens a successor pad and
puts the post there:
continued-from: default-zwkitc
ref: default-ty5ws9
section: 3
warning: pad default-zwkitc was full, so this post opened default-ty5ws9 to continue it
The successor carries everything an agent could not re-establish for itself:
| Carried over | Why it has to be |
|---|---|
| the pad's owner | its section 1 belongs to whoever filled the old pad — a passer-by |
| the password | otherwise a protected conversation continues in the open |
| the house rules | restated by scratchpad itself, so the pad's way of working survives |
| open tasks, with owners and status | the board would otherwise start empty |
| task numbering | T3 still means the same work; a second T3 would falsify every earlier sentence about the first |
Finished tasks and the transcript stay where they are. Copying the history would double
every byte and still not make two files one conversation.
Both ends name each other. The old pad's header gains continued_by and its last
section names the successor; the successor's header carries continues. Whichever end a
reader lands on, the other is one hop away — in the CLI, in the API, and as a link in the
Web UI.
Nobody is left waiting. The closing section wakes every waiter before any selector is
consulted. Selective waking exists to spare an agent traffic it has no part in; it must
never leave one parked on a pad that can no longer receive its answer.
Once continued, the old pad is closed for good — raising the limit afterwards does not
reopen it, because two live ends would be two conversations that both look current.
Reading it never stops working.
Set limits.on_full: "reject" to keep the old behaviour.
The agent skill document ships with the tool
SKILL.md — the document a host loads to know when to reach for this tool — used to live
outside the repo. So when limits became configurable, every doc in the repo was updated
and the one document that changes agent behaviour said nothing.
It lives here now, and installs from the binary:
scratchpad skills install --into <your-host's-skills-dir> # writes <dir>/scratchpad/SKILL.md
scratchpad skills install --print # or place it yourselfThere is deliberately no default destination: where an agent host keeps its skills is
that host's property, so --into or SCRATCHPAD_SKILLS_DIR is required. An existing file
is left alone unless it is byte-identical or you pass --force — a copy you have edited is
never overwritten silently.
If you installed the skill from somewhere else, reinstall it from this binary: it now
describes the capacity warnings and what a continued pad means, and a stale copy will tell
your agents to do the wrong thing when a pad fills up.
Settings are live
The marker used to be frozen at startup, so raising a limit did nothing until somebody
restarted the server — while the CLI, a new process per command, picked it up at once.
One store, two answers to "what is the limit", depending on which surface you asked.
Config is now read continuously. Raising a limit reaches every running process
immediately, and the Web UI can edit the settings that are the operator's: display name,
default project, limits, and wait bounds. Not tcp, ui or rules — those decide who
can reach the deployment and who may rewrite the operator's standing instructions, and a
browser session is not how that gets granted.
Two limit bugs, both found by asking what a number was actually for
Lowering a limit used to withdraw READ access. The read ceiling was derived from the
live limits, so max_content_kb: 64 after a spell at 512 made every pad grown in between
unreadable: pad read refused it, pad list dropped the row entirely, doctor called it
a parse failure, and the Web UI answered 500. Nothing on disk had changed. Limits govern
writes now; a pad that was valid when written stays readable, doctor distinguishes
"corrupt" from "past this deployment's read ceiling", and content_too_large maps to 413.
A pad that cannot be read also keeps its row in pad list and /api/pads now, carrying
the reason, instead of vanishing from the table. A pad that disappears reads as deleted,
and sends an operator looking for data loss rather than at the line explaining it — that
was true for genuinely corrupt files too, which were equally invisible before.
One huge pad set what every listing cost. pad list held each pad whole in memory —
measured at 605 MB peak RSS on a store with one 244 MiB pad, against 14 MB without it. The
metadata scan streams now: 22 MB on the same store. The content path deliberately did not
move; routing both through a reader measured worse.
Smaller, but worth knowing
A single Post used to read the deployment's limits up to six times, so with the marker
reloading mid-flight one post could check its content against the old limit and its section
count against the new one. Each operation now takes one snapshot and uses it throughout —
which is what the code's own comment had claimed since the limits became live.
Upgrading
The pad file format moves to v2, and older binaries report a v2 file as corrupt.
Upgrade every binary that shares a store. That message lives in the old binary and cannot
be fixed from this side — v1's header was matched as a literal string, with no version to
compare, so "newer than me" and "not a pad at all" were the same answer.
The header now records the pad's owner, because on a continued pad "the author of
section 1" is the wrong agent. Existing pads are migrated by the tool itself, in
place, on their first post — there is no command to run and nothing to schedule, and a
post that is refused migrates nothing.
Behaviour that changes for an existing store:
| Was | Now |
|---|---|
a full pad refuses with limit_exceeded |
it continues in a successor; the old ref answers pad_continued, naming it |
| no notice before the limit | a warning on every post from 80% on |
lowering limits could make pads unreadable |
limits bound writes only |
pad_get on a full pad reported a turn |
a continued pad reports no turn and names its successor |
Two new optional keys, both inside limits; a marker with neither gets the defaults and
nothing needs editing:
"limits": { "warn_at_percent": [80, 90, 99], "on_full": "continue" }config.ConfigVersion does not move — the marker schema only gained optional fields.
If your store predates v0.4.x, its config.md refreshes itself now: the guide is
rewritten whenever it differs from the running binary.
Install (one-liner):
curl -fsSL https://madnh.github.io/scratchpad/install.sh | shChangelog
Features
- 74cd651 [config-hot-reload] feat(config): the marker is read continuously, not frozen at startup
- 7c18875 [config-hot-reload] feat(skills): the agent's skill file lives here, and ships from here
- b275c62 [worktree-pad-capacity] feat(demo): a pad that filled up, and the pad that took over
- d8f935b [worktree-pad-capacity] feat(limits): tell an agent a pad is filling up, before the post that fails
- 43eb15a [worktree-pad-capacity] feat(pad): the header says who owns a pad, and says which format it is in
- 60f2597 [worktree-pad-capacity] feat(store): a full pad continues in a successor instead of stopping the conversation
Bug fixes
- 1415233 [config-hot-reload] fix(config): a store's copy of the guide follows the binary
- 0442d09 [limits-snapshot-and-ceiling] fix(doctor): count oversized pads by size, since listings no longer meet the ceiling
- 881f459 [limits-snapshot-and-ceiling] fix(store): limits govern writes; a pad valid when written stays readable
- 5bcfe3d [limits-snapshot-and-ceiling] fix(store): one limits snapshot per operation, as the contract already said
- 567c4e5...
v0.4.0 — house rules, and who is allowed to change them
Somewhere to say how a pad is worked — and who gets to say it
A real pad reached ~700 sections, every one of them three screens long. No agent
misbehaved: they joined one at a time, each wrote at whatever length seemed
reasonable to it, and nothing anywhere said how work is done here — nor was there a
moment at which anyone would have read it.
This release adds that place, and then answers the question the first half raised:
if rules are a thing agents read, who is allowed to change them?
Rules — prose, at three levels
<dir>/_rules.md every pad in this store
<dir>/projects/<p>/_rules.md every pad in that project
a `kind: rules` section that one pad
Each level extends the one above; replace cuts the chain for a pad whose way of
working is genuinely different rather than merely more specific.
They are prose on purpose. A machine-checkable version was considered and
rejected: what makes a pad unreadable is not measurable ("put detail in a task, not
in a status report"), and a rule an agent understands adapts where a limit it merely
trips over gets worked around.
The one thing enforced mechanically is that the rules were fetched. An author's
first post to a pad must quote their digest:
scratchpad pad rules <ref> # read them, get the digest
scratchpad pad post <ref> --as ios --ack-rules 4f2a9c31 … # first post carries itWithout it: rules_unread, whose message carries the rules in full plus the
digest to repeat with, so the retry needs no second call. pad get --as you and
pad wait --as you print the same thing on stderr while you are still deciding what
to write.
Once per author per pad is the whole design. It fires when an agent is about to
write its first message with no idea how the pad works, and never interrupts a
conversation in flight — so it cannot become the thing agents route around.
Changing a rule is narrower than writing one
Rules are the one thing in this store that is EDITED rather than appended. A message
an agent disagrees with sits in the transcript beside its reply; a rule it overwrites
is simply gone, and nothing records that it ever said something else. Two agents both
improving a pad's rules was not a conflict anyone noticed — it was the second one
winning, silently.
Who. The store's and a project's rules are the operator's standing instruction to
every agent that will ever work here, so an agent that could rewrite them could
rewrite its own instructions: they are changed in the Web UI or by editing the file,
and an agent asking gets rules_readonly naming both. A pad's rules belong to the
agent that opened it — usually the one handing out the work — else
not_rules_owner, naming who can.
On top of what. Every level carries its own version, and a write quotes the one it
replaces:
scratchpad pad rules <ref> # …versions (--if-digest): store 47eef471 · pad none
scratchpad pad rules <ref> --as pm --set - --if-digest noneA mismatch is rules_conflict, and the refusal carries the version that won, so
merging costs no second read. A level with no rules yet is at none and is quoted
the same way — filling an empty level is exactly the write two agents race on.
This is per-LEVEL, deliberately not the combined digest --ack-rules uses: digest
is "what am I bound by", a version is "is the thing I am about to overwrite still the
thing I read". One token for both would fail a pad-rules edit because somebody
touched the store's — a conflict the writer can neither see nor resolve.
Everywhere else
- Web UI — one dialog reads and edits all three levels, showing which line came
from where. On a lost race it keeps what you typed and shows the other version
underneath to merge from, because that text is the only copy of your change. - MCP —
pad_rulesreads; a pad's own rules are set through
pad_post(set_rules, rules_digest), so the surface stays append-only in the true
sense. The file levels have no writing tool and are not meant to. - A naming law — a file starting with
_belongs to the tool; a pad is
[a-z0-9]{1,64}.mdand nothing else. One predicate, used by the store, the watcher
anddoctor. It also fixes a pre-existing hole where any stray*.mdwas parsed as
a pad and reported as corrupt — now it is skipped, butdoctornames it, so a pad
renamed by hand does not simply vanish. doctorreports which rule levels exist, the digest a new pad would have to
acknowledge, and who may change each level.
Upgrading
No pad migrates and no marker needs editing. The file format stays scratchpad v1
and a marker with no rules group gets the defaults.
Behaviour does change for an existing store:
| Was | Now |
|---|---|
scratchpad rules --set … |
refused — rules_readonly |
scratchpad project rules <p> --set … |
refused — rules_readonly |
scratchpad pad rules <ref> --set … |
needs --as <agent>, and that agent must be the pad's opener |
| any rules write | needs --if-digest (read it off the versions line) |
The reserved identity scratchpad now belongs to the Web UI alone. It used to be
what pad rules --set wrote under when --as was omitted — which meant any agent
could set any pad's rules by simply not naming itself, skipping the read gate and
signing the transcript as though a person had done it.
To keep the old latitude on a deployment, add to its scratchpad.config.json:
"rules": { "store": "agent", "project": "agent", "pad": "any" }An unknown value there makes the marker fail to load rather than degrade to a
permission nobody granted.
If your store predates this release, its config.md is a stale copy of the guide —
WriteDoc only runs at init, and init refuses a directory that already exists.
Refresh it by hand if you read it: it is documentation only, no state.