A collaborative coding-agent orchestrator for atproto radl.app

integration with userinput.app #109

Closed disnetdev.com
targeting main from radial/impl-cb618fc8b335
Closed opened by and assigned to disnetdev.com
targeting main from radial/impl-cb618fc8b335

Addressed both request_changes findings on the human-gated userinput intake (impl-znqy7sftgvuxqmqsuji32okd).

Foreign strongrefs now honour the version they pin #

packages/ui/src/lib/userinput.ts gained one documented policy, split by what a pin buys:

  • Authority needs an exact uri#cid pin. moderators() honours an owner's app.userinput.member grant only against the board version this tab fetched; effectiveStatus() and liveDiscussion() honour a moderator status and an author edit only against the discussion version fetched. A superseded pin falls back — to the owner alone (the board URI's DID, which cannot be rewritten), to open, to the original text. The status case is the one with teeth: a planned set against text its author has since replaced would otherwise be carried onto the new text, which is exactly how a member gets walked into importing something else.
  • Presentation still resolves by URI. A discussion's space ref grants nothing — it names the board it was posted to, and every discussion older than the owner's last board edit legitimately pins a superseded version, so an exact match there would empty the section on a rename with no safety gain. goalsByOrigin stays URI-keyed for the same reason. This is a deliberate deviation from the finding's "board or discussion" wording; the rest of the finding is implemented as written.
  • Dropped pins are reported, not swallowed. discoverUserinput returns staleGrants and each row carries stale; the Userinput page states both, because "no moderator has looked at this" and "a moderator looked at an earlier draft" are different sentences.

New pure tests cover the dropped grant (owner-only fallback, unauthorized status ignored), the dropped status and edit, and the stale counters.

Permutation coverage for the new record type and index rules #

goldenScenario() now carries setFeedbackSource and origin-bearing goals, so the 200-permutation whole-index deepEqual covers them:

  • four setFeedbackSource records — one board toggled off→on (enabled, and the later record is the one the view names), one toggled on→off (associated, absent from the view), a non-admin's, and one naming a project this space does not hold (both ignored as unresolved);
  • two goals imported from one discussion at two different CIDs — two members importing the same feedback — proving goalsByOrigin groups by URI so a feedback edit cannot un-badge an import.

Assertions live in a new feedback sources and imported goals block in materializer.test.mjs, including that a project with no source lists none and a goal nobody imported is in no group.

Also updated docs/adr-userinput-intake.md with the pin policy and added the missing src/lib/userinput.ts row to packages/ui/README.md's module map.

Checks #

pnpm lint (typecheck + type-strip + generated-file check), pnpm test (all packages; ui 565 passed), and pnpm codegen with a clean git diff — all pass. No packages/daemon changes.

Remaining risks #

An owner who rewrites their board record in place silently loses every moderator grant until they reissue them; the section says so and degrades to owner-only authority rather than failing open. Likewise a discussion rewritten in place (rather than revised via app.userinput.edit) reads as open until its status is re-set. Both are fail-closed by choice. The earlier operational risks stand: a Constellation or foreign PDS outage still hides feedback, reported separately from an empty board, and two members can still import the same discussion twice.

Radial artifact

Commits 0