Skip to content

feat(worker): publish agent work through the GitHub bot - #145

Merged
kixelated merged 8 commits into
mainfrom
quest/m0/kix/publication
Oct 19, 2026
Merged

kixelated merged 8 commits into
mainfrom
quest/m0/kix/publication

Conversation

@kixelated

@kixelated kixelated commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Implements Bot publication and deletes the quest.

Problem

Jobs could run, but their output had no path to GitHub. publishQuest and createPullRequest were unused primitives limited to a single quest file. mergePullRequest always used a merge commit, which squash-only and rebase-only repositories refuse.

Behavior

  • POST /api/runner/publish (runner bearer token, HTTPS rather than the hub socket so slow GitHub writes don't stall other messages). The request carries job, attempt, head (the commit the changes build on, or null), message, base64 changes (at most 500 files and 16 MiB in total, each optionally executable or a symlink; submodules are unsupported), an optional pull: { title, body, draft }, and session. The reply is { head, pull? }, or 202 { decision, cancel }.
  • Validation before any write:
    • The attempt fence: the job's current (attempt, runner), still running.
    • Plain work-tree paths only.
    • Per-workflow path limits: plan → quest documents under quest/ (Markdown other than AGENTS.md/CLAUDE.md), bootstrap → flake.nix/flake.lock, start/iterate → their changes. No workflow may touch .github/workflows/.
    • The requester's maintainer authority, checked live.
    • The branch's expected head.
  • Refusals become decisions, not silent drops. Refused paths, a contributor-fork PR head, a branch changed outside the job, and a branch left over from earlier work each pause the job with a new publication question kind. Answering resumes the agent's session.
  • Branches:
    • plan, start and bootstrap write the job's branch on GITHUB_FORK_OWNER/<repo>, which is verified by its parent ID.
    • iterate writes the PR's actual head, either an upstream branch or a service-fork branch.
    • No job writes a default branch.
    • Commits go through the Git Data API with no author set, so GitHub attributes them to the App.
  • Reconciliation:
    • The service creates the commit, then records it as pending under the fence just before a fast-forward-only ref update. If the outcome is lost, the next publication adopts it.
    • Every publication, including a delivery alone, first checks the branch is where the job left it.
    • Delivery looks for a PR this job opened on the branch (open or closed, created since the job began) before creating one, and recovers from a 422, so a retry never opens a duplicate.
    • A closed PR is never replaced.
  • Draft rules:
    • Planning PRs are always drafts, and so are PRs of jobs that were asked to cancel.
    • Otherwise draft defaults to true, and draft: false marks the PR ready. Nothing ever converts a PR back to draft.
    • Delivery needs an enabled project. A paused project can still preserve work on its branch. Delivery rereads the attempt after the branch write, so an ended attempt opens no PR and a cancelled one stays a draft.
  • Merge method: squash, then merge commit, then rebase, chosen from what the repository allows.
  • Schema: migration 0009_publication.sql adds jobs.head, pending, pendingAt and pull, and rebuilds job_questions for the new kind. Iterate jobs start with head set to the PR head.
  • Docs:
    • cloudflare/GITHUB.md gains a Publication section covering the contract, token scopes and broader installation grants, and the untrusted fork-PR CI warning.
    • The README and the runner/operator-setup quests point to it.

Validation

  • just check and just test pass in nix develop after merging main: 231 Worker tests and 225 CLI tests.
  • New test/publish.test.ts runs against a stateful fake of GitHub's Git, ref and pull request APIs. It covers:
    • fenced-out and stale writes
    • authority loss
    • path policy and decisions
    • lost ref updates and lost deliveries
    • closed PRs
    • paused projects
    • iterate heads, including contributor forks
    • a wrong fork parent
    • token scopes and draft rules

Limitations: not verified against real GitHub

These need real App installations. They are listed under operator setup and were not exercised here:

  • an upstream installation token opening a PR whose head is on the fork's separate installation (the cross-installation fork/PR test)
  • a fork commit whose parent is an upstream commit pushed after the fork was made
  • GitHub's signature on App-authored API commits
  • pushing an upstream PR head under branch rules

Decisions (settled by the maintainer, 2026-10-19)

  1. Fork naming. Forks stay named GITHUB_FORK_OWNER/<upstream repo name> for the m0 pilots. A per-project fork repository ID, recorded at enrollment, comes before opening to other owners.
  2. A leftover branch on the fork (for example from a stopped earlier job for the same quest) pauses the job with a decision to delete it.
  3. Attribution. The service adds no Co-authored-by lines; the agent writes them per the repository's instructions.
  4. Interview paper trail. It comes from the agent's quest files and PR body for now. The service adds the recorded answers once the decision inbox's prompt format lands.
  5. Paused projects. Branch commits are allowed so partial work is kept, but opening a PR is refused.
  6. Contributor-fork iteration is always reported as blocked until a live test shows an App token can push there.

Merge coordination

Suggested follow-ups

  • A per-project fork repository ID at enrollment, before opening to other owners (decision 1).
  • Service-rendered decision paper trail in planning PR bodies, once the decision inbox's prompt format lands (decision 4).
  • A published job event, so agent activity can show commits and the PR.
  • An execution action to continue an iteration from a PR head that moved.
  • A live test of App-token pushes to contributor forks (decision 6).

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 18, 2026 22:09
Runners publish a running attempt's file changes at POST /api/runner/publish.
The Worker validates the attempt fence, each workflow's permitted paths, the
requester's live maintainer authority, and the branch's expected head, then
commits the changes as the App with the Git Data API on the job's one branch:
the service fork for plan/start/bootstrap, or an iterated PR's own head.
Refused changes and blocked updates pause the job with a publication decision.

A pending commit is recorded under the fence just before a fast-forward-only
ref update, so a lost outcome is adopted on the next publication instead of
replayed. Delivery finds the branch's open PR before creating one, never
replaces a closed one, keeps planning PRs draft, and marks finished work
ready. Merges pick squash, then merge commit, then rebase from what the
repository allows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Outcome: the publication path is implemented and passes just check and just test locally. GitHub is mocked throughout. This PR stays a draft because six product decisions in the description are still open. The real cross-installation fork/PR behavior also hasn't been verified, because that needs live App installations; it is tracked under the operator-setup quest.

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 19, 2026 15:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T17:29:34.088084Z b5a58cf New commits
🔒 Security Review ✅ Completed 2026-10-19T17:31:59.397393Z b5a58cf New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 076790fea9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/jobs/publish.ts Outdated
Comment on lines +175 to +176
else if (input.head !== head)
throw new JobError(`The job's branch is at ${head ?? "no commit yet"}; continue from it`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate the branch before pull-only delivery

When a runner publishes commits first and later sends a pull-only request, this branch compares input.head only with the cached jobs.head and skips branch.head(). If the service-fork branch was pushed or deleted in between, deliver() can open the PR from that unexpected live head while returning the stale cached SHA, bypassing the branch-change decision used for publications containing changes. Resolve the branch and verify its live head before opening or updating the pull request.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Fixed in 39efeb0: a pull-only publication now runs the same branch verification as a commit (verify in publish.ts, inside contribute), so a branch moved or deleted outside the job pauses on the publication decision and opens no PR. Regression test: "pauses a delivery alone when the branch moved outside the job". A window between that check and the PR create remains, since GitHub cannot pin a PR to a head SHA at creation.

(Written by Claude Opus 5.5)

Comment thread cloudflare/src/github/service.ts Outdated
Comment on lines +852 to +854
const open = async () =>
(await this.#api<PullHead[]>(token, `${pulls}?state=open&head=${encodeURIComponent(label)}`))[0] ??
null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve closed PRs during interrupted-delivery recovery

If GitHub creates the PR but the response or subsequent store.delivered() write is lost, job.pull remains null. Should the maintainer close that PR before the retry, this recovery lookup excludes it with state=open, so the retry proceeds to create a replacement instead of honoring the stated closed-PR behavior. Reconciliation needs to find the prior closed PR for the same job branch as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Fixed in 39efeb0: when the job has no recorded PR, recovery lists state=all PRs for the branch and takes one created since the job began, so a PR closed after an unrecorded delivery is refused rather than replaced. The creation-time bound keeps a closed PR from an earlier job on the same branch name from blocking a new job. Regression tests cover both cases.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 19, 2026 08:33
…und on recovery

A pull-only publication now checks the job's branch through the same
verification as a commit, so a branch changed outside the job pauses on a
decision instead of opening a PR from it. Recovering an unrecorded delivery
also finds pull requests the maintainer closed since, so none is replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	cloudflare/README.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39efeb0ad2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/jobs/publish.ts Outdated
return "Changing GitHub Actions workflows needs the workflows permission, which the App does not request";
if (workflow === "bootstrap" && path !== "flake.nix" && path !== "flake.lock")
return "A devShell bootstrap publishes only flake.nix and flake.lock";
if (workflow === "plan" && !path.startsWith("quest/")) return "Planning publishes only quest files";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict planning publications to actual quest documents

For a planning request containing paths such as quest/tool.sh or quest/AGENTS.md, this prefix check allows the files to be committed even though isQuest() defines quest documents as Markdown excluding agent-instruction files. This bypasses the stated plan-only policy and lets a planning job publish unrelated code or modify instructions; validate the path with the shared quest predicate in addition to requiring the quest/ directory.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Fixed in 519cbac: planning now requires quest/ plus the shared isQuest predicate, so quest/tool.sh and quest/AGENTS.md are refused with a decision. GITHUB.md says so too.

(Written by Claude Opus 5.5)

const label = `${fork.owner.login}:${job.branch}`;
// An unrecorded earlier delivery may have opened one, which the maintainer may since have closed.
// GitHub reports whole seconds, so compare against the second the job was created in.
const since = Math.floor(job.createdAt / 1000) * 1000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish earlier PRs created in the same second

When an earlier job's PR was closed and a replacement job for the same deterministic branch is created within the same GitHub timestamp second, flooring createdAt makes that older PR pass the recovery cutoff. opened() then treats it as the new job's PR and rejects delivery because it is closed, rather than opening the intended replacement. The reconciliation key needs to distinguish jobs without relying solely on second-resolution creation time.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree; leaving this as is. For an earlier PR to fall in the same second as this job's creation, all of these would have to happen inside one second: the earlier job delivers its PR, the PR is closed, the fork branch is deleted (otherwise the new job pauses on the leftover-branch decision before it can deliver), and the replacement job is created. Rounding the other way would instead miss this job's own PR if it were created in the second the job was, which is equally impossible because a job must be dispatched and commit first. A stronger key would mean recording a delivery marker before the create, which adds state for a case that cannot happen in practice.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 122594b296

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/jobs/publish.ts Outdated
Comment on lines +190 to +191
const draft = input.pull.draft || job.workflow === "plan" || job.cancel === 1;
const pull = await github.deliver(upstream, job.actor, job, { ...input.pull, draft });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate the job before delivering its pull request

If the runner finishes or loses the attempt, or a maintainer cancels the job while contribute() is awaiting GitHub, this cached job remains valid-looking and deliver() still opens or updates the PR without another store fence. In the cancellation case, a request with draft: false can also mark the PR ready using the stale cancel === 0, and later deliveries intentionally cannot return it to draft. Re-read the current attempt and cancellation state immediately before delivery.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Fixed in 519cbac: publish rereads the attempt under the fence after the branch write and delivers from that row, so an attempt that ended meanwhile opens no PR and a cancellation keeps it a draft. Regression test: "rechecks the attempt and its cancellation after writing the branch, before delivering". A cancel landing during deliver itself can still slip through, but that window is now one GitHub round trip rather than the whole branch write.

(Written by Claude Opus 5.5)

…ivery

Planning publications now accept only quest documents under quest/, using
the shared isQuest predicate, so scripts and agent instructions there are
refused. Delivery rereads the attempt after the branch write, so an attempt
that ended meanwhile opens no PR and a cancellation keeps it a draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 519cbac561

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

-- The commit the job's branch holds, as far as kix has published or observed
-- it. NULL until the first publication creates the branch; an iteration starts
-- at the pull request's head.
ALTER TABLE jobs ADD COLUMN head TEXT;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Backfill heads for existing iteration jobs

When this migration runs with an existing queued, running, or waiting iterate job, the new head remains null even though that job's branch already starts at its recorded revision. On its first publication, verify() sees the live PR branch at revision but the stored null head and repeatedly pauses it as an externally created branch. Backfill head = revision for existing iteration jobs as part of the migration.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree; leaving the migration without a backfill. Nothing that uses jobs is deployed yet (the merged 0008_output.sql drops job_output for the same reason), so no queued or running iteration job can exist when this runs. New iteration jobs start with head set to the PR head at creation.

(Written by Claude Opus 5.5)

Comment thread cloudflare/src/github/service.ts Outdated
const base = await this.#api<{ tree: { sha: string } }>(token, `${path}/git/commits/${sha(parent)}`);
const tree = [];
for (const change of changes) {
const mode = change.executable ? "100755" : "100644";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve or reject non-regular Git file modes

When a job changes a symlink or submodule path, the publication format provides only an executable flag and this code always writes a regular blob (100644 or 100755). Publishing a symlink therefore silently replaces it with a regular file containing its target text, while a submodule cannot be represented correctly at all. Carry the Git object type and mode through the request, or explicitly refuse unsupported entries rather than corrupting them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that silently writing a regular file was wrong. Fixed in 22103b2: a change may set symlink: true, whose content is the link target, and is written with mode 120000; executable and symlink together are refused. Submodule pointers have no representation in a content-based change, so GITHUB.md documents them as unsupported. The runner adapter must set the flag; the service does not read the base tree to infer modes.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 19, 2026 09:35
A change may set symlink, whose content is the link target, so publishing a
symlink no longer replaces it with a regular file. Submodule changes have no
representation and are documented as unsupported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary

The maintainer chose to merge. Auto-merge (squash) is enabled at b5a58cf.

What this PR does: jobs can now publish through the GitHub bot via POST /api/runner/publish. Each publication is checked against the attempt fence, the workflow's path policy, live maintainer authority, and the branch's expected head. Refused publications pause the job with a publication decision. Commits fast-forward under a recorded pending state, delivery never opens a duplicate PR or replaces a closed one, and mergePullRequest picks squash, then merge commit, then rebase. Migration 0009_publication.sql adds the schema. The six settled decisions are in the PR description.

Reviews: Codex reviewed three times, which used up the three fix rounds.

  • Five findings fixed: verify the branch before a delivery alone, and keep closed PRs during recovery (39efeb0); limit planning to quest documents, and refence the attempt before delivery (519cbac); keep symlink modes (22103b2).
  • Two declined, with replies on their threads: the same-second PR cutoff, and a head backfill in the migration (nothing that uses jobs is deployed yet).
  • Codex has not reviewed 22103b2. The maintainer accepted the earlier reviews as the required review.

Since the last push: merged main (#147, #148, #149) in b5a58cf with no conflicts. These commits only add skills and quest plans; none of them refers to the deleted publication.md. quest check, just check and just test (231 Worker tests) pass locally, and CI passes on b5a58cf.

Coordination: #144 still has 0008_inbox.sql, which collides with main's 0008_output.sql. Once this PR lands, #144 needs 0010.

Follow-ups: listed in the description and not yet planned. They are: a per-project fork repository ID, a service-rendered decision paper trail, a published job event, continuing an iteration from a moved PR head, and a live test of App-token pushes to contributor forks.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit 995b00e into main Oct 19, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5a58cf1e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +44 to +45
if (workflow === "plan" && !(path.startsWith("quest/") && isQuest(path)))
return "Planning publishes only quest files";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject symlinks from planning publications

A planning request for quest/foo.md with symlink: true passes this path-only check, after which parsePublication emits mode 120000; the job therefore publishes a symlink rather than a quest document. If that PR is merged, GitHubService.#snapshot rejects the matching quest entry because it is not a regular file, breaking subsequent project refreshes. Include the change mode in the planning policy and refuse symlinks for plan jobs.

Useful? React with 👍 / 👎.

kixelated added a commit that referenced this pull request Oct 19, 2026
Brings in bot publication (#145). The inbox migration becomes
0010_inbox.sql after 0009_publication.sql. merge.md and planning.md
drop the deleted publication and inbox quests from Required, and
merge.md's emptied section goes away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 19, 2026
…inbox

Codex review findings on #144:

- A publication decision from bot publication (#145) fell through to
  "could not be read" with no way to answer. The inbox now lists the
  refused paths or why the branch is blocked and takes a text reply,
  which the answer route accepts from the form.
- A failed PR job whose pull request is no longer observed crashed the
  inbox: the retry action dereferenced missing activity before the
  refresh reason was checked. The action is built only when unblocked.
- The inbox read only the 50 newest jobs, so an older waiting job
  vanished. It now also reads every queued, running, and waiting job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Oct 19, 2026
* chore(quest): claim quest/m0/kix/prompts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(worker): make the decision inbox the signed-in home

Add /inbox: one active decision thread across a maintainer's enrolled projects, coalesced per job target, with agent questions, account-limit choices, failed jobs, new pull requests, new issues, and ready quests. Define the agent prompt contract (choices, recommendation, free text, permission vs decision) and validate prompts and answers. Job routes accept inbox form posts, bind decisions to the revision on screen, and refuse stale or repeated answers. Per-maintainer read state marks changed items unread and keeps skips until an item changes. Sign-in now lands on the inbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(worker): keep the decision thread in one column on wide screens

The PR meta line reused .entry-meta, whose grid-column: 2 split the thread
into two columns once there was room. Also show GitHub's check and review
enums as plain words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(quest): drop the deleted inbox quest from paper-trail

#150 added paper-trail.md requiring /quest/m0/kix/prompts.md, which
this PR deletes. That was its only blocker, so the Required section
goes away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(worker): answer publication decisions and keep older jobs in the inbox

Codex review findings on #144:

- A publication decision from bot publication (#145) fell through to
  "could not be read" with no way to answer. The inbox now lists the
  refused paths or why the branch is blocked and takes a text reply,
  which the answer route accepts from the form.
- A failed PR job whose pull request is no longer observed crashed the
  inbox: the retry action dereferenced missing activity before the
  refresh reason was checked. The action is built only when unblocked.
- The inbox read only the 50 newest jobs, so an older waiting job
  vanished. It now also reads every queued, running, and waiting job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(worker): retry failures only where the retry can work

Codex review findings on #144, round 2:

- Retrying a failed planning interview started an unrelated one under a
  new plans/<id>. The retry now keeps the target, which the jobs route
  accepts, so the new job replaces the failure.
- A failed quest job offered "Try again" after the quest stopped being
  ready, which the route then refused. The inbox explains instead.
- An older failure beyond the 50-job history showed as fresh work or
  vanished. The inbox also reads each target's latest failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(worker): read each target's latest job and encode quest links

Codex review findings on #144, round 3:

- A finished job beyond the 50-job window let its still-open issue, PR,
  or quest show as fresh work. The inbox now reads each target's latest
  job however old: every one awaiting a decision, plus any other whose
  target is still observed. An index on (repository, target, createdAt)
  backs the lookup, replacing the 50-job window.
- Quest links interpolated raw paths, so a `#` or `?` in a filename
  broke the board link. They now reuse the board's path encoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(worker): limit publication replies by characters, not JSON length

The inbox's reply box allows 1,000 characters, but the answer route
checked the JSON-serialized length against 1024, so quotes, backslashes,
and line breaks could get a valid reply refused as too large. The route
now checks the text itself against the same 1,000-character limit the
form uses, after normalizing the CRLF line breaks forms submit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(quest): plan batched inbox reads for m1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(quest): plan retries that continue from the published branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated deleted the quest/m0/kix/publication branch October 21, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant