Conversation
🎩 PreviewA preview build has been created at: |
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 18, 2026 21:19
694dfab to
b3ac3e8
Compare
camielvs
force-pushed
the
09-18-feat_run_a_pipeline_in_a_project
branch
from
September 18, 2026 21:19
ca51a8c to
39a3270
Compare
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 18, 2026 21:26
b3ac3e8 to
b59af04
Compare
camielvs
force-pushed
the
09-18-feat_run_a_pipeline_in_a_project
branch
from
September 18, 2026 21:26
39a3270 to
3771df4
Compare
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 18, 2026 21:45
b59af04 to
3689e2f
Compare
camielvs
force-pushed
the
09-18-feat_run_a_pipeline_in_a_project
branch
from
September 18, 2026 21:45
3771df4 to
3725bff
Compare
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 18, 2026 22:09
3689e2f to
53a3efb
Compare
camielvs
force-pushed
the
09-18-feat_run_a_pipeline_in_a_project
branch
from
September 18, 2026 22:09
3725bff to
0d1309e
Compare
Draft
Collaborator
Author
camielvs
marked this pull request as draft
September 18, 2026 22:19
This was referenced Sep 18, 2026
This was referenced Sep 22, 2026
This was referenced Sep 22, 2026
camielvs
force-pushed
the
09-18-feat_run_a_pipeline_in_a_project
branch
from
September 23, 2026 20:57
0d1309e to
e369ae1
Compare
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 23, 2026 20:57
53a3efb to
826eab1
Compare
This was referenced Sep 23, 2026
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 23, 2026 23:00
826eab1 to
17d4348
Compare
camielvs
force-pushed
the
09-18-feat_run_a_pipeline_in_a_project
branch
2 times, most recently
from
September 23, 2026 23:36
0099091 to
fc948ad
Compare
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 23, 2026 23:36
17d4348 to
d1aef85
Compare
camielvs
force-pushed
the
09-18-feat_run_a_pipeline_in_a_project
branch
from
September 23, 2026 23:57
fc948ad to
b5bacfe
Compare
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 23, 2026 23:57
d1aef85 to
cddb017
Compare
A run belongs to a project by carrying one annotation per project, the id in the key and a constant value — so these are a set of memberships, and a run can sit in several projects at once. The data layer is plural for that reason, even though each way of starting a run picks one. Attribution has to be right at submission. The endpoint that sets a single annotation afterwards takes the key as a path segment and rejects this one for its slashes, encoded or raw, so there is no repairing a run that was submitted unattributed. Every run in the app is created through one function, so the project travels as one option on it. Nothing passes it yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mutation was private to the editor's submitter, which is the only place a run could be started from. Moved out as it was, with two additions: the project a run belongs to, and the invalidation that makes a project's run feed notice it — otherwise the feed is stale for five minutes and a run you just started appears to have gone nowhere. Behaviour is unchanged for the editor, which passes no project. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Picking a pipeline in a project now offers to run it, in the project's own context: the run carries the project, so it lands in the feed on the same page. It works for a pipeline the backend holds and for one held in this browser, since the preview already has the parsed spec of either. The arguments dialog is reused as it is, so a pipeline with required inputs is asked about exactly as it would be in the editor — except for the offer to copy arguments from a past run, which is withheld for a backend pipeline. Those runs are found by name in this browser, so for a pipeline the backend holds they would belong to a different pipeline that merely shares its name. A pipeline that does not validate is not offered at all, rather than submitted to fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A rerun is the same pipeline run again, so it belongs where the original belonged. Every project of it, not one: memberships are a set, and a rerun that kept only the first would quietly drop the rest. The projects are read at the moment of the rerun rather than on render. A click that beat the read would submit a copy belonging nowhere, and that cannot be corrected afterwards — so the read is also allowed to fail the rerun, since a refused rerun is something the reader can simply do again. Nothing is asked of the backend when projects are switched off, so a reader not using them keeps reruns that cannot fail this way. The same run's annotations were being cached under three different keys, one of them varying by whether the id arrived as a string or a number, so they are now read through one set of options. The rerun therefore usually finds them already loaded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opening a pipeline from a project now says so in the editor, and the runs started there belong to that project until the reader says otherwise. The project lives in the url rather than in the editor's state, for two reasons. A reload or a duplicated tab keeps it, which is what "the runs of this tab" has to mean. And the chip that names it is nowhere near the submitter that acts on it — in one editor the submitter is not even visible — so both read one value instead of keeping two copies that can disagree. An id off a link or a bookmark is only acted on once a project answers to it, since a run attributed to a project nobody can see cannot be corrected. A project that is merely slow, or a backend that fails for some other reason, keeps the attribution; only a project that is gone drops it. With projects switched off the id is ignored altogether, so a shared url cannot attribute runs in a UI that never mentioned a project. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dialog that asks for a run's arguments now also asks which project it belongs to, offering the projects that hold this pipeline. Choosing one sets the same url the editor already reads, so the chip and the field are two views of one answer rather than two answers. Nothing answers "which projects hold this pipeline" — there is no such endpoint, and resources cannot be filtered by what they point at — so it is one resource listing per project. Those listings are read through the very query the project page uses, so a pipeline just added to a project is offered here without a second cache to invalidate. A project's pointer records the name a pipeline had when it was added, so a pipeline renamed since is recognised by the registry id carried beside the name. One renamed without ever having an id cannot be recognised, and is missing from the projects holding it — the same limit the preview already documents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
camielvs
force-pushed
the
09-18-feat_local_pipelines_in_projects
branch
from
September 24, 2026 00:13
cddb017 to
a25f507
Compare
camielvs
force-pushed
the
09-18-feat_run_a_pipeline_in_a_project
branch
from
September 24, 2026 00:13
b5bacfe to
96a6606
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
A project can hold pipelines and it can list runs, but nothing in the app could make a run that belongs to a project — the run feed was read-only, and the one run attached to a project on the dev backend had been put there by something other than this app.
The association already exists in the backend's read path: a run belongs to a project when it carries the run-level annotation
tangleml.com/project/project-id/<project_id>. So this is not a new data model, it is the missing write path.All three ways in are here: from the project page, from the editor, and by rerunning a run that already belonged to one.
Memberships are a set, not a field
The project id lives in the annotation's key, with a constant value. That makes these annotations a set of memberships: a run can belong to several projects at once. The data layer is plural (
projectIds) for that reason, even though each way of starting a run picks one — which also means a rerun can carry a multi-project run's attribution across without dropping any.Attribution is write-once
PUT /api/pipeline_runs/{id}/annotations/{key}takes the key as a path segment, and 404s on this key — URL-encoded or raw — while a slash-free key on the same run returns 200. Probed both ways.So there is no repairing a run that was submitted unattributed, and no revoking one that was attributed wrongly. Attribution has to be right at submission. That prices every "the project id is quietly dropped" path as permanent, and is why the remaining cases validate the id where it comes from somewhere untrusted, and keep it verbatim where it is a historical fact.
Where the project comes from
Every run in the app is created through one function,
submitPipelineRun, so the project travels as one option on it and all three entry points share it. What differs is only where the id comes from.From the project page, it is definitional — you pressed the button on a resource row of that project, so there is nothing to validate. The preview already holds the parsed spec for a pipeline the backend holds and for one held in this browser, so both can be run, and the existing argument dialog is reused as-is rather than growing a second arguments UI.
In the editor, it lives in the
?projectId=search param rather than in component state, for two reasons. A reload or a duplicated tab keeps it, which is what "the runs of this tab" has to mean; and the chip that names it is nowhere near the submitter that acts on it — in the v2 editor the submitter issr-only— so both read one value instead of keeping two copies that can disagree. The dialog's project field writes that same param, so the field and the chip are two views of one answer. An id off a link or a bookmark is only acted on once a project answers to it: a project that is merely slow, or a backend failing for some other reason, keeps the attribution, and only a project that is gone drops it. With the flag off the param is ignored altogether, so a shared url cannot attribute runs in a UI that never mentioned a project.On a rerun, it is read from the run being rerun, and every project of it rather than one. The read happens inside the mutation, not on render: a click that beat it would submit a copy belonging nowhere. The read is also allowed to fail the rerun, since a refused rerun is something you can simply do again, where losing the attribution is permanent. Nothing is asked of the backend when projects are off, so reruns elsewhere in the app cannot fail this new way.
Which projects hold a pipeline has no endpoint to ask — and resources cannot be filtered by what they point at — so it is one resource listing per project, read through the very query the project page already uses. No second cache to invalidate: a pipeline just added to a project is offered immediately.
Also here
["projects", id, "runs"]— because the resource mutations document why the loose["projects", id]prefix must not be invalidated, and a test pins that.Verification
npm run validateand the full suite green — 2815 tests. Among them: that a submission with no project is byte-identical to today's (every run in the app goes through this path, so that is a regression guard rather than a feature test); that a digits-only project id arriving as a number is still read as a string, since the router JSON-parses search params; and that a junk param attributes nothing rather than inventing a project.Not yet verified in a browser. The end-to-end claim that matters — that the backend accepts the annotation we write at creation and then returns the run in the project's feed — has only unit coverage so far. The script for it is written and waiting; the dev backend was unavailable when it came time to run it. Worth doing before this merges.
Known limitations
?pipeline_id=filter on the projects list would remove the whole scan — worth raising with the backend team.🤖 Generated with Claude Code