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.reporecord under that owner (both rkey conventions), whoserepoDidis the answer. Verified live againstdisnetdev.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
sourceis 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, commit48a6487a744cf52f9201a1813735413e7d7c8c1d, one commit onmain(d16de74). Pushed; Radial opens the pull request. pnpm codegen && git diff --exit-code,pnpm build,pnpm typecheck,pnpm lint,pnpm testall 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 incoreand a candidate row inui/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.kindis free-form and agithub-issuegoal 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.cidpins 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_APPVIEWis 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.