A collaborative coding-agent orchestrator for atproto radl.app

integration with tangled issues #99

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

Tangled issues as candidate goals #

A project whose remote is on tangled now has an Issues on tangled section at the foot of its page: the repository's open issues, offered as goals somebody could write. Importing one writes nothing — it opens the ordinary goal composer with the issue's title and body already in it, saying whose words they are, and the reader creates the goal. What an agent ever reads is therefore a goal a member wrote, having read the text and edited anything an agent should not be told to do.

That is the answer to the request's own concern, and it is structural rather than a matter of framing. Ingestion polls member repos for com.disnetdev.radial.*, so an sh.tangled.repo.issue is in no RecordStore, no materialize() and no bundle — with no code at all. The whole path is the one guests.ts already established for comments from outside a space, rule for rule.

What was added #

packages/ui/src/lib/issues.ts Discovery: parse a tangled remote, resolve the repository's own DID, list open issues from tangled's appview, re-read each from its filer's own PDS, drop the ones already taken up, and produce the composer seed and the goal create arguments.
packages/ui/src/lib/components/ProjectIssues.svelte The section: muted, behind the guest rail, "not a member" on every row, previews interpolated as text.
com.disnetdev.radial.goal.source Optional {kind, uri, cid?} — where an imported goal's text was read from.
radial goal create --source-kind/--source-uri/--source-cid The one write path, unchanged in shape: the browser writes through the same runCli() a shell does.

Discovery is view-time and opt-in: the section does not query the appview until a reader presses Look for issues, and remembers the answer per browser profile. Tangled's appview is a third party with no SLA, its answer is not part of the fold (two materializers agree whether or not it answered), and a page that queried it on sight would also be telling tangled which repositories this browser reads.

Three rules the module keeps, each tested:

  • The index is a hint, never an authority. The appview says where an issue is; every candidate is re-read from the repo of whoever filed it and must still say it belongs to this repository. An issue that cannot be re-read is counted and not shown, never rendered from the appview's copy.
  • The identity rule. A tangled repository's DID is not its owner's, and comparing one against the other is silently always false. The resolution is the daemon adapter's own algorithm — owner handle → DID, then the sh.tangled.repo record under that owner (both rkey conventions), whose repoDid is the answer. Verified live against disnetdev.com/radial, whose owner holds records in both conventions.
  • Already-imported issues drop out, for everybody in the space rather than for whoever imported one, which is why source is on-protocol.

The lexicon change #

goal gains an optional source, additive on the same terms as artifact.title (a goal signed before the field existed cannot be rewritten, so it must never become required). kind is free-form like closeGoal.disposition; the one kind this build mints, tangled-issue, is checked to name an sh.tangled.repo.issue record, in validateInvariants and again in the CLI.

It is the author's assertion of provenance, not a proof — deliberately. The title and body are what a human read and signed, so re-fetching the issue and copying its bytes (what comment bless does, because a blessing quotes somebody verbatim) would take the editing away and buy nothing. It has exactly one consumer: hiding a row in the candidate list.

It is not in the turn bundle. An agent handed the at-uri could fetch the live issue and read comments and edits nobody reviewed, which is the thing the import gate exists to prevent. core/test/bundle.test.mjs asserts a sourced goal produces a bundle with no trace of the issue anywhere in it — over the whole serialized object, since bundle.md and bundle.json are both rendered from it.

Provenance #

  • Branch radial/impl-fcc5eb65fb07, commit 48a6487a744cf52f9201a1813735413e7d7c8c1d, one commit on main (d16de74). Pushed; Radial opens the pull request.
  • pnpm codegen && git diff --exit-code, pnpm build, pnpm typecheck, pnpm lint, pnpm test all clean — core 206, atproto 32, ingest 20, sidecar 64, daemon 547, ui 49 + 576.
  • New tests: ui/src/lib/issues.test.ts (27, pure), ui/src/lib/components/ProjectIssues.svelte.test.ts (7, jsdom — including that nothing is fetched before a reader asks, and that a hostile issue body reaches the screen as text and produces no elements), sidecar/test/goal-source.test.mjs (4), plus the bundle and validation cases in core and a candidate row in ui/test/thread-grid.test.mjs.
  • Docs: design §4 (the record) and §13 (the prompt-injection paragraph, beside the guest-comment one), packages/lexicons/README.md, packages/ui/README.md.

Remaining risks #

  • GitHub projects are not covered. tangledRepo() returns "no opinion" for a non-tangled remote and the section does not draw, which is honest but partial: GitHub's issues are reachable and offering them is a second adapter. Nothing here forecloses it — source.kind is free-form and a github-issue goal validates today.
  • The row cannot link an individual issue. Tangled's issue pages are numbered by its appview's database, and the number is in neither the record nor the appview's JSON, so the section links the repository's issue list rather than guessing a URL.
  • An issue edited after import is not noticed. source.cid pins the version that was read, so the question is answerable later, but nothing watches for it. The blessed-comment surface badges this because the bundle carries the snapshot; here the goal is the snapshot, in the member's own words, so there is less to say — a divergence badge would be a follow-up, not a gap in the gate.
  • The appview is a single point of discovery. TANGLED_APPVIEW is a constant for that reason; an operator who needs to own it has one place to point it at, but there is no configuration record for it yet.

Radial artifact

Commits 0