From 1c73b828123a2d37f7855d8ba86830d59df77e27 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 17 Sep 2026 10:34:00 -0600 Subject: [PATCH] feat: create file locks on concurrent builds and scaffolding verification/tests to prevent race conditions and conflicts while reviewing --- assets/agents/adversary/scripts/run_checks.py | 33 +++++++++++++++++++ assets/agents/code-reviewer/graph.yaml | 7 ++++ assets/agents/probe/config.yaml | 8 +++++ assets/agents/review-gauntlet/graph.yaml | 5 +++ assets/agents/sisyphus/config.yaml | 3 ++ 5 files changed, 56 insertions(+) diff --git a/assets/agents/adversary/scripts/run_checks.py b/assets/agents/adversary/scripts/run_checks.py index 299132b..c90d605 100644 --- a/assets/agents/adversary/scripts/run_checks.py +++ b/assets/agents/adversary/scripts/run_checks.py @@ -51,11 +51,43 @@ GRAPH_STATE_FILE, so an inherited live state file would silently win. import json import os import signal +import fcntl import subprocess +import sys import time PER_COMMAND_TIMEOUT_SECS = 900 TOTAL_DEADLINE_SECS = float(os.environ.get("ADVERSARY_RUN_CHECKS_DEADLINE_SECS") or 3300) +STACK_LOCK_PATH = os.path.expanduser("~/.cache/coyote/review-stack.lock") +STACK_LOCK_WAIT_SECS = float(os.environ.get("ADVERSARY_STACK_LOCK_WAIT_SECS") or 1200) + + +def acquire_stack_lock(): + """Serialize with the probe lane's shared-stack work (advisory flock on a + well-known path; the probe is instructed to flock the same file around + stack boot/teardown). Bounded wait so a hogged lock can never turn into a + false red verification: on timeout we proceed WITHOUT the lock and note it + on stderr. The fd is held for the process lifetime (released on exit).""" + try: + os.makedirs(os.path.dirname(STACK_LOCK_PATH), exist_ok=True) + fd = os.open(STACK_LOCK_PATH, os.O_CREAT | os.O_RDWR) + except OSError as e: + sys.stderr.write(f"WARN: review-stack lock unavailable ({e}); running unlocked\n") + return None + limit = time.monotonic() + STACK_LOCK_WAIT_SECS + while True: + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + return fd + except OSError: + if time.monotonic() >= limit: + os.close(fd) + sys.stderr.write( + "WARN: review-stack lock still held after bounded wait — proceeding " + "WITHOUT it; shared-stack collisions with the probe lane are possible\n" + ) + return None + time.sleep(5) DRAIN_TIMEOUT_SECS = 10 TAIL_LINES = 50 MAX_TAIL_CHARS = 8000 @@ -181,6 +213,7 @@ def main(): ) deadline = time.monotonic() + TOTAL_DEADLINE_SECS results = [] + _stack_lock = acquire_stack_lock() if cmds else None # noqa: F841 — held until exit for cmd in cmds: remaining = deadline - time.monotonic() if remaining <= 0: diff --git a/assets/agents/code-reviewer/graph.yaml b/assets/agents/code-reviewer/graph.yaml index 7871d6b..d7a1a58 100644 --- a/assets/agents/code-reviewer/graph.yaml +++ b/assets/agents/code-reviewer/graph.yaml @@ -436,6 +436,13 @@ nodes: (cross-domain concerns from the leaders' reports + teammate alerts, or "None"), `operational_history` (from prior_art_notes; "" when the lane didn't run), `org_context_section` (from org_context_notes; "" when the lane didn't run). + Prior review decisions supplied in the caller context are SETTLED: never + invert or re-litigate one on the same line without NEW evidence (the line + changed, a new failure appeared). If you believe a settled decision is + wrong, do not re-flag it as a finding — surface it in `cross_file` as an + explicit conflict citing the prior decision, so the human sees a + disagreement, not a flip-flop. + prompt: | Domain reports ({{file_count}} changed files, grouped into the domain slices below): {{domain_reports}} diff --git a/assets/agents/probe/config.yaml b/assets/agents/probe/config.yaml index 8fb2841..9d7febd 100644 --- a/assets/agents/probe/config.yaml +++ b/assets/agents/probe/config.yaml @@ -101,6 +101,14 @@ instructions: | ## Rules + 0. **Shared-stack discipline (parallel review lanes).** Wrap every stack boot/teardown + (docker compose, dev servers, fixed-port services) in + `flock "$HOME/.cache/coyote/review-stack.lock" ` — the adversary's + verification stage holds the same lock. A held port NEVER justifies removing or + stopping a container you did not create THIS RUN: wait on the lock, pick another + port, or return INCONCLUSIVE naming the conflict. Killing another lane's container + is a defect, not a remedy. + 1. **Never modify implementation code.** Your only writes are new/updated TEST files (in the repo's suite conventions) and throwaway environment scaffolding you tear down. The implementer owns all fixes. diff --git a/assets/agents/review-gauntlet/graph.yaml b/assets/agents/review-gauntlet/graph.yaml index ba958c5..3fd00a7 100644 --- a/assets/agents/review-gauntlet/graph.yaml +++ b/assets/agents/review-gauntlet/graph.yaml @@ -407,6 +407,11 @@ nodes: The workspace may contain test files from an aborted earlier attempt of this lane; reconcile rather than duplicate. + Shared-stack rule: wrap any stack boot/teardown in + `flock "$HOME/.cache/coyote/review-stack.lock" ` (the adversary's + verification stage holds the same lock), and NEVER remove or stop containers + you did not create this run — a held port means wait, re-port, or INCONCLUSIVE. + Derive expected behaviors from the plan/contract BEFORE reading implementation code, run existing suites first, then author spec-first tests for the uncovered delta. Return your USAGE_PROBE verdict. diff --git a/assets/agents/sisyphus/config.yaml b/assets/agents/sisyphus/config.yaml index 07265d1..d7c2c21 100644 --- a/assets/agents/sisyphus/config.yaml +++ b/assets/agents/sisyphus/config.yaml @@ -314,6 +314,9 @@ instructions: | Diff: Rigor: Security posture: Plan / acceptance criteria: + Prior review decisions: Local-run recipe / usage suites: " --variables {"verification_commands": "[\"cargo test --all\", \"cargo clippy -- -D warnings\"]"} <— the cargo commands above are only an EXAMPLE: replace them with THIS project's exact build/test/lint commands; never guess, and pass "[]" when none apply>