Skip to content

feat: file uploads and an AI-agent path on the submit page - #114

Merged
crypticpy merged 17 commits into
mainfrom
feat/submit-uploads
Oct 17, 2026
Merged

crypticpy merged 17 commits into
mainfrom
feat/submit-uploads

Conversation

@crypticpy

Copy link
Copy Markdown
Owner

Summary

Two independent additions to /submit/, both off-by-default or zero-config for existing deployments.

File uploads (optional, signed-in submitters only). Screenshots and decks can be attached on the submit page instead of dragged onto the issue afterwards.

  • services/uploads/: a stateless Vercel function (zero deps). It verifies the submitter's GitHub token, sniffs and validates the file (PNG/JPEG/WebP; PDF ≤ 4 MiB), and writes it with the deployment's own GitHub App to a private uploads repo as one parentless commit per file on u/<user-id>/<set>/<sha256>. Vercel hosts nothing. Per-user caps of 3 drafts and 9 files are re-checked after each write.
  • Issue bodies carry upload:<set>/<sha256>.<ext> references. The scaffolder resolves them only under the issue author's id, using a read-only token minted per job and passed to the scaffold step only. It re-hashes the bytes and applies the existing image and attachment checks. A failed mint or a missing file becomes a warning on the draft and never a failed submission.
  • uploads-cleanup.yml deletes files when the issue (or its merged draft) closes, and runs a daily sweep for anything older than 14 days that no open submission names. It fails closed when it can't list issues. The scaffolder never deletes.
  • Off unless submit.uploads_url is set; requires signin_url.

"Using an AI agent?" npm run generate now writes submit/agent-instructions.txt and submit/answers.schema.json from _data/schema.yml. Import answers on /submit/ loads the agent's JSON into the form and runs the form's own checks.

Design and as-built notes: docs/submit-uploads-plan.md. Admin setup: docs/admin-guide.md → Files attached on the submit page. Decision log: docs/decisions.md (2026-10-16).

Review

/freview (completeness + principal review) found 1 blocker, 2 major and 4 minor issues. All are fixed in 40a2452 and 435ef32:

  • a cleanup paging loop on matching-refs
  • a mint failure that failed every submission
  • a remove race on the page
  • a fallback that dropped files
  • pr-closed cleanup with the issue still open
  • the concurrency group
  • per-user caps racing under parallel POSTs

uploads.js was split into a controller and a stateless uploads-view.js.

Test plan

  • npm test: 1309/1309
  • npm run test:build: 155/155
  • npm run lint, npm run validate, generate --check, prettier --check ., actionlint
  • npm run test:flows (a11y, browser): not run locally
  • End to end on a deployment: deploy services/uploads, create the private uploads repo and App, set UPLOADS_APP_ID/UPLOADS_APP_PRIVATE_KEY/UPLOADS_REPO and submit.uploads_url, then submit with a screenshot and a deck

Known gap: a declined submission's files wait for the daily sweep (up to 14 days), because the automation closes the issue with GITHUB_TOKEN, which starts no workflow.

🤖 Generated with Claude Code

coa-chrisc and others added 13 commits October 16, 2026 18:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
services/uploads/ validates a signed-in submitter's screenshot or PDF and
writes it straight to the deployment's private uploads repository through its
own GitHub App, one parentless commit and ref per file. Stores nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
new-entry.yml gives only the scaffold step a read-only token for the uploads
repository. uploads-cleanup.yml deletes a submission's files when its issue
closes, and sweeps unreferenced files older than 14 days. Both do nothing
while UPLOADS_REPO is unset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scaffolder reads files attached on the submit page from the private uploads
repository, under the issue author's id only, and runs them through the same
checks as downloaded files: images.mjs and attachments.mjs now split fetching
from checking and saving. Only scripts/lib/uploads.mjs sees the uploads token.
Output without upload: lines is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With submit.uploads_url set (and sign-in on), signed-in submitters get a picker
on the screenshots and file questions. Images are re-encoded in the browser,
each file uploads as it is picked, the draft remembers what was uploaded, and
the issue body carries upload: references. Blank uploads_url leaves the page
exactly as it was; validate.mjs rejects uploads_url without signin_url.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A declined submission's issue is closed with the built-in GITHUB_TOKEN,
which starts no issues: closed run, so its files waited for the daily
sweep. Also run on pull_request: closed for same-repository entry/
drafts, resolve the issue from the branch name, and keep any set another
open submission by the same author still names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm run generate writes submit/agent-instructions.txt and
submit/answers.schema.json from _data/schema.yml, so an AI agent helping
a submitter gets the exact questions and options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Read git/matching-refs in one call: GitHub ignores per_page/page there,
  so the paging loop never ended once the uploads repo held 100 refs.
- A failed uploads token mint no longer fails every submission; the
  scaffolder drafts the entry and warns per upload: answer.
- pr-closed cleanup keeps files while the issue is still open.
- One concurrency group per closed issue or pull request, so queued
  close runs are not cancelled.
- Re-check the per-user caps after a write and remove only the branch
  this request created, so parallel POSTs cannot exceed them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- removeItem looks the item up again after its DELETE answers, so two
  quick removes no longer take out the wrong file.
- A failed direct send with uploaded files falls back to the copy-out
  box, which keeps the upload: lines, instead of a link that drops them.
- Split the page's upload rendering into uploads-view.js (stateless);
  uploads.js keeps the state and the requests.

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

chatgpt-codex-connector Bot commented Oct 16, 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-17T00:13:43.832976Z 697de15 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.

Comment thread test/scripts/agent_files.test.mjs Fixed

@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: 435ef3245a

ℹ️ 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 assets/js/submit.js
coa-chrisc and others added 3 commits October 16, 2026 18:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a 401 the page is signed out, so count() was 0 and the fallback
opened GitHub's form without the files, unannounced. The files are still
held in the draft: the fallback now says only a fresh sign-in sends them
and still offers GitHub's form without them.

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: 697de15f29

ℹ️ 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 scripts/lib/uploads.mjs Outdated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@crypticpy
crypticpy merged commit 8a3df50 into main Oct 17, 2026
27 checks passed
@crypticpy
crypticpy deleted the feat/submit-uploads branch October 17, 2026 00:28
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.

3 participants