Skip to content

feat(kix): render planning decisions into PRs from recorded answers - #172

Merged
kixelated merged 9 commits into
mainfrom
quest/m0/kix/paper-trail
Oct 21, 2026
Merged

kixelated merged 9 commits into
mainfrom
quest/m0/kix/paper-trail

Conversation

@kixelated

@kixelated kixelated commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Completes quest/m0/kix/paper-trail.md.

Problem

A planning PR's paper trail was whatever the agent wrote, so it could paraphrase or drift from what the maintainer actually chose, and it carried untrusted prompt text straight into a public PR body.

Behavior

  • src/jobs/trail.ts renders a ## Decisions section from the job's answered agent questions whose prompt is a decision, one ### Round N per answered prompt. Each option gets ✅ (chosen) or ⬜, and free text shows after ✍️. Permission requests and the service's account and publication questions are left out, so runners and accounts never reach the PR.
  • Every prompt and answer string appears only as an inline code span. Line breaks and controls become spaces, bidi controls are dropped, and the fence is one backtick longer than any run inside. GitHub renders no Markdown or HTML inside a code span and links no @user/@org/team mention, #N/GH-N/owner/repo#N reference, commit SHA, bare URL, or closing keyword. I checked this against GitHub's own renderer (POST /markdown, gfm, context kixelated/quest): code spans linked nothing, and the same text outside them linked mentions, issues, the commit, and the URL.
  • publish adds the section to every plan delivery, between <!-- kix:decisions --> markers. Any section the agent wrote, including a forged or unterminated one, is dropped first. Re-delivery renders the section again from the record, so it updates in place and is never duplicated. If the body would exceed GitHub's 65,536-character limit, the rounds that don't fit are counted in a note instead of being cut mid-round.
  • Each runner assign now carries instructions (null except for plan). A planning job's instructions tell the agent not to write the trail but still to record settled decisions in the quest Plans. The shared skills are unchanged, so local agents keep writing their own trail. runner.md tells the future runner to forward these instructions.
  • Docs: cloudflare/GITHUB.md#publication, cloudflare/README.md, and quest/m0/kix/planning.md. The finished quest is deleted.

Validation

  • just check and just test pass in the Nix shell (cloudflare: 232 tests; all: 269).
  • test/trail.test.ts covers the rendered format; skipping permission prompts, account prompts, and invalid answers; 15 hostile inputs, parsed as GFM with marked, which must yield no link, image, emphasis, blockquote, or extra HTML, and no untrusted text outside code spans (Markdown, @user, @org/team, Closes/fixes/RESOLVES, #N, owner/repo#N, GH-N, SHAs and owner/repo@sha, bare URLs and www., GitHub issue URLs, emails, HTML and comment markers, backtick runs, blank-line breakouts, bidi and line separators); replacing a forged section; and the body limit. A mutation that drops the code spans fails the hostile test.
  • test/publish.test.ts runs a multi-round interview with a permission round, free text, and an account limit whose account question must not appear, then checks a re-delivery after another round updates the one section. A start job's body is left untouched.
  • test/jobs.test.ts checks the instructions on planning and other assignments.

Decisions made without asking (recommended options)

  • Code spans rather than zero-width breaks. Code spans neutralize every GitHub pattern at once, including repository-specific custom autolinks that can't be enumerated, and copied text stays exact.
  • Option labels only. Descriptions are left out to keep the record compact; the inbox still shows them.
  • Section appended at the end of the body rather than placed by the agent.
  • Planning PRs only, as the quest scopes it.
  • Instructions as a field on assign, since the runner that will use them doesn't exist yet.

Suggested follow-ups

  • Render the same record for implementation (start) PRs whose jobs asked decisions, if wanted.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 21, 2026 11:57
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service now writes a planning pull request's decision section from the
job's answered decision questions, with a check mark on each choice and the
maintainer's own words. Permission requests and account or publication
questions stay out. Every prompt and answer appears only in a code span, so
GitHub links no mention, reference, URL, or closing keyword in them. Each
delivery re-renders the section and drops any the agent wrote, and planning
assignments tell the agent not to write the trail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 21, 2026 19:08
@kixelated

Copy link
Copy Markdown
Owner Author

Outcome: this is complete and ready for review. It was started by a background /quest-start agent from a /quest-spawn session.

  • Planning PRs now get a ## Decisions section that the service renders from the job's answered decision rounds. Each option gets ✅ or ⬜, and any free text follows ✍️. Permission, account, and publication questions are left out.
  • All untrusted prompt and answer text appears only as escaped inline code spans. GitHub's own /markdown renderer confirmed that mentions, issue, cross-repo and commit references, bare URLs, and closing keywords inside those spans link nothing.
  • Re-deliveries replace the section, including any copy the agent wrote. Planning assign messages carry instructions telling the agent not to write the trail.
  • just check and just test pass in the Nix shell.

No decisions are open. The PR body lists the routine choices I made (code spans over zero-width breaks, labels without descriptions, the section appended at the end, planning PRs only, instructions on assign) and one optional follow-up: the same record for start PRs.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 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-21T20:47:46.209622Z 4947d5f New commits
🔒 Security Review ✅ Completed 2026-10-21T20:52:03.655727Z 4947d5f 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.

kixelated and others added 3 commits October 21, 2026 12:44
…es its PR

A retried plan job continues the failed job's pull request (#171), but
the decision section rendered only the new job's answers, so the first
delivery after a retry erased the earlier rounds. Render the answers of
every job on the target that delivered the same pull request.

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: 0add401c30

ℹ️ 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/trail.ts Outdated
*/
export function withDecisions(body: string, questions: { prompt: unknown; answer: unknown }[]): string {
const own = body
.replace(/<!-- kix:decisions -->[\s\S]*?(<!-- \/kix:decisions -->|$)/g, "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Prevent embedded end markers from escaping sanitization

When a later publication passes the rendered PR body back into withDecisions, an untrusted prompt or answer containing <!-- /kix:decisions --> makes this non-greedy regex stop at the marker inside its code span. The remainder then becomes own outside the span and is prepended to the fresh section, so content following the marker—such as mentions or Closes #1—can regain GitHub semantics and leaked fragments accumulate on subsequent deliveries. Remove the generated section without interpreting marker text inside its contents as a delimiter, or neutralize marker strings before rendering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 1f61fb8: a section now ends only at an end marker on its own line (or the end of the body). Untrusted text is always inside a code span after a list prefix, with line breaks flattened, so it can never start a line. The hostile-input test now includes a marker followed by Closes #6 @octocat and checks that copying the published body back yields the same section with nothing outside code spans.

(Written by Claude Opus 5.5)

Comment thread cloudflare/src/jobs/trail.ts Outdated
* backtick run inside, so the text cannot close it.
*/
export function code(value: string): string {
const flat = value.replace(/[\u202a-\u202e\u2066-\u2069]/g, "").replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, " ");

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 Remove all Unicode bidirectional controls

When untrusted text contains U+061C, U+200E, or U+200F, this sanitization leaves those Unicode bidi-control characters intact while removing only embedding, override, and isolate controls. Such input can therefore render with a misleading visual order in the public decision record despite the function's guarantee that bidi controls are dropped; include the remaining Bidi_Control code points in the removal set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 1f61fb8: code() now also drops U+061C, U+200E, and U+200F, covering every Bidi_Control code point. Tests cover each one.

(Written by Claude Opus 5.5)

kixelated and others added 3 commits October 21, 2026 13:22
An untrusted prompt containing the end marker stopped the section match
inside its code span when an agent copied the published body back, so
the rest of the section leaked outside code spans. Code spans never start
a line, so the section now ends only at an end marker on its own line, or
at the end of the body. Also drop U+061C, U+200E, and U+200F with the
other bidirectional controls.

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: 7415b7d691

ℹ️ 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".

.replaceAll(END, "")
.trimEnd();
const section = render(questions, BODY_LIMIT - own.length - 2);
return section ? (own ? `${own}\n\n${section}` : section) : own;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the required attribution at the end

For every planning PR with at least one recorded decision, this append places the generated section after the agent-supplied body, so an otherwise compliant (written by <model>) footer is no longer the final content of the GitHub post. Preserve that footer after the generated section, or insert the section before it, so publication continues to satisfy the repository policy.

AGENTS.md reference: AGENTS.md:L31-L31

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Not changing this. The (written by <model>) line is this repository's convention for agents developing quest (AGENTS.md), not part of the shared workflow in assets/AGENTS.md, and the core stays independent of any one repository's conventions. Detecting a trailing attribution line would couple kix to that convention. The appended section is the service's own content, labeled "Recorded by kix from the maintainer's answers", so the agent's attribution still ends the agent's text. Appending at the end of the body is one of this PR's recorded decisions, which the maintainer accepted. If an agent should control where the section goes, a follow-up could honor an empty <!-- kix:decisions --> placeholder the agent writes.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Owner Author

Merge summary for head 4947d5f.

Changes since the PR opened:

Decisions:

Review: Codex reviewed 1f61fb8 with no findings and 7415b7d with the one declined finding. Since then the only change is a clean merge from main. CI passes, and just check and just test pass locally.

Follow-ups:

  • A failed planning job that published commits but never opened a pull request still loses its rounds when retried, because decisions keys on the shared pull request. Recording which job a retry continues would cover that.
  • Optionally honor an empty <!-- kix:decisions --> placeholder so an agent can choose where the section goes.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit d0a2981 into main Oct 21, 2026
5 checks passed
@kixelated
kixelated deleted the quest/m0/kix/paper-trail 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