Skip to content

feat(turns): warn when active stories touch the same files - #304

Open
boudydegeer wants to merge 1 commit into
theam:mainfrom
boudydegeer:feat/cross-story-collision-guard
Open

feat(turns): warn when active stories touch the same files#304
boudydegeer wants to merge 1 commit into
theam:mainfrom
boudydegeer:feat/cross-story-collision-guard

Conversation

@boudydegeer

Copy link
Copy Markdown

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:

  • one story.collision_detected fact per overlapping story is appended to the timeline (external key turn:<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;
  • one turn.collisions_detected turn event summarizes the affected stories;
  • the prompt gains a section headed "Other active stories touch files you changed" listing those stories and paths, asking the agent to keep edits to them minimal and to mention the overlap in its commit or pull request description.

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_failed and 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_files already 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

  • Evidence, not attention. flagAttention moves the story to attention and clears the active agent, and an open attention item keeps the story out of ready after 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 story GET, the UI, and facility_get_story without new fields.
  • Only completed, cleanly captured evidence counts. Rows with capture_error or without completed_at are skipped, so the current turn's own freshly inserted row and any failed capture cannot produce a false overlap.
  • A story with no completed evidence cannot collide. Its files are unknown. The first turn of a new story is therefore unwarned; every later turn is covered. Documented in the lifecycle reference.
  • Exact paths. Directory-level or module-level overlap would catch more and also warn more; exact paths are the conservative first step and the data model does not preclude a coarser mode later.
  • detectCollisions is 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

  • No migration and no schema change. New fact type story.collision_detected on story_evidence_events (source facility) and new turn event types turn.collisions_detected / turn.collision_check_failed.
  • No manifest, API, SDK, or MCP contract change. The timeline entries flow through existing shapes.
  • Cost: three bounded queries per dispatch, all on existing indexes (stories_org_project_status_idx, github_pull_requests_project_state_idx, turn_git_evidence_story_completed_idx). Only the path column 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.
  • Prompt size: at most 10 stories × 20 paths plus two sentences, well under the existing transcript budget.
  • No cost, budget, credential, or security implication. The prompt lists file paths that the same principal can already read from the timeline of every story in the project.
  • Suggested title classification: feat (patch in 0.x). Not breaking: existing stories, manifests, and clients are unaffected.

Verification

  • pnpm verify passes 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).
  • Behaviour verified beyond the test suite: the new integration suite drives the real TurnDispatcher, StoryWorkspaceService, TurnGitEvidenceService, and StoryCollisionService against 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.
  • Documentation updated: reference/lifecycle.md (turn evidence section), guides/operate-story.md (what the timeline entry means), and the documentation contract test now requires story.collision_detected to be documented.

Commands run:

DATABASE_URL=postgres://facility:facility@127.0.0.1:5461/facility_test \
  pnpm --filter @facility/api exec vitest run test/story-collisions.test.ts               # 2 passed
DATABASE_URL=postgres://facility:facility@127.0.0.1:5461/facility_test \
  pnpm --filter @facility/api exec vitest run test/story-collisions.integration.test.ts   # 2 passed
pnpm verify                                                                               # exit 0

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:
    • two stories change 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, one story.collision_detected fact with the expected external key and data, one turn.collisions_detected event, no attention item, and the story is not in attention;
    • the overlap is symmetric: the first story's next turn is warned about the second;
    • three stories share 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

  • Should merged-branch exclusion also honour 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.
  • Would you rather the prompt block ask the agent to add an explicit note to the PR body, instead of "when it matters"? I kept the softer wording to avoid noisy PR descriptions for trivial overlaps.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Warn agents when two active stories touch the same files

1 participant