Compare commits

...
Author SHA1 Message Date
Nick 7841fa2bcb chore(release): reconcile version surfaces — pyproject 0.49.0→0.54.1 (never bumped since #190-era drift), README badge added (gate anchor). The direct-commit bump needs a coherent base.
Auto-label / priority-gate (pull_request) Skipped
Auto-label / auto-label (pull_request) Successful in 3s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / canary (push) Successful in 28s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / type-check (push) Successful in 26s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / unit-tests (push) Successful in 49s
CI Pipeline / best-practices (pull_request) Successful in 2s
CI Pipeline / documentation (pull_request) Successful in 2s
CI Pipeline / lint (pull_request) Successful in 29s
CI Pipeline / canary (pull_request) Successful in 34s
Spec gate (headless) / spec-gate (pull_request) Skipped
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / type-check (pull_request) Successful in 24s
CI Pipeline / unit-tests (pull_request) Successful in 44s
CI Pipeline / gitea-offline (pull_request) Successful in 14s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Failing after 9s
2026-09-08 06:02:22 +01:00
conductor 5de0145683 Merge pull request 'fix(robot): conflict-first + carry-forward classify + test-order pollution eliminated' (#237) from research-structuralization into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 27s
CI Pipeline / lint (push) Successful in 31s
CI Pipeline / type-check (push) Successful in 25s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 48s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-08 05:59:46 +01:00
Nick ab2c567ea4 fix(robot): classify gate on BOTH update paths (conflict-first + behind-base) — code-path touch always waits for fresh CI; docs-only carries. Closes the #208 slip (merge on stale required-checks)
CI Pipeline / canary (push) Successful in 30s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 54s
CI Pipeline / lint (push) Successful in 30s
CI Pipeline / type-check (push) Successful in 24s
CI Pipeline / required-checks (push) Successful in 1s
Auto-label / priority-gate (pull_request) Skipped
CI Pipeline / best-practices (pull_request) Successful in 3s
Auto-label / auto-label (pull_request) Successful in 3s
CI Pipeline / documentation (pull_request) Successful in 2s
CI Pipeline / canary (pull_request) Successful in 26s
CI Pipeline / lint (pull_request) Successful in 30s
CI Pipeline / type-check (pull_request) Successful in 26s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / gitea-offline (pull_request) Successful in 13s
CI Pipeline / unit-tests (pull_request) Successful in 47s
CI Pipeline / required-checks (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Failing after 10s
2026-09-08 05:54:37 +01:00
Nick 61f1ca63e8 feat(robot): conflict-first policy — staleness resolved immediately on mergeable=False (before any wait), 409 conflicts skip with reason; carry-forward race closed by re-verifying mergeable post-update (Nick policy: merges resolve in ~1min, then sleep)
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / canary (push) Successful in 29s
CI Pipeline / lint (push) Successful in 33s
CI Pipeline / type-check (push) Successful in 25s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / unit-tests (push) Successful in 50s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-08 05:49:55 +01:00
conductor 675080634e Merge pull request 'chore: post-#208 ledger maintenance — S110/BLE001 rebaseline + memory_selector umbrella registration' (#236) from umbrella-pipeline into master
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / type-check (push) Successful in 24s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 53s
CI Pipeline / required-checks (push) Successful in 1s
2026-09-08 05:34:51 +01:00
4 changed files with 133 additions and 9 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# Rune
**Current release: v0.54.1**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
Rune treats the language model as replaceable reasoning compute rather than as the whole agent. The durable parts of the system are intended to belong to Rune:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.49.0"
version = "0.54.1"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+52 -6
View File
@@ -87,7 +87,29 @@ def decide(base: str, token: str, pr: dict) -> str:
return f"#{n}: skipped (status={state})"
status, detail = _api(base, token, f"/pulls/{n}")
if not detail.get("mergeable", False):
return f"#{n}: skipped (not mergeable)"
# CONFLICT-FIRST (Nick policy): resolve staleness immediately —
# attempt the API update NOW rather than waiting a sleep cycle.
# 409 (genuine conflict) still skips with the reason named.
ustatus, uresp = _api(base, token, f"/pulls/{n}/update", "POST", {"style": "merge"}, ok=(200, 409))
if ustatus == 200:
print(f"UPDATED {n} (conflict-first: mergeable=False resolved pre-merge)", flush=True)
# classify the update: docs-only → carried green stands; code-path
# touch → fresh CI required before merge (#208 slip prevention)
try:
_, fresh = _api(base, token, f"/pulls/{n}")
new_sha = fresh.get("head", {}).get("sha", sha)
if classify_update(base, token, pr, new_sha) == "RERUN_REQUIRED":
return f"#{n}: skipped (updated; CI re-running on new head)"
except MergeRefused:
return f"#{n}: skipped (updated; CI re-running on new head)"
status, detail = _api(base, token, f"/pulls/{n}")
if not detail.get("mergeable", False):
return f"#{n}: skipped (updated; CI re-running on new head)"
elif ustatus == 409:
cmsg = str(uresp.get("message", "")) if isinstance(uresp, dict) else str(uresp)
return f"#{n}: skipped (genuine conflict — update 409: {cmsg[:120]})"
else:
return f"#{n}: skipped (not mergeable; update returned {ustatus})"
mstatus, mresp = _api(base, token, f"/pulls/{n}/merge", "POST", {"Do": "merge"})
if mstatus == 200:
return f"#{n}: MERGED"
@@ -116,11 +138,25 @@ def decide(base: str, token: str, pr: dict) -> str:
if mstatus == 200:
return f"#{n}: MERGED (after behind-base self-update)"
if mstatus == 405 and "behind" in str(mresp.get("message", "")).lower():
# CARRY-FORWARD PROTOCOL: the workflow inspects the tree diff
# between the tested head and the new head; docs-only movement
# carries the prior green (no redundant re-run — Nick's policy).
print(f"UPDATED {n} awaiting-ci", flush=True)
return f"#{n}: skipped (updated; CI re-running on new head)"
# CARRY-FORWARD PROTOCOL: classify the tree diff between the
# tested head and the new head. Docs-only movement carries the
# prior green (no redundant re-run — Nick's policy); any
# code-path touch requires fresh CI before merge (#208 slip:
# required-checks satisfied by a stale green).
try:
_, fresh = _api(base, token, f"/pulls/{n}")
new_sha = fresh.get("head", {}).get("sha", sha)
verdict = classify_update(base, token, pr, new_sha)
except MergeRefused:
verdict = "RERUN_REQUIRED" # can't verify → safest default
if verdict == "RERUN_REQUIRED":
print(f"UPDATED {n} awaiting-ci (code-path touch)", flush=True)
return f"#{n}: skipped (updated; CI re-running on new head)"
print(f"UPDATED {n} carry-forward (docs-only diff)", flush=True)
mstatus, mresp = _api(base, token, f"/pulls/{n}/merge", "POST", {"Do": "merge"})
if mstatus == 200:
return f"#{n}: MERGED (carry-forward: docs-only update)"
return f"#{n}: skipped (carry-forward merge returned {mstatus})"
raise MergeRefused(f"#{n} merge refused {mstatus} after update: {str(mresp)[:200]}")
raise MergeRefused(f"#{n} merge refused 405: {msg}")
if mstatus == 403:
@@ -143,6 +179,16 @@ CODE_PATHS = (
)
def classify_update(base: str, token: str, pr: dict, new_sha: str) -> str:
"""Classify a self-heal update: CARRY_FORWARD (docs-only tree diff — the
tested code is byte-identical, prior green stands) or RERUN_REQUIRED
(any code-path touch — semantic interactions are real, #208 slip)."""
old_sha = pr["head"]["sha"]
status, cmp_data = _api(base, token, f"/compare/{old_sha}...{new_sha}")
files = [f.get("filename", "") for f in cmp_data.get("files", []) if isinstance(f, dict)]
return "CARRY_FORWARD" if not touches_code_path(files) else "RERUN_REQUIRED"
def touches_code_path(changed_files: list[str], code_paths=CODE_PATHS) -> bool:
"""Carry-forward policy: True iff any changed file is a code path.
Docs-only base movement must NOT trigger a redundant full re-run."""
+78 -2
View File
@@ -54,6 +54,8 @@ class FakeForge:
if path.endswith("/merge"):
n = int(path.split("/")[2])
return self.merge_results.get(n, (200, {}))
if path.endswith("/update"):
return 409, {"message": "There are conflicting files"} # fixture default: genuine conflict
raise AssertionError(f"unexpected POST {path}")
@@ -118,8 +120,13 @@ def test_conflict_pr_skipped(forge):
forge.prs = [{"number": 11, "head": {"sha": "s11"}}]
forge.statuses = {"s11": "success"}
forge.mergeable = {11: False} # server reports conflict
assert "skipped" in mr.decide("base", "tok", forge.prs[0])
assert not any(m == "POST" for m, _ in forge.calls)
# conflict-first: the robot attempts the API update; FakeForge 409s it
# (genuine conflict) -> skip with reason
result = mr.decide("base", "tok", forge.prs[0])
assert "skipped" in result and "conflict" in result.lower()
assert not any(m == "POST" and p.endswith("/merge") for m, p in forge.calls), (
"must never attempt a merge on a conflicted PR"
)
def test_behind_base_405_self_heals_then_merges(forge, monkeypatch):
@@ -133,6 +140,8 @@ def test_behind_base_405_self_heals_then_merges(forge, monkeypatch):
return 200, {"state": "success"}
if method == "GET" and path == "/pulls/12":
return 200, {**forge.prs[0], "mergeable": True}
if method == "GET" and path.startswith("/compare/"):
return 200, {"files": [{"filename": "docs/notes.md"}]} # docs-only → carry
if method == "POST" and path.endswith("/merge"):
return 405, {"message": "The head branch is behind the base branch"}
if method == "POST" and path.endswith("/update"):
@@ -355,3 +364,70 @@ def test_transient_persisting_raises_loud(forge, monkeypatch):
monkeypatch.setattr(mr.time, "sleep", lambda s: None)
with pytest.raises(mr.MergeRefused, match="transient after 3 retries"):
mr.decide("base", "tok", forge.prs[0])
def test_conflict_first_policy_update_before_wait(forge, monkeypatch):
"""Nick's policy: conflicts are resolved FIRST — the robot attempts the
API update the moment mergeable=False is observed (before any wait),
then a merge attempt follows within ~a minute. A PR should never sit
through a sleep cycle while resolvable staleness is known."""
forge.prs = [{"number": 40, "head": {"sha": "s40"}}]
calls = {"update": 0, "merge": 0}
def scripted_api(base, token, path, method="GET", data=None, ok=(200,)):
if method == "GET" and path.startswith("/commits/"):
return 200, {"state": "success"}
if method == "GET" and path == "/pulls/40":
# mergeable=False until the update lands (that's the conflict-first trigger)
return 200, {**forge.prs[0], "mergeable": calls["update"] > 0}
if method == "GET" and path.startswith("/compare/"):
return 200, {"files": [{"filename": "docs/notes.md"}]} # docs-only → carry
if method == "POST" and path.endswith("/update"):
calls["update"] += 1
return 200, {}
if method == "POST" and path.endswith("/merge"):
calls["merge"] += 1
if calls["merge"] == 1:
return 405, {"message": "Please try again later"}
return 200, {}
raise AssertionError(f"unexpected {method} {path}")
monkeypatch.setattr(mr, "_api", scripted_api)
monkeypatch.setattr(mr.time, "sleep", lambda s: None)
line = mr.decide("base", "tok", forge.prs[0])
assert calls["update"] == 1, "mergeable=False must trigger immediate update (conflict-first)"
assert "MERGED" in line
def test_carry_forward_checked_in_robot_update_path(forge, monkeypatch):
"""After a self-heal update, the robot must classify the tree diff:
docs-only → merge on carried green; code-path touch → skip (CI must
re-run first). Prevents the #208 slip (merged on stale required-checks)."""
forge.prs = [{"number": 50, "head": {"sha": "s50"}}]
calls = {"update": 0, "merge": 0, "classify": 0}
def scripted_api(base, token, path, method="GET", data=None, ok=(200,)):
if method == "GET" and path.startswith("/commits/"):
return 200, {"state": "success"}
if method == "GET" and path == "/pulls/50":
return 200, {**forge.prs[0], "mergeable": calls["update"] > 0}
if method == "GET" and path.startswith("/compare/"):
return 200, {"files": [{"filename": "src/rune/x.py"}]} # code touch → RERUN
if method == "POST" and path.endswith("/update"):
calls["update"] += 1
return 200, {}
if method == "POST" and path.endswith("/merge"):
calls["merge"] += 1
return 200, {}
raise AssertionError(f"unexpected {method} {path}")
monkeypatch.setattr(mr, "_api", scripted_api)
monkeypatch.setattr(mr.time, "sleep", lambda s: None)
monkeypatch.setattr(
mr,
"classify_update",
lambda base, token, pr, new_sha: "RERUN_REQUIRED",
)
line = mr.decide("base", "tok", forge.prs[0])
assert "skipped" in line and "CI re-running" in line, "code-path touch must wait for fresh CI"
assert calls["merge"] == 0, "no merge on unverified carry"