Dev blog 2026-10-22 · Keel 0.5.3 development

Reports, client and Discord

A report filed in game becomes a thread, then a credit.

We built one loop for feedback. A tester files a report from inside the game, the server keeps it, a Discord bot turns it into a public thread, we fix it, and the patch notes name the tester. This post walks the loop, tours the code behind it, and says where it is thin.

Live today is Keel 0.5.2.1. It has the report form, the server's record, the Bug Hunters board, the Discord bot and Link Discord. Threads for in-game reports, the kind check, our /kind and /backfill commands, half points for useful context and the public notice on the form are coming in 0.5.3, which is built on main and not released.

The game's top bar while docked at Haven on Layer 1. At the top right of the top plate sit buttons for Help, Journal, Menu and Report, with a Dev tools button under them. Six berthed Carriers are listed on the left.
The Report button sits in the top bar beside Help, Journal and Menu. A store capture of a 0.5.2 build, committed on 2026-10-21 before the 0.5.3 form text, taken on a dev account, so a Dev tools button shows. It shows the button, not the form: no capture of the form exists yet.
The problem

Feedback went into a list and stayed there.

An in-game report landed in a private list that only we could read. A bug posted in Discord reached that list only when we copied it over by hand.

When a tester pressed Send they got one line in the log. Nobody else could see the report, so nobody else could add a me too. A duplicate earned no points on the board, and a merge waited for our click.

We cannot count what this lost, because feedback that never arrives leaves no row. We can say what our vision document asks for: every in-game report is read, and the reporter of every fix is credited. The server already scored that credit for in-game reports. The Discord half rested on us copying notes across by hand.

IN GAME, BEFORE 0.5.3Report formclientPrivate listserver fileCredit lineif it was fixedDISCORD, BEFORE 0.5.2Chat posta playerHand copyus, by handPrivate listserver file
Schematic drawn for this post, not a capture. The two roads a report took before 0.5.2 for Discord and before 0.5.3 for the game.
What we wanted instead

Three rules for the loop.

We settled these as answers to our own design questions on 2026-10-21 and 2026-10-22. The code follows them, and the decisions log keeps the answers.

Say it before you send it

From 0.5.3 the form tells you the text and your pilot name will be public. Your location, ships and log stay private. There is no private option during the alpha, and that includes exploits.

The server keeps the record

The bot reaches the record only through a short list of intake ops. The game server stays the only writer of the file and the only source of report ids.

We keep the last word

A model writes the title, checks for abuse and reads the kind. It can hold a report back or choose a forum. We can overrule it with /title and /kind.

What we built

One report, one id, from the form to the credit.

The server gives every report an id and keeps the pilot's text, the kind and a little game context. The bot gives it a thread in Discord. A release closes it with the version that fixed it, and the login page and the patch notes carry the credit.

800characters of a report that the server and the bot keep
20reports one pilot may file in a day, one every 30 seconds
30 sbetween the bot's looks for new in-game reports, coming in 0.5.3
7 daysafter a report closes before its thread locks, coming in 0.5.3

The Bug Hunters board on the login page scores a bug fix 1 point and an idea 2. A duplicate that adds useful context earns half, which is coming in 0.5.3.

1 The formclient: draws it, sends one op2 The recordserver: the id, the limits, the context3 The threadbot: check, title, post, tag0.5.34 The fixus: code, tests, a resolve line5 The resolverelease: stamps the fixing version6 The creditboard, patch notes, thread tag
Schematic drawn for this post, not a capture. Blue boxes are the server, purple is the bot and white is us. The step marked 0.5.3 is coming in that build.
Kept on the serverThe id, the time, the pilot's name, the kind and the text. The state, our note and the version that fixed it. A game context: the layer, whether you were docked, your tutorial step, your Marks, up to six ships and your last ten log lines.
Sent to the botFor a new report: the id, the kind, the text, the pilot's name and the time. Never the game context. For a closed one: the state, the version, our note, the credit flag, the merge target and the count of me too votes.
Schematic drawn for this post, not a capture. The lists follow _add_report, _report_context, _intake_reports_since and _intake_reports_state.
As a tester lives it

From a button in the top bar to a line in the patch notes.

The tester sees a form, one line in the log, then a thread and a credit. The server owns the id, the limits and the record. The bot owns the thread. The client only draws and sends.

  1. Press ReportThe Report button sits in the top bar. Pick Bug or Suggestion and write up to 800 characters. The box keeps your words as a draft until they are sent.
  2. Read the noticeThe form says the report is posted publicly in Keel's Discord with your pilot name. Your location, ships and log stay private. Coming in 0.5.3.
  3. SendThe server answers with one line in your log. You can send one report every 30 seconds and 20 a day. Offline, the form keeps your text.
  4. Find the threadOn its next 30 second pass the bot checks the text, writes a title and posts a thread with a Me too button and the Open tag. Coming in 0.5.3.
  5. See it closeWhen a fix ships, the thread is tagged Fixed and says which version fixed it. The login board scores you and the patch notes can name you. The thread locks 7 days after it closes.
FormWhat happened, or what would make it better?
FormYour report is posted publicly in Keel's Discord ... with your pilot name and this text. Your location, ships and log stay private. Leave out anything you would not post in public.
LogBug report N saved. Thank you.
LogSuggestion N saved. Thank you.
LogWait S seconds before the next report.
LogThat is 20 reports today. Thank you; the rest can wait for tomorrow.
FormNot connected. Your report is kept here; send it once you are back online.
ThreadPILOT sent a bug from the game: the report text, quoted.
ThreadFiled as report N. Same problem? Press Me too.
ThreadThis report was filed as a suggestion and posted here because the text reads as a bug.
ThreadFixed in VERSION. Thanks for the report.
ThreadClosed: NOTE
LoginEvery report that ships scores: an idea 2 points, a bug fix 1. A duplicate that adds useful context scores half.
NotesReported by NAME, NAME. Thank you!

The wording is copied from the client, the server and the bot's own message templates. The dots in the form's notice stand for a short phrase that names the forum. N, S, PILOT, VERSION, NOTE and NAME are filled in by the game or the bot. The thread lines for in-game reports are coming in 0.5.3, and so is the notice and the half credit sentence on the login board.

Filing from Discord works too. Settings has Link Discord, which gives a code of 6 characters that lasts 10 minutes. Typing it as /link in Keel's Discord makes the reports you file there count for your pilot. That shipped in 0.5.2.

Under the hood

A form, a record, a poll and a resolve.

The loop spans the game server, the Discord bot and three owner tools, and no piece of it is large. The bot's reports.py is 1,062 lines and its thread code in gamethreads.py is 409. The server owns the record and the ids, the bot owns the thread, and we own the fix and the last word.

The game serverworld.py handles the report op, access.py paces it, dev_log.py stores and scores it, and discord_ops.py and intake.py answer the bot.
The Discord botgamethreads.py makes the thread, backfill.py posts the history and reports.py closes and merges. It is a separate program under its own Windows account.
Our toolsrelease.py applies the resolves, keel_watch.py posts new report ids to a private webhook, and keel.py lists open reports. The watcher and keel reports never show report text.

One report, as a call chain

Steps 3 and 4 are coming in 0.5.3, and so is the thread tag in step 6. The rest is in the live build.

  1. SendThe form sends one op with the kind, the text and the client version._build_report, _send
  2. RecordThe server cleans and paces the text, saves the context, takes the next id and writes the file._report, _report_allowed, _report_context, _add_report
  3. LearnEvery 30 seconds the bot asks for reports after its cursor and holds each new one.tick, poll, _intake_reports_since, hold
  4. PostThe bot checks the report is still open, asks a model for a title, an abuse flag and a kind, and posts the thread.post, settle_kind, create_thread
  5. ResolveWe fix the code and write a resolve line. The release applies it with the server stopped and stamps the version.parse_resolve, apply_resolves
  6. CloseWithin 5 minutes the bot reads the new state, tags the thread and names the version. The board and the notes pay the credit.refresh, close, fixed_text, credits

The main functions

_build_reportmain.gd

Builds the form: a Bug or Suggestion pick, a text box, the public notice and Send. The box saves a draft to disk on each change and clears it when a report goes out.

_reportworld.py

Cleans the text to 800 characters, refuses a pilot who files too fast, copies the context and answers with the line that lands in the log.

_add_reportdiscord_ops.py

The one place a report id is made: the highest id plus one, under the world lock. Game and Discord filings share it, and a test files twenty reports from two threads to prove no id repeats.

_intake_reports_sincediscord_ops.py

Hands the bot up to 20 open in-game reports after an id, oldest first. A null start returns only where the log ends, so the bot begins from now.

postgamethreads.py

Checks the report is still open, then makes one model call for a title of at most 8 words, an abuse flag and a kind. It withholds abuse and posts the rest. Three failed answers withhold a report for us to read.

set_kindgamethreads.py

Our /kind command. It corrects the server's record, rebuilds the thread in the other forum, points the old thread at the new one and locks it.

apply_resolvesrelease.py

Marks each report named in the resolve file resolved, squeezes our note to one line of at most 200 characters and writes the release version as resolved_in. A decline writes no version.

closereports.py

Reads a report's new state and sets the tag. Fixed says Fixed in the version, and Declined says Closed with our note. The threads of merged duplicates hear it too.

creditsdev_log.py

Scores the board: 1 for a bug fix, 2 for an idea, half for a duplicate that added context, and nothing for a report resolved without credit.

check_reportskeel_watch.py

Posts the id, kind and author of each new report to a private webhook, at most 8 lines a run, and never the text.

Choices we are happy with

The context stays homeThe intake sends five fields for a new report. A test asserts that exact set, so a new field cannot leave the server through that op without a failing test.
A restart posts nothing twiceThe bot keeps a cursor and a row per report. Tests lose its cursor, roll the game's save back and drop its rows after retention. None makes a second thread, and a rollback still lets new reports through.
The model is told to doubtThe kind prompt says to favour precision and to answer unsure when in doubt. The kind the pilot picked is not in the prompt, and the server's record stays as filed.
Our commands win/title posts a held report without the check, /kind moves a thread and fixes the record, and a merge the bot makes on its own comes with an Undo button.
The client side

The server owns the record. The client draws a form.

  • client/main.gd builds the form and sends one report op with the kind, the text and the client version. It decides no limit, no id and no thread.
  • The draft sits in a file in the game's user folder and changes with every keystroke, so a half-typed report survives a restart.
  • client/features/landing.gd draws the Bug Hunters board and the Reported by lines from what the server sends. It does not score anything.
  • The client self-test checks that the form exists and that its notice says publicly.
What the code looks like in numbers
21ops in the bot's intake, 13 of them about reports
506lines in discord_ops.py, which also holds linking and dev events
409lines in gamethreads.py
470lines in backfill.py
What the tests prove

What is pinned, and what is not.

Server tests live in server/test_alpha.py, which has 553 test methods by our count, and they run on every commit with the client self-test. The bot has 367 test functions in bot/tests, 54 for threads and kinds and 47 for the backfill. They run offline against a fake Discord and a fake intake, by hand when bot code changes. We did not run them for this post.

Pinned

  • test_reports_since_hands_the_bot_in_game_reports_without_their_context: five fields, 20 to a page, a cursor, and a start from now.
  • test_dev_names_cannot_be_signed_up_and_reports_are_paced: a second report in a row gets the Wait line.
  • test_game_and_intake_reports_never_share_an_id: twenty filings from two threads, twenty different ids.
  • test_a_new_in_game_report_becomes_a_thread_without_its_game_context: the forum, the title, the quote, the Me too button, the Open tag, and nothing from the context.
  • test_a_suggestion_that_is_plainly_a_bug_lands_in_bugs_with_the_filed_as_line and test_an_unsure_answer_keeps_the_filed_kind_and_adds_no_line: the two ends of the kind check.
  • test_report_kind_corrects_a_reports_kind_and_the_board_follows: after /kind an idea counts double.
  • test_a_rerun_makes_nothing_new_even_after_the_retention_drops_the_rows: the backfill cannot post twice.
  • test_no_webhook_logs_only_and_never_posts_report_text: the watcher posts ids, not text.

Not pinned, or thin

  • The title, abuse and kind call has no model test set. The sets cover answers, duplicates, sorting and injection. The bot's tests script the model, so they prove our handling and not its judgement.
  • Nothing counts how often the kind check moves a report, or whether it was right.
  • No test drives a real Discord. Tag and lock refusals and post backoff are tried against a fake one, and the first real run of /backfill is still ahead, after the 0.5.3 release.
  • We have not timed the gap from Send to thread. The bot looks every 30 seconds, then waits on the model, and Discord can refuse a post.
  • No capture of the report form exists, and the client test only checks that the form exists and says it is public.
  • The Reports help page still describes the older flow and does not say the text is public. The form does.
What we learned and what is next

The rules were easy. The edges were the work.

One privacy rule covered two pathsOn 2026-10-21 we ruled that only new in-game reports get public threads, and a review built the backfill to skip everything filed before the public notice. On 2026-10-22 we ruled that the backfill posts every report ever filed, with the pilot's name where the credit flag is on. We removed the cutoff. Next time we write the rule per path.
Mislabelled reports are realWe asked what happens when a bug is filed as an idea, or the reverse. Threads were routed by the kind the pilot picked and nothing reclassified. We added a text check that moves a thread only when it is confident, says on the first post what it was filed as, and leaves the server's record alone until we run /kind.
A bot post skips AutoModDiscord screens what players post, not what a bot posts. So the bot checks the text itself before it posts, and it holds abuse for us to read. The same call treats the report as data inside tags, never as instructions.

Where we would push next

  • Coming in 0.5.3: threads for new in-game reports, the kind check and /kind, merges the bot makes on its own with an Undo, half points for a duplicate that added context, and the public notice on the form.
  • Coming in 0.5.3: a one-time /backfill that posts every report ever filed, with its state and the version that fixed it.
  • A capture of the report form for this blog. It needs a new keel shots flag, because the list has none.
  • A model test set for the title and kind call, like the ones for answers and duplicates. It is not in a build yet.
  • Help page wording that says the text is public. It is not in a build yet.
Join the open alpha

Tell us what broke, and what you wished for.

Keel is free to play on PC, and the open alpha is where the report loop lives. Find us in the Keel Discord, tell us what the report loop did to your evening, and file a report from inside the game when something looks wrong.

Players whose reports become fixes are credited in game. Try it first with something small: press Report, pick Suggestion, and tell us what the game should have said.