# Changelog

## [Unreleased]

## [0.26.0] - 2026-09-09

### Requires

- **spt-core 0.54.0 or newer**, unchanged. The faster tag pickup below rides spt-core
  0.67's IO frames; an older core installs this release and simply keeps the tick.

### Added

- **Board Files.** The Hub Daemon now writes the board to disk each cycle: one
  Markdown file per State (`backlog.md` … `shelved.md`) plus `milestones.md`, per
  Project it serves, under the node's alchemy config dir. Each line names a Request's
  ref, title, Type, flags, tags, Milestone, last-updated day and GitHub link — never a
  body — so a question about many Requests is a `grep`, not one `view` per Request.
  Files are rewritten only when something changed, and carry the time they last did.
- **`board [--json]`** tells an agent where those files are: the Project, the as-of
  time, then one line per State and one for Milestones with its count and absolute
  path. It refuses by name when the bound repo is not a Hub Project on this node, or
  when the daemon has not written yet — never an empty listing. A new keyword hint
  (`grep the issues`, `board files`, `issue list` …) points at it.
- **Tags fire sooner.** A `#< … #>` tag is now picked up the moment your next prompt
  reaches the agent, or the moment a turn closes, instead of waiting for the scanner's
  next tick. Nothing else about scanning changed: the tick, the idle-edge scan and
  the activity gate are as before, and a tag is still never fired twice.

### Fixed

- **A Request ships in exactly one release, and the board agrees with the record.**
  `release <tag>` and the Releases-thread cards now share one rule: a Request that
  closed after the previous release published and no later than this one's publish
  time plus the 30-minute grace ships in this release, and a close the grace claimed
  is never counted again by the next card. A Milestone whose closes landed a minute
  after publish was attributed by `release` to one version and by the board to the
  next; both now name the earlier one.
- **A release card never re-lists a Request an earlier card already showed.** A later
  commit whose subject merely named a shipped Request (a docs follow-up, say) made the
  next card announce it again. Cards are recomputed from GitHub every cycle, so both
  fixes heal existing boards on their own — no Request is edited.

## [0.25.0] - 2026-09-08

### Requires

- **spt-core 0.54.0 or newer**, unchanged.

### Fixed

- **Hints no longer imply that `create` is missing.** An agent who tried
  `spt shell cmd <instance> create` was refused with a message listing "the shell's
  vocabulary" as the `cmd` ops only, and the first hint pointed at `shell cmd <verb>`
  generically, so the one verb every newcomer needs first looked absent. The `request`
  and `alchemy` hints now spell out that `create` and `comment` carry free text and ride
  `spt shell send <instance>` (verb on line 1, title on line 2, body after), and the
  filing hint also fires on `create`, `new request` and `open an issue`.

## [0.24.0] - 2026-09-06

### Requires

- **spt-core 0.54.0 or newer**, unchanged. Keyword hints surface on spt-core 0.67.0 and
  newer; an older core installs this release and simply shows none.

### Added

- **Keyword hints.** alchemy now declares eleven manifest hints that spt-core surfaces
  to an agent, once per session each, when the agent's own words touch the Hub. Say
  "request", "gh issue", "backlog", "my bag", "milestone", "dispatch", "watch", "release"
  or write a `#<` tag, and the hint names the alchemy verb those words were reaching for,
  with the `#< … #>` shortform and the `spt shell cmd <instance> …` spelling side by
  side. The first hint any agent meets steers them off raw `gh issue` and onto the
  shell, which holds the credentials and keeps the board true. An agent holding an
  alchemy instance sees the full text; one holding none is told a hint exists and to run
  `spt adapter hints alchemy`, which prints the whole set on any node.

## [0.23.0] - 2026-08-21

### Requires

- **spt-core 0.54.0 or newer**, unchanged from 0.22.0. On an older core the install is
  refused with a message naming the version — update the core first, then update
  alchemy as the very next command.

### Added

- **A new `type` verb, for a request that never got a type.** `type #42 bugfix` fills in
  a missing type. It only ever *fills*: a request that already has a type is refused,
  and the refusal names the type it is keeping. A type records the form a request
  arrived in, so it is set once when the request is created and never changed — this
  verb exists for the one case where it never got set at all, which is a request filed
  straight onto GitHub instead of being raised through Discord or `create`. Filling
  `idea` also marks the request needs-operator, exactly as creating one does.

### Fixed

- **Thread counts in the pinned contents post are right again — every one of them was
  one too high.** The count of each thread included a message Discord itself had
  written (the "renamed this thread" notice left behind when threads were given their
  project number), so a thread with two cards read `3` and an empty one read `1`. Only
  the count was ever wrong; nothing was misfiled. The numbers correct themselves on the
  next cycle, with nothing to do on your side.
- **The daemon log stops reporting the same handful of harmless posts as orphans, for
  ever.** Every cycle logged an "unparseable" line for each state thread and both
  milestone threads, on every project — around fifty lines a cycle, and nearly three
  thousand in a single run. They were the same Discord "renamed this thread" notices,
  which are not alchemy's posts at all. That list exists to surface a card that has
  genuinely lost its way, and a real one had no chance of being noticed inside the
  noise. It now carries only posts that are genuinely unexplained.
- **A request with no type is no longer invisible.** Its card showed a bare `?` where
  the type goes, which read as ordinary board furniture — one such request sat
  unnoticed for seventeen days, because it *did* have a state and the board looked
  fine. The card now says plainly that the type is missing and names the verb that
  fills it, and the daemon says so once in its log too. Both clear themselves the
  moment you fill it in.

## [0.22.0] - 2026-08-21

### Requires

- **spt-core 0.54.0 or newer**, unchanged from 0.21.0. On an older core the install is
  refused with a message naming the version — update the core first, then update
  alchemy as the very next command.

### Fixed

- **A busy board stops re-announcing work it finished long ago.** On a project whose
  Done thread had grown past a hundred cards, the bot posted fresh cards — and fresh
  update notes — for requests that had been done for hours, days or weeks, and did it
  again on every cycle. Discord hands out a thread's history one page at a time, and
  alchemy was reading only the first page and treating it as the whole thread: any card
  below that line looked like a card that had never been posted, so it got posted
  again. Every history alchemy reads now goes all the way to the end of the thread.
  Nothing to do on your side — the noise stops on its own, and the duplicate cards it
  left behind are cleaned up as the board settles.
- **Cleaning up duplicate cards keeps the original.** When a request has ended up with
  more than one card in the same thread, the one that stays is the oldest — so a card
  keeps its place in the thread and its link keeps working, instead of jumping to the
  bottom.
- **Thread entry counts in the pinned contents post are accurate again.** They came
  from the same first-page read, so any thread past a hundred entries had been
  reporting a hundred.
- **Seeds are no longer missed because a channel got busy.** The recovery pass that
  re-lists seeds, and the tidy-up that removes the "started a thread" notices, both
  read only the first page too.

## [0.21.0] - 2026-08-04

### Changed

- **Requires spt-core 0.54.0 or newer.** This release raises the floor and depends on
  it outright, so install the core update first. What 0.54.0 changes for alchemy: when
  you send text and a file in one command, the file now arrives *before* the text
  instead of after it.
- **Update alchemy as the first thing you do after updating spt-core** — not later in
  the same session, and do not assume your machine's own update sweep did it. Until
  you do, you are running the previous alchemy against the new frame order, which is
  the exact misdelivery this release removes: a file sent with a create attaches to
  the Request you made *before* it, and the one you were actually creating comes out
  empty. Nothing warns you — it looks like it worked. Installing alchemy 0.21.0 on an
  older core is refused with a message naming the version, so the only gap you have to
  close yourself is this one.
- **A file you send with a create or a comment now lands in that same write.** This
  always worked, but it used to work by repair: alchemy noticed the file arriving a
  moment too late and edited it into the Request or comment afterwards. Now the file
  is simply already there when the write happens. You should see no difference except
  that a `send --file` no longer produces a second edit to the Request.

### Removed

- **The retro-apply window is gone** (the repair described above). It could not stay:
  with the frames arriving in the corrected order, the same code would have attached
  your file to the Request you created *previously* and left the one you were actually
  creating empty — a real misdelivery, not a cosmetic one, and one that silently
  affects the ordinary case of minting a few Requests in a row where one carries a
  file. This is why the version floor and the removal ship together in one release: on
  spt-core 0.54.0 the repair is harmful, and below it the repair is needed.

## [0.20.0] - 2026-08-03

### Changed

- **`create` now refuses a body longer than 1,600 characters** instead of minting it.
  The refusal tells you the length, the limit, and what to do: mint the Request with
  the ask itself, then add the detail as a comment on it. Comments are not limited —
  they are where the discussion belongs. The limit counts only the text you supply
  (the `Requester:` line and the `Attachments:` block do not count against it), and it
  applies the same way to a `--file` whose text fills the body. Seeds written in
  Discord are unaffected. Because the limit sits below the card's display budget, a
  Request created this way always shows in full on its card.

## [0.19.0] - 2026-08-03

### Added

- **`/request 42` looks up one Request.** It answers privately with the Request's
  title, its current State, and links to both places it lives — its card on the board
  and its issue on GitHub. Run it in the Project's channel or any of its threads; that
  is what says which Project the number belongs to. It needs no special capability,
  since everything it shows is already on the board. A Request minted moments ago has
  no card yet, and the answer says so and still links the issue. `/alchemy-help` now
  mentions it.
- **Messages that expire say when they expire.** The receipt you get when a Seed is
  promoted, and the notice telling you a Request left your bag, now end with a small
  line naming the time they will be removed. It renders in your own timezone.

### Changed

- **Thread names now carry their Project's number** — `Backlog 0`, `Backlog 1`,
  `Seeds 0`, and so on for every thread the bot maintains. Searching Discord for a
  thread name used to match the identically-named thread of every Project at once;
  now it finds one. Existing threads are renamed in place on the first run after this
  update — nothing is recreated, no cards move, and the pinned Contents post uses the
  new names too. The number comes from the order Projects are registered in, so if a
  Project is removed the remaining threads are renumbered, again by renaming.

### Fixed

- **A file sent together with a comment now attaches to that comment.**
  `spt shell send <shell> "comment #42 …" --file notes.txt` used to post the comment
  with no attachment and then hold the file back for your next `create`, where it
  would have been silently read as that Request's body. The file now lands on the
  comment it was sent with, as an attachment, above the signature line. Sending a file
  on its own still stages it for your next `create` exactly as before.

## [0.18.0] - 2026-08-02

### Changed

- **Long Request bodies are now shortened on the Discord card.** A card mirrors up to
  about 1,800 characters of the body and then stops with an ellipsis; the card's title
  still links to the Request on GitHub, where the full text always lives. The
  `Requester:` line and the whole `Attachments:` block are kept no matter how long the
  body was, so you never lose track of who asked or what they attached. Cards shorter
  than that are unchanged.
- **Bag-drop notices now clean themselves up.** The `🎒 dropped from your bag` message
  posted in a Project channel when a Request leaves the assembly is deleted eight hours
  after it was posted. It is a heads-up for whoever lost the entry, not a permanent
  record — the Request itself keeps the history. Nothing else the bot posts is affected,
  and a notice still standing when the daemon restarts is still removed on time.
- **A Project channel can now be moved to any category.** If you drag a `⚙` channel out
  of `「Alchemy」` and file it wherever you prefer, the bot now finds it where it is and
  keeps using it, with all of its threads, Seeds and pinned Contents post intact. It
  will not move the channel back, and it will not build a second empty one beside it.
  Previously, re-filing a channel made the bot treat it as missing and start over.

### Fixed

- A Request body or release roundup containing emoji or other non-Latin text could crash
  the card render at the exact length where the text had to be shortened. The cut now
  always lands cleanly between characters, and never in the middle of a link.

## [0.17.1] - 2026-08-01

### Fixed

- **`release <tag>` no longer no-ops the first time you run it after `sweep`.**
  It asked GitHub to filter by `state: ACCEPTANCE` server-side — the one label the
  `sweep` seconds earlier had just written, and GitHub's label index lags a write by
  seconds. So the verb read an empty list and answered `nothing to promote`, which is
  indistinguishable from the real empty answer; re-running it moments later worked.
  Three releases in a row hit this (v0.15.0, v0.16.0, v0.17.0). It now reads the
  closed list and matches the label locally, exactly as `sweep` always did — no
  timing dependency, no retry.

  This also revises an earlier diagnosis: the 30-minute grace window added in 0.16.0
  was written for the v0.15.0 no-op, but the no-op recurred twice with grace live.
  Grace fixes a real, separate cut-ordering hazard and stays; it was never what was
  breaking this.

- **A lagging label index can no longer mint a second Watch masterlist**, or tell you
  there are no Milestones to join moments after you minted one. Those reads take the
  label filter but re-check an empty answer without it before acting, so a negative is
  never acted on unconfirmed.

## [0.17.0] - 2026-08-01

### Changed

- **`/alchemy-help` teaches the reaction key.** A "React to promote a seed" section
  now follows the Capturing paragraph — every Type reaction with what it promotes to
  (🐛 Fix · 🔧 Change · ➕ Add · ✨ Feature · 💡 Idea, with the Idea flag noted as
  subtext). An Assembling tip suggests things to ask an SPT agent, and thread and
  button names are bold throughout.
- **"Dropped from your bag" notices post in the Project channel.** They used to ride
  the Updates thread's transition note — and because they @-mention whoever lost the
  bag entry, that auto-joined you to Updates. The transition note stays in Updates
  (mention-free); the drop notice, with the mention, lands in the main channel.

## [0.16.1] - 2026-08-01

### Fixed

- **Release cards no longer adopt a Request because prose mentioned its number.**
  A commit body sentence like "the 129-cycle #15 loop" was enough to put #15 (and
  its Milestone) on the v0.16.0 card. A ref now counts only from the commit's
  subject line, or from a body line led by `fixes` / `closes` / `resolves`. The
  polluted card corrects itself in place.

## [0.16.0] - 2026-08-01

### Added

- **Comments sign who wrote them.** Every comment posted through alchemy now ends
  with a small signature naming the endpoint and machine it came from
  (`*-flynn@NODE*`), so a board conversation reads like one.

### Changed

- **Release cards name what actually shipped — whatever state it's in.** A release's
  card now lists the Requests its own commits name (`#N` / `fixes #N` between the
  previous tag and this one), grouped under their Milestone, even if a Request is
  still open. The six release cards that read "(no tracked Requests in this
  release)" fill themselves in retroactively.
- **`release <tag>` forgives a slightly-late close.** A Request closed within 30
  minutes after the release published still gets promoted and named in the roundup
  (marked as a late close) — no more republishing a release just to move its
  timestamp past the closes.
- **Thread hide times match their traffic.** Updates hides after 1 hour; Inactive
  Milestones, Cut, and Done after 24 hours; every other alchemy thread stays
  visible a week (was: everything 3 days).

### Fixed

- **Cards with Discord custom emoji stop refreshing forever.** A Request body
  carrying a token like `<a:AttentionAnimated:…>` made its card re-edit on every
  cycle, indefinitely. Cards now render the emoji short-code Discord actually
  stores. If a card ever loops like this again, the daemon log names the drifting
  field and calls the loop out after 3 quiet-issue cycles instead of scrolling
  anonymous `refreshed` lines.
- **`spt shell send <text> --file <path>` in one command no longer strands the
  file.** The text half used to arrive first, minting the Request empty and
  leaving the file stuck in staging. The file now catches up to its own create —
  text lands in the body, non-text attaches — and the answer says so.

## [0.15.0] - 2026-07-31

### Added

- **`/alchemy-help` — the board tour, in Discord.** A new slash command that answers
  privately (only you see it) with how the Requests board works: Seeds and the Type
  reactions that promote them, the 🎒 Bag, staging a Milestone with `/new-milestone`
  and `/pick-milestone`, the card controls (Needs Operator, Change State, Edit), and
  how to read the State threads. It works anywhere in the server and for everyone —
  it writes nothing, so no Alchemist capability is needed. Same doctrine as
  `process-guide operator-discord` on the shell, dressed for Discord.

## [0.14.1] - 2026-07-31

### Changed

- **Sending a file with `create` now fills the request body.** `spt shell send … --file
  <path>` used to always upload the file and link it as an attachment; agents sending a
  written-up request as a file intuitively mean "this is the body", and now that is what
  happens — the file's text becomes the request body, after any inline body lines. Add
  `--attach` on the create line to get the old attachment behavior. A comment with a
  file still attaches it, and a file that is not text (a screenshot, say) attaches under
  either verb — the answer tells you when that happened.
- **A file can be staged before the repo is bound.** The upload now happens at the
  write, not at file arrival, so sending `--file` to a fresh instance no longer refuses.

## [0.14.0] - 2026-07-31

### Added

- **`process-guide` — how the Hub is meant to be used.** A new read verb: bare
  `process-guide` prints an index of four sections, and `process-guide <section>`
  expands one. `lifecycle` is the intended flow of a Request, its Milestone, and the
  release that ships it; `operator-agent` is prompt starters for what to ask your agent
  at each point; `operator-discord` covers the board's features (Seeds, reactions, the
  Bag, staging, card controls) and when to use each; `shell` is everything an agent
  needs to drive alchemy consistently. Works before a repo is bound, exactly like
  `help`. Asking for a section that does not exist names the four that do.
- **A Request added to a Milestone already under way catches up.** `milestone add` now
  moves a fresh joiner to the later of its own state and the Milestone's — capped at
  WIP, so nothing skips ahead to acceptance or done. Requests already further along,
  and anything done, cut, or shelved, stay where they are. A joiner still flagged
  needs-operator is attached but held below greenlit, and the answer says so by name.

### Changed

- **Release announcements group shipped Milestones separately.** The Releases-thread
  card now lists `Shipped Milestones:` above `Shipped Requests:`, so a Milestone no
  longer reads as one more Request in the count.
- **Every ref in a release announcement is a link.** Each `#N` in the roundup now
  links to its GitHub issue; titles follow the link as plain text.

### Fixed

- **A freshly minted card can be bagged immediately.** The 🎒 reaction used to arrive
  up to a reconcile cycle (~5 minutes) after the card; it now lands in the same act
  that posts the card, on mints and relocations both.

## [0.13.1] - 2026-07-29

### Fixed

- **The `bags` answer names people again.** v0.13.0 read names from the guild's member
  list, which this bot has no permission to fetch, so every Bag was labelled with a raw
  Discord id. Names are now looked up per person through a call that needs no special
  permission, and anyone whose name genuinely cannot be read still shows as their id
  rather than dropping out of the list.

## [0.13.0] - 2026-07-29

### Changed

- **Marking a Request for your next Milestone is now your Bag, and the reaction is 🎒.**
  The old word collided with "build target" in ordinary conversation, so the whole thing
  is renamed: you put a Request **in your Bag**, your Bag is what Finalize empties, and
  the board seeds 🎒 on every Backlog and Eval card. **Anything you already marked with
  the old 🎯 carries over automatically** — the daemon moves it onto the card on its next
  pass and clears the old reaction. Nothing you had marked is lost, and you do not have
  to re-mark anything.
- **The `targets` verb is now `bags`.** Same answer, and it now leads with `Bagged
  Requests in <owner/repo>` rather than a bare word that reads like a list of goals.
  `--json` unchanged apart from the key.
- **The bags answer names people.** The bot owner never held the Alchemist role, so their
  Bag was labelled with a raw Discord id instead of a name. Fixed.

### Fixed

- **The Change State panel no longer lingers, and no longer keeps acting.** Picking a
  State used to leave the dropdown sitting in the channel indefinitely — and still live,
  so picking again wrote another State change to a Request that had already moved. The
  panel now closes itself: it reports what it did, then disappears once the card has been
  updated. If something fails it stays put and tells you what broke.
- **The Change State dropdown no longer offers the State the Request is already in.**
  That option did nothing when picked.

## [0.12.0] - 2026-07-28

### Added

- **Finalize turns your picks into a Milestone.** Press **Finalize** on your staging
  post and the Requests you have picked are attached to a Milestone as sub-issues.
  `/new-milestone` asks for a title and a body and mints it; `/pick-milestone` shows you
  the Milestones sitting in Backlog or Eval and lets you choose one. Your picks are read
  **at the moment you press it**, not from the list the post happens to be showing — so
  a Request you picked seconds ago is included, and one you took back is not.
- **Finalizing clears the picks it used.** Your 🎯 comes off each Request it assembled,
  so the next Milestone starts from an empty list. If a pick cannot be cleared, the
  answer says so by name and so does the Updates thread — that Request is still picked
  and would be assembled again next time, and you should know which one.
- **One Updates note per Finalize**, naming the Milestone and every Request that went
  into it. The staging post is removed once it has been spent; press Finalize with
  nothing picked and nothing is created at all.
- **Request cards show which Milestone they belong to**, as a link to the Milestone
  with its title. A Request that is not in one shows no such line.
- **A new `targets` verb.** `targets` answers with the Requests currently picked in
  Discord, grouped by who picked them and by which thread they are in — a grouping is
  left out when everything shares it. Add `--json` for the machine shape. The answer
  comes from the Hub Daemon's most recent pass over the board, so it states the time it
  was read: it is as fresh as the last cycle, not live.

## [0.11.0] - 2026-07-28

### Added

- **Mark a Request for the next Milestone by reacting to it.** Every Request card in
  the Backlog and Eval threads now carries a 🎯 reaction, already placed for you — click
  it and that Request is one of your picks. Picks are your own: two Alchemists can each
  be assembling a different Milestone at the same time without touching each other's
  lists, and a reaction from someone without the Alchemist role does nothing at all, the
  same way it does nothing on a Seed.
- **Your picks survive a Request changing State.** A Request that moves between Backlog
  and Eval is re-posted as a new card, which used to mean everything on the old card was
  gone. Your picks now move with it, and the new card says who is carrying one. A
  Request that moves further along — to Greenlit or past it — is no longer something you
  can pick, so those picks are released, and the Updates thread says so and names who
  lost one. Nothing disappears quietly.
- **Taking a pick back.** Remove your reaction, as you would expect. On a card your pick
  was carried onto there is no reaction of yours to remove — Discord does not let a bot
  put one back for you — so on those, clicking 🎯 clears the pick instead.
- **`/new-milestone` and `/pick-milestone` open a staging post.** Run either one in a
  Project channel and a pinned post appears in Backlog listing your picks — each with a
  link to its card and a link to the issue — with **Finalize** and **Cancel**. Running
  the command again edits that same post rather than opening a second one, and the post
  keeps up with your picks on its own as you add or drop them. `/pick-milestone` tells
  you straight away if there is no Milestone in Backlog or Eval to add to, instead of
  letting you assemble a list and discover it at the end.

### Known limitation

- **Finalize does not assign yet.** The button is there and says so when pressed;
  assembling the Milestone from your picks lands in the next release. Your picks and the
  staging post are left untouched in the meantime.

## [0.10.0] - 2026-07-28

### Added

- **The shell now tells you when it skipped one of your turns.** The tag scanner reads
  your session turn by turn, and a turn it cannot key on is skipped. Most of those are
  session-preamble turns that never contain a tag, and those stay silent as before. But a
  real turn of yours arriving without the key the scanner needs would have been passed
  over without a word — so a tag you wrote in it would simply never have run, and nothing
  would have said so. You now get a message naming the turn that was skipped and telling
  you to re-issue anything you wrote there. It arrives on your own poll, so it never
  interrupts you when you are idle. You should never see this message: it exists to make
  a failure that is supposed to be impossible impossible to *miss*.

## [0.9.1] - 2026-07-28

### Fixed

- **The Change State button now appears on cards that were already on the board.** In
  0.9.0 it was drawn only on cards the bot posted or edited afterwards, so a Request
  sitting quietly in a thread kept its old two buttons indefinitely — the control was
  shipped and unreachable for exactly the Requests you were most likely to want it on.
  A card's buttons are now checked like the rest of it, so every card picks up the
  control on the next cycle, in place. Cards that are already current are left alone.

## [0.9.0] - 2026-07-28

Everything here happens on the Discord board: a request card you can **act on**, and a
releases thread that finally looks like the rest of the board.

### Added

- **Change the State of a Request from its card.** Every Request card now carries a
  **Change State** button beside Needs Operator and Edit. Clicking it opens a private
  dropdown listing every State — you pick one and the card moves itself to the right
  thread. You need the Alchemist role, same as the other controls.
  - **Greenlit is not in the list.** A Request reaches Greenlit only by riding its
    Milestone's greenlight, so offering it on a single card would be offering something
    that cannot happen. If one is submitted anyway, it is refused in the same words the
    `state` verb uses.
  - **Cut and Shelved ask twice.** Choosing either shows a confirm button, and nothing
    is written until you click it. This is the same second step you already get when an
    agent tries to cut something with a tag, so the two ways in are equally hard to do
    by accident.
  - **Milestone cards do not get the control.** Changing a Milestone's State carries
    all of its Requests with it, and that is not something to hand to one unpreviewed
    click.
  - The card behaves like the Edit control does: while the change is in flight the card
    wears the 🔄 mark, and when it lands the refreshed card is the only answer you get.
    A failure or a refusal still tells you what happened.
- **Release announcements are cards now.** A release in the Releases thread renders like
  every other Bot post — a linked title, its roundup of shipped Requests, and its own
  color, distinct from any State's, because a release is not in a State.
  - **Your existing announcements are converted in place**, not re-posted. They keep
    their position, their links and their permalinks, and nothing is announced twice.
  - **A release you edit on GitHub updates on the board.** Retitle it or rewrite its
    notes and the card catches up on the next cycle instead of showing what it said the
    day it shipped.
  - Which Requests a release claims is unchanged.

## [0.8.0] - 2026-07-28

Everything here is one subject: **a shell that tells you which repo it is.** If you run
more than one instance, this release is why you can trust what they answer.

### Changed

- **Every answer to a write now names the repo it wrote to.** Where a reply used to read
  `comment 42 added to #23`, it now reads `comment 42 added to BigscreenVR/spt-core#23`.
  Applies to create, comment, state, tag, flag, link, dispatch, milestone, watch, sweep
  and release. You still *type* a bare `#23` — nothing about the input changed. This
  matters because the old reply was equally true of every instance you have running, so
  driving the wrong one told you nothing was wrong. Reads already named their repo.
- **BREAKING — `daemon project add` no longer takes a repo.** It now registers the repo
  the instance you ran it in is bound to, and a repo typed after it is refused by name
  rather than quietly used. If you were running `daemon project "add owner/repo"`, run it
  from a shell bound to that repo instead. `daemon project "remove owner/repo"` is
  unchanged and still takes an explicit repo — deliberately, because a repo that was
  archived, renamed or deleted can no longer be bound to anything, and you still need to
  be able to take it off the board. Bare `remove` now defaults to the bound repo.

### Added

- **`bind-repo` renames the instance to the repo you bound it to.** After
  `bind-repo BigscreenVR/spt-core`, that instance answers to `spt-core` — so you drive it
  by the repo's name instead of `alchemy-3`, and you no longer have to remember to set
  that yourself. If the rename cannot be done (the name is taken, say), the bind still
  stands and the reply tells you both things plus the one command to finish it by hand.
- **`bind-repo` now tells you whether a Discord board is coming.** A successful bind that
  is followed by silence used to leave you assuming a board would appear. It now says
  which of three things is true: the repo is already on this machine's board, or it is
  not and `daemon project add` is what puts it there — offering that is the user's call,
  not something an agent should just do — or there is no Discord bot set up here at all,
  in which case no board is coming for anything until `daemon setup` runs.

## [0.7.3] - 2026-07-27

### Fixed

- **`alchemy daemon` with no subcommand now prints usage instead of crashing.** Typing it
  bare produced a Rust stack trace. It now lists the subcommands it takes. An unrecognised
  one says so by name and lists them too, and both exit with a code a script can tell apart
  from a real answer.

## [0.7.2] - 2026-07-27

### Fixed

- **Images on request posts, actually this time.** v0.7.1 uploaded the picture with the
  post, which was the right idea, but it fetched the file from a link that does not work
  for anything but a signed-in browser — so the fetch failed, and it failed quietly, and
  the post came out with no image again. The file is now pulled the way GitHub's API
  expects, and if a picture ever cannot be fetched the bot writes a line saying which one
  and why instead of showing you an empty post.

## [0.7.1] - 2026-07-27

Three things v0.7.0 got wrong on the board, and one notice that had outlived its job.

### Fixed

- **Images on request posts actually appear now.** v0.7.0 pointed the post at the file
  where it is stored, and that store is private — so Discord could not fetch it and
  quietly showed nothing at all. The picture is now uploaded with the post itself.
  Existing requests pick theirs up the next time their post refreshes.
- **A filename no longer shows up with a stray `!` in front of it.** Attachments listed
  on a request post read `!screenshot.png`. The `!` belongs to GitHub, which uses it to
  render the picture; on Discord it was just a stray character. Gone from the post, kept
  on GitHub where it does something.
- **The 🔄 on an edit now lasts as long as the edit does.** It used to disappear the
  moment the change was saved, while the post still showed the old text — so it cleared
  before there was anything to see. It now stays until the post has actually been
  updated.

### Changed

- **A promotion receipt now stays for 12 hours** instead of two minutes, and it is
  cleaned up by the bot's regular pass rather than a countdown — so restarting the bot
  can no longer leave one sitting in the channel forever.
- **A successful edit no longer pops up a "updated (refreshing)" message.** The 🔄 and
  the updated post say it better. If something goes wrong — or the change is refused,
  like setting Needs Operator on a request past Eval — you still get told, in words.

## [0.7.0] - 2026-07-27

Nine changes to what the Discord board actually shows you: the order it puts things
in, what a release post claims, pictures, and what happens to a seed nobody was
around to catch.

### Added

- **A picture posted with a request now shows up on its card.** If a seed had an
  image attached, the request's post in Discord carries that image instead of just
  naming the file. Requests filed before this release pick their picture up the next
  time the board refreshes — you do not need to re-file or edit anything.
- **Promoting a seed now tells you where it went.** Click a type reaction and alchemy
  replies in the channel with the emoji you pressed and a link straight to the new
  request's post: `🐛 promoted: #13 | per-repo live issues list files`. The message
  clears itself after a couple of minutes, so the channel stays seeds-only. Before
  this, the seed and its Seeds-thread entry both vanished on promotion and left you
  with nothing to click.
- **The Contents post now opens with your project's name, linked to its repo.** One
  click from the pinned post to GitHub.
- **Seeds posted while the bot was offline are picked up when it comes back.** A seed
  written during an outage used to be stranded: no type reactions, no Seeds-thread
  entry, and no way to promote it — you had to notice and repost it yourself. Alchemy
  now finds those seeds on its next pass and captures them normally.

### Fixed

- **A project you add with existing history no longer lists everything backwards.**
  Adding a repo that already had requests and releases filled its threads
  newest-to-oldest, so the Releases thread read upside down. Threads now read oldest
  at the top, the same way a project that grew on the board looks.
- **A release post no longer claims the previous release's requests.** Every release
  announcement listed everything ever shipped, so each post repeated the one before it
  and the same request appeared as "shipped" in release after release. A release now
  names only what shipped in it — what closed since the previous release went out.
- **The post-update notice no longer sends you to a verb that does nothing.** After
  `spt adapter update alchemy`, alchemy's own closing note said to run
  `spt shell relink <id>` on each running shell. For a shell that is still online that
  answers `SHELL_ALREADY_ONLINE: relink is the online switch — nothing to do`, which
  reads like "you're fine" while the instance keeps running the previous binary. The
  note now says what is actually true: running shells adopt the new binary at their next
  relaunch, nothing forces it while they are online, and if you need it now you end that
  instance's process and relink it — same instance, same repo binding.

### Changed

- **Project channels are now named `⚙spt-progress-tool` instead of `_spt-progress-tool`.**
  Your existing channel is **renamed in place** — same channel, same threads, same
  history, same pinned post, nothing to move. Who holds write control does not change
  either: the Alchemist role keeps its current name, so nobody loses the capability and
  no role needs re-granting.
- **Image attachments embed in the GitHub issue instead of showing as a filename.**
  A `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp` or `.bmp` attached to a request — through
  `--file` or carried across from a seed — now renders as the picture on the issue page.
  Other file types link exactly as before.
- **Editing a request from Discord shows you it is working.** Submitting the Edit form
  puts a 🔄 on the request's post while the change is being written, and takes it off
  when the write lands — including when it fails, so the marker never sits there
  spinning.

## [0.6.0] - 2026-07-27

Two ways alchemy could lose your words without saying so. Both are closed.

### Fixed

- **Request links in Discord render as links again when a title contains `](`.** A
  Request titled something like ``add: `](x)` as a header`` used to flatten its whole
  Updates line — the `#42`, the link, the title, all of it — into flat literal text.
  Discord's link parser does not stop where a link ends: it keeps looking, finds the
  `](` in the title, and treats everything up to it as one broken link. Titles now go
  out with a space between that bracket and paren — the only character alchemy changes,
  and every other character of your title is untouched. This affects Updates notes,
  Seeds-thread entries, and the sub-issue lines on a Milestone card, where a single
  awkward title used to break the links on every other line in the same post.

### Changed

- **A verb that cannot use an argument now refuses it instead of ignoring it.**
  `comment #42 this text` posted the body from line 2 and silently threw away
  `this text` — the reply said SUCCESS and the words were gone. It now stops and
  tells you which token it cannot take and where the text belongs:
  `comment takes 1 positional arg (got 'this') — the body goes on line 2 and below`.
  The same check is on `view`, `state`, `link`, `watch`, `unwatch`, `release` and
  `help`, matching what `create` and `list` already did.
- **Verbs that take a list still take one.** `tag #42 +perf -stale ui`,
  `flag #42 -needs-routing`, `milestone add #1 #2,#3` and a `dispatch` note are
  unchanged — the new refusal is per verb, precisely so these keep working.

## [0.5.0] - 2026-07-27

The Hub Daemon — the background process that runs the Discord board and delivers your
Watch notifications — is now started and kept alive by spt itself, instead of by
whichever alchemy shell happened to come online first.

**This release requires spt 0.44.0 or newer.** An older node will refuse to register it.

### Changed

- **BREAKING: `daemon ensure`, `daemon stop` and `daemon restart` are gone.** Starting
  and stopping the Hub is no longer something alchemy does, so those three do not exist
  any more. Running one tells you so and points you at `spt adapter service status
  alchemy`, which is where the Hub's state now lives. **`daemon status` and
  `daemon project list|add|remove` are unchanged** — reporting on the Hub and choosing
  which projects it carries were never the same thing as running it.
- **`daemon setup` no longer starts the Hub at the end of onboarding.** It finishes the
  same way otherwise — token stored, invite link handed back, server detected, config
  written — and closes by telling you that spt brings the Hub up on its next pass, and
  what to run if it stays down.
- **Bringing your own shell back after a restart is unchanged, but the Hub does it by
  itself.** If the Hub Daemon dies, it is restarted for you within seconds. Nothing to
  run, and no shell has to bind first to make it happen.

### Fixed

- **The Hub no longer dies when the shell that started it is torn down.** It used to be
  started by a shell instance, which made it that shell's child process — so closing
  that shell could take the Hub down with it and silently stop Watch notifications for
  everyone on the machine, until someone noticed. It is now started independently of
  every shell, so there is nothing to take it down.
- **Installing an alchemy update no longer means stopping the Hub by hand.** spt holds
  the Hub across the install and starts it again on the new version. Your own shell
  instances still need to be relinked or closed for the install (they hold the program
  file open) — but the Hub is no longer one of the things you have to shut down and
  remember to bring back.

## [0.4.3] - 2026-07-26

One fix: the masked-link repair 0.4.2 claimed was wrong, and made the render it was
supposed to fix happen everywhere instead.

### Fixed

- **Linked lines render as links again.** 0.4.2 escaped your Request title before putting
  it in the text half of a masked link (`[text](url)`), on the assumption that Discord
  honors backslash escapes there. It does not. An escaped `\]` in that position is a
  backslash and a bracket, which malforms the construct — so instead of protecting the
  line, the escaping dropped the whole thing, URL and all, to literal text with visible
  backslashes. It also regressed titles that worked before: a title containing `[` with no
  closing `]` used to render as a clean link and came back wearing a stray backslash.

  Three live experiments settled it: a title with a code span and a title with brackets
  both rendered literal, while a title with no symbols at all rendered as one clickable
  link. The construct was never the problem; escaping it was.

  Your title is now kept out of the text half entirely. The link text is the part the
  daemon generates itself — the `#N` ref, or a fixed word where there is no ref yet — and
  your title sits beside it as ordinary text:

  | | 0.4.2 | 0.4.3 |
  | --- | --- | --- |
  | Seeds-thread entry | `- [<title>](jump)` | `- [seed](jump) — <title>` |
  | Updates, new seed | `🌱 [<title>](url) — new seed` | `🌱 [new seed](url) — <title>` |
  | Updates, mint | `[#N \| <title>](url) — seed → Backlog` | `[#N](url) \| <title> — seed → Backlog` |
  | Updates, transition | `[#N \| <title>](url) — now <thread>` | `[#N](url) \| <title> — now <thread>` |
  | Milestone sub-issue | `- [#N \| <title>](url)` | `- [#N](url) \| <title>` |

  Markdown inside a title now renders as markdown, out there in the open — your words as
  you wrote them, rather than a broken link.

  Same repair asymmetry as before, stated plainly: milestone posts are the only ones the
  daemon rewrites when they drift, so those fix themselves on the next cycle. Seeds-thread
  entries and Updates notes are written once and never revisited — every one already
  posted stays broken, and there is no migration pass. The Request card carries the same
  facts and has always rendered correctly. Expect one round of milestone-post rewrites
  right after this install, then zero. (Request #6, reported from live use, and the second
  attempt at it.)

## [0.4.2] - 2026-07-26

Two fixes for things that went wrong in the open: a file you attached to a Seed, and a
Request title that contained markdown.

### Fixed

- **A Request title containing markdown no longer breaks the post that links it.** The
  daemon writes masked links (`[text](url)`) in Seeds-thread entries, Updates notes and
  Milestone sub-issue lines, with your title as the text. A `]` in that title closed the
  bracket early and spilled the rest of the line — URL and all — as literal text; a title
  carrying its own link broke the render completely. This repo's Request #5, titled
  ``add: `# [<Owner>/<repo>](https://github.com/...)` as TOC header``, is exactly that shape.

  The text half is now escaped. It is **escaped, not stripped**: a Request whose title is
  about a link must still be able to say so. The url half is untouched, because it is the
  key the Seeds-thread mirror lookup finds an entry by.

  This repairs lines written from now on, and only partly repairs what is already up.
  Milestone posts are rewritten whenever they drift, so a broken sub-issue line there
  fixes itself on the next cycle and then holds steady. A Seeds-thread entry or an
  Updates line that was already posted broken stays broken: those are written once and
  never revisited. Re-post the Seed, or read the Request card, if you need a clean one.
  (Request #6, reported from live use.)

- **A Seed's attachments now survive the mint.** Reacting a Type onto a `#` Seed that
  carried a file minted a Request with no trace of it — and because the mint deletes the
  Seed, and Discord's own file URLs expire with the message that held them, the file was
  not merely unlinked but destroyed. There was no repair path afterwards.

  Each blob is now fetched and re-uploaded to the same rolling `alchemy-attachments`
  draft release a `--file` payload rides, and linked from the issue body — in the write
  that creates the issue, and before the Seed is deleted. A reaction-minted Request now
  carries its files exactly as a `create --file` one does.

  A file that cannot be carried is **named in the issue body and logged**, and the mint
  proceeds anyway: refusing would strand the Seed with nothing to repair it from, and
  silence was the whole defect. (Request #9, reported from live use.)

## [0.4.1] - 2026-07-26

Fixes the `flag` verb over the command channel. **Upgrade straight past 0.4.0** — the
verb it added only worked one of the two ways you can drive alchemy.

### Fixed

- **`flag` now works over `spt shell cmd`.** In 0.4.0 it dropped its arguments there
  and answered `flag needs a ref (#N)` no matter what you passed:

  ```
  spt shell cmd <id> flag "#21" "-needs-operator"    # 0.4.0: "flag needs a ref (#N)"
  ```

  The same request written as a payload (`spt shell send <id> "flag #21
  -needs-operator"`) worked correctly throughout, so the flag itself was never at
  risk — but the error blamed the caller for a ref they had supplied, which is the
  worst way to be wrong.

  Nothing about the verb's behaviour changed; only that it now receives what you typed.

## [0.4.0] - 2026-07-26

Flags can finally be taken off. Until now a Request flagged **Needs Operator** or
**Needs Routing** wore that flag forever — there was no verb that could remove one.

### Added

- **A `flag` verb.** It sets and clears the two flags, using the same token grammar
  `tag` already uses:

  ```
  spt shell cmd <id> flag "#42 +needs-operator -needs-routing"
  ```

  A bare name adds (`flag #42 needs-routing`), `+` adds, `-` removes. Only the two
  real flag names are accepted — anything else is refused by name rather than quietly
  turned into an ordinary label, so a typo cannot invent a flag that nothing filters on.
  `tag` still refuses these names, exactly as before; `flag` is the one way in.

  **Needs Operator can only be *set* on a Request in Backlog or Eval** — those are the
  only States it belongs in. **Clearing it works in any State**, and that is the point:
  a Request that shipped and closed while still flagged is precisely the one that needs
  the flag off, and it was the case with no way out.

  Clearing Needs Operator posts a short comment naming who cleared it. Clearing that
  flag says the Operator discussion happened, and that claim is worth a record. Setting
  a flag is never commented on — asking for a decision needs no permission.

### Fixed

- **The Needs Operator button on a Request's Discord card now clears a stale flag.**
  It used to refuse to do anything at all once the Request left Backlog or Eval, which
  meant a finished, closed Request kept its flag with no way — button or verb — to
  remove it. The button still refuses to *add* the flag outside those States.

  If you keep a work queue by filtering on Needs Operator, this is the change you will
  notice: finished Requests can now leave that list.

## [0.3.2] - 2026-07-26

Fixes a quiet one: writing a request by tag could drop the code out of what you wrote.

### Fixed

- **Code you put inside a tag now survives.** When you drive alchemy by writing a
  `#< … #>` tag, anything you wrapped in backticks or a fenced block inside that tag was
  being deleted before the request was filed. So a comment written like this:

  ```
  #< comment #22 the fix is `taskkill /T` #>
  ```

  was posted as *"the fix is"* — the command gone, the sentence still reading as if it
  were complete. Nothing failed and nothing warned you; the request simply arrived
  missing the part you most likely cared about.
  Tag bodies are now filed exactly as you wrote them, backticks and code blocks included.

  This only ever affected tags. Requests written with `spt shell send` were never touched.

  The rule about *quoting* a tag is unchanged and still works the way it always has: if
  you wrap the tag's **opening** `#<` in backticks or put it inside a code block, it is a
  quotation and never fires — which is what lets you show someone an example safely. What
  changed is that quoting something **inside** a live tag no longer removes it.

## [0.3.1] - 2026-07-25

Fixes the reason the background service kept disappearing, and withdraws the workaround
that 0.3.0 shipped for it a few hours earlier.

### Fixed

- **Closing a shell no longer takes the background service down with it.** The service is
  what delivers your notifications when a request changes state — for everyone on the
  machine, not just whoever started it. Closing an unrelated shell could take it down
  silently, and nothing brought it back until someone happened to start a new shell. It is
  now started in a way that leaves it belonging to no one, so it survives.

### Changed

- **`daemon autostart` is gone.** 0.3.0 added it — a scheduled task that restarted the
  service every five minutes. It was the wrong fix for the problem above and is withdrawn.
  Nothing replaces it: the service still starts by itself the first time any shell connects,
  and now it stays up. If you installed the task from 0.3.0, remove it:
  `schtasks /Delete /TN "Alchemy Hub Daemon" /F`.

## [0.3.0] - 2026-07-25

Two things that used to require editing a config file by hand and hoping: adding a
project to the board, and keeping the background service alive.

### Added

- **Add a project to the board with a command.** `daemon project "add owner/repo"`
  registers a repo with the hub — it gets its own Discord channel, its status threads,
  and its notifications start being delivered. Until now the only way was to edit the
  hub's config file by hand and restart it, and the failure when you forgot was silent
  and misleading: your shell bound to the repo, every command worked, and the repo just
  never appeared in Discord.
- **The new project appears without restarting anything.** The running hub notices the
  change on its next cycle and sets the channel up in place. Nothing else on the board
  is interrupted — previously the restart briefly disconnected every other project too.
- **See and undo the list.** `daemon project "list"` shows the registered repos.
  `daemon project "remove owner/repo"` un-registers one: the hub stops polling it and
  stops delivering its notifications. **Its Discord channel and everything in it are
  left alone** — removing a project never deletes a channel or its history. You cannot
  remove the last one; a hub with no projects would not start.
- **Keep the background service alive on its own.** `daemon autostart "install"` hands
  the service's startup to Windows Task Scheduler instead of to whichever shell happened
  to start it first. Two things change: closing a shell can no longer take the service
  down with it, and if the service dies for any reason it comes back within five minutes
  without anyone noticing it was gone. `daemon autostart "status"` tells you which of the
  two situations you are in, and `"remove"` undoes it.

### Why the autostart matters

The service is what delivers notifications when a request changes state — for everyone
on the machine, not just the person who started it. It could previously disappear when
an unrelated shell was closed, and nothing brought it back until someone happened to
start a new shell. There was no error and no warning; notifications simply stopped
arriving. If you run the hub, install the autostart task once.

## [0.2.1] - 2026-07-25

A quieter shell. Nothing you type changes; what changes is when the shell reads your
transcript looking for written requests.

### Changed

- **The shell stops reading your transcript while you are not working.** Written
  requests can only ever appear in your own messages, so a shell watching an idle
  person is asking a question whose answer cannot have changed. It now reads the
  moment you stop working — which is exactly when your last message becomes
  readable — then waits until you are active again. A request written as the last
  thing you say still runs without being prodded, same as before.
- **A shell that loses track of whether you are working keeps reading anyway.** The
  signal it uses is best-effort, so if one goes missing the shell falls back to
  reading on a slow timer and picks straight back up the instant you send it
  anything. Losing that signal can make a written request answer more slowly; it
  cannot make one go unanswered.

### Added

- **A new shell knows whose transcript to read straight away.** Previously it learned
  that from the first command you sent it, so a freshly started shell had to be
  prodded once before written requests worked. It no longer does — the first thing
  you write is picked up.
- **A way to see what the shell is doing.** Create a file called `scan-log.on` in a
  shell instance's folder and it records each time it reads your transcript, and each
  time it skips because you are away, to `scan.log` beside it. Delete the file to
  switch it off. Useful when a written request did not run and you want to know
  whether the shell ever looked.

## [0.2.0] - 2026-07-25

First published release. Alchemy is a Requests shell: it turns requests captured in
chat into tracked GitHub issues, keeps their state in sync, and tells people when
something they care about moves.

### Added

- **Install and update straight from a release.** `spt adapter add --release
  BigscreenVR/spt-progress-tool` installs alchemy, and `spt adapter update alchemy`
  moves a node to the newest published version. Both ride the GitHub CLI you have
  already signed in to, so a private repo needs no extra credential.
- **Drive a shell with the Request verbs**: `view`, `list`, `state`, `tag`,
  `dispatch`, `milestone`, `link`, `watch`, `unwatch`, `sweep`, `release`,
  `bind-repo`, `daemon`, and `help`. Free-form `create` and `comment` take a whole
  message body instead of positional arguments.
- **Request tags — drive the shell from your own words.** Write
  `#<` … `#>` in a message and the shell reads it out of your own transcript and
  acts on it, so an agent with no shell tooling can still file and update requests.
  A tag inside backticks or a code block is a quotation and never runs.
- **A confirmation step for anything destructive.** Cutting or shelving a request,
  removing a milestone member, or cancelling a notification never happens straight
  from a tag: the shell hands back a token and waits for you to repeat it, and the
  pending action expires after 24 hours instead of firing later by surprise.
- **Notifications that find you.** `watch` follows a request and messages you when
  its state changes; `watch --for <someone>` registers it on their behalf, so
  whoever asked for the work hears about it without having to ask again.
- **Address a specific instance.** If you run more than one shell, prefix a written
  request with `@<instance-id>` so exactly one of them acts on it.

### Fixed

- **A request could be filed twice from a single message.** Under some transcript
  timing the shell could re-read a message it had already acted on and file a second
  copy of the same request, while only telling you about one of them. It now tracks
  what it has handled per message rather than per line, so a repeat read does
  nothing.
- **Two people on one machine no longer overwrite each other's setup.** Instances
  are named per owner, so two people could each have an `alchemy-0`; their saved
  settings collided and a restarted shell could come back pointing at somebody
  else's project. Each instance now keeps its own settings.

### Changed

- Requests filed through a tag credit the person who asked in the body, because the
  footer names the shell's owner. If you are filing on someone else's behalf, say so
  on a `Submitted by:` line and the shell keeps it.
