feat(turns): warn when active stories touch the same files - #304
Open
boudydegeer wants to merge 1 commit into
Open
feat(turns): warn when active stories touch the same files#304boudydegeer wants to merge 1 commit into
boudydegeer wants to merge 1 commit into
Conversation
Before an engine starts, compare the changed files recorded for the story with the evidence of every other active story in the project that has an open branch. Overlaps are recorded on the story timeline as story.collision_detected facts and listed in the prompt so the agent keeps its edits to those paths minimal. Done, archived, deleted stories and branches whose pull request already merged are ignored. The check is advisory: it never blocks a turn and never creates attention.
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.
What changes
Closes #303
Before the engine starts, the dispatcher compares the changed files recorded for the story with the recorded changed files of every other active story in the project that has an open branch. When they overlap:
story.collision_detectedfact per overlapping story is appended to the timeline (external keyturn:<turnId>:collision:<otherStoryId>, so a retried turn does not duplicate it), carrying the other story's id, title, provider identity, branch, status, up to 20 overlapping paths, and the total count;turn.collisions_detectedturn event summarizes the affected stories;Stories that are
done, archived, deleted, without a branch, or whose branch belongs to a merged pull request in the mirror are ignored. Overlapping stories are sorted by overlap size; at most 10 are listed in the prompt and the rest are counted.The check is advisory. It never blocks or fails a turn, never creates attention, and a detection failure is recorded as
turn.collision_check_failedand the turn continues without the block.The story page's timeline summary line renders the new fact as
overlaps "<title>" on N files · <branch>.Why
A story is serial inside itself, but stories run in parallel, and two agents editing the same file on two branches discover it as a merge conflict on the second pull request, after both have been reviewed.
turn_git_evidence.changed_filesalready holds every path each turn changed; nothing compared one story's evidence with another's. This makes that evidence useful before the conflict instead of after.Design notes
flagAttentionmoves the story toattentionand clears the active agent, and an open attention item keeps the story out ofreadyafter the turn. That is the right signal for "a human must act", and the wrong one for "be careful with these paths". The timeline already carries per-turn facts and is exposed through the storyGET, the UI, andfacility_get_storywithout new fields.capture_erroror withoutcompleted_atare skipped, so the current turn's own freshly inserted row and any failed capture cannot produce a false overlap.detectCollisionsis isolated in the dispatcher with its own try/catch so a query problem is recorded on the turn rather than turning into a failed dispatch.Persistence, compatibility, and release classification
story.collision_detectedonstory_evidence_events(sourcefacility) and new turn event typesturn.collisions_detected/turn.collision_check_failed.stories_org_project_status_idx,github_pull_requests_project_state_idx,turn_git_evidence_story_completed_idx). Only thepathcolumn is extracted from the JSON (jsonb_path_query_array), so the control plane never loads full evidence rows. Runs once per turn, before the engine; the engine's own runtime dominates by orders of magnitude.feat(patch in 0.x). Not breaking: existing stories, manifests, and clients are unaffected.Verification
pnpm verifypasses locally (Node 24.13.1, pnpm 11.20.0, Docker 29.2.1): lint, typecheck, clean build, both isolated databases recreated, all 25 critical API suites (each in its own process, skips forbidden), remaining package tests, removed-component check, guards, audit (2 high, both pre-existing and ignored by repo config).TurnDispatcher,StoryWorkspaceService,TurnGitEvidenceService, andStoryCollisionServiceagainst Postgres with the fake workspace runtime, with an engine that writes exactly the files each turn should be blamed for, so the evidence rows the guard reads are produced by the same capture path as production.reference/lifecycle.md(turn evidence section),guides/operate-story.md(what the timeline entry means), and the documentation contract test now requiresstory.collision_detectedto be documented.Commands run:
Coverage added:
services/api/test/story-collisions.test.ts: sorted intersection; empty block without collisions; path cap with(+N more); story cap with the "N more stories" line.services/api/test/story-collisions.integration.test.ts:src/shared.ts; the first turn of each is unwarned (no evidence yet); the next turn of the second story gets the prompt block naming the first story and only the shared path, onestory.collision_detectedfact with the expected external key and data, oneturn.collisions_detectedevent, no attention item, and the story is not inattention;lib/core.ts; after one is archived and another's branch is recorded as a merged pull request, the remaining story's next turn has no block and no facts.Not run: the Docker-backed workspace E2E tier. This change does not touch workspace execution boundaries.
Open questions
github_branches.deleted_at, for branches deleted without a mirrored pull request? Left out to keep the rule explainable in one sentence; it is a one-clause change.