Compare commits

...
Author SHA1 Message Date
Nick 810af91368 merge research-structuralization (ROBOT_TOKEN in auto-version + canary contract update) 2026-09-08 06:29:30 +01:00
Nick 7089285a3e Merge remote-tracking branch 'gitea/research-structuralization' into sweep-live 2026-09-08 06:23:10 +01:00
Nick 4a33074b6a fix(release): auto-version authenticates via ROBOT_TOKEN — the reserved GITEA_TOKEN is the read-only auto token (gitea-actions bot cannot commit to protected master; 403 'user cannot commit to repo') 2026-09-08 06:22:55 +01:00
conductor 99de8ce1be Merge pull request 'fix(release): surface gate self-heals base drift + drift detail to stdout' (#240) from research-structuralization into master 2026-09-08 06:18:44 +01:00
conductor a1dcdadde2 Merge branch 'master' into research-structuralization 2026-09-08 06:17:03 +01:00
Nick 56bc364832 fix(release): surface gate recovers from base drift — unchanged surfaces carrying a stale version are force-edited to NEW (with evidence line); drift detail printed to stdout (stderr was swallowed by runner logs) 2026-09-08 06:15:19 +01:00
conductor 8bcf6a81b0 Merge pull request 'chore: trigger release pipeline (empty commit — proves direct-commit bump)' (#239) from trigger-bump into master 2026-09-08 06:09:39 +01:00
Nick d1121f6f52 chore: trigger release pipeline — proves direct-commit bump end-to-end (surfaces aligned by #238) 2026-09-08 06:08:06 +01:00
conductor 01b0129c2e Merge pull request 'chore(release): reconcile version surfaces — pyproject 0.49.0→0.54.1, README badge' (#238) from version-reconcile into master 2026-09-08 06:04:23 +01:00
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. 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 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) 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) 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 2026-09-08 05:34:51 +01:00
Nick 0e060d863b chore: post-#208 ledger maintenance — S110 79→80 (new guarded except in shard fix), BLE001 351→348 (merge removed blind excepts), memory_selector registered in umbrella map 2026-09-08 05:32:48 +01:00
conductor 41c4f75b76 Merge pull request 'P1-03: unify memory selection and restore true abstention (closes #26)' (#208) from p1-03-memory-selection into master 2026-09-08 05:29:18 +01:00
7 changed files with 156 additions and 20 deletions

No files matched your search

+18 -7
View File
@@ -31,7 +31,7 @@ jobs:
- name: Auto-bump if the PR did not carry a version bump
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
RELEASE_SERVER_URL: ${{ gitea.server_url }}
RELEASE_REPO: ${{ gitea.repository }}
PR_TITLE: ${{ gitea.event.pull_request.title }}
@@ -126,20 +126,31 @@ jobs:
drifts = []
for rel, pat in checks:
if rel not in edited:
# unchanged surface: must ALREADY carry NEW
# unchanged surface: must ALREADY carry NEW — if it carries a
# DIFFERENT version (base drift, e.g. pyproject at 0.49.0 while
# master is 0.54.1), RECOVER by force-editing to NEW. The gate
# stays: nothing commits unless every surface carries NEW.
status, cur = api(f"/contents/{rel}?ref=master")
text = base64.b64decode(cur.get("content", "")).decode("utf-8", "replace") if status == 200 else ""
if status != 200:
drifts.append(f"{rel}: unreadable ({status})")
continue
text = base64.b64decode(cur.get("content", "")).decode("utf-8", "replace")
hits = re.findall(pat, text)
if NEW not in hits:
drifts.append(f"{rel}: unchanged but does not carry {NEW} (found {sorted(set(hits))})")
fixed = re.sub(pat, lambda m: m.group(0).replace(m.group(1), NEW), text, count=1)
if fixed != text:
edited[rel] = (fixed, cur["sha"])
print(f" RECOVERED {rel}: base drift {sorted(set(hits))} -> {NEW}")
else:
drifts.append(f"{rel}: pattern present but unfixable (found {sorted(set(hits))})")
else:
hits = re.findall(pat, edited[rel][0])
if NEW not in hits:
drifts.append(f"{rel}: edited content does not carry {NEW}")
if drifts:
print("VERSION-SURFACE DRIFT — bump aborted, nothing committed:", file=sys.stderr)
print("VERSION-SURFACE DRIFT — bump aborted, nothing committed:")
for d in drifts:
print(f" - {d}", file=sys.stderr)
print(f" - {d}")
sys.exit(1)
# Commit each surface DIRECTLY to master (bookkeeping, not reviewable code)
@@ -162,7 +173,7 @@ jobs:
PYBUMP
- name: Create release from merged pull request
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_TOKEN: ${{ secrets.ROBOT_TOKEN }}
PR_TITLE: ${{ gitea.event.pull_request.title }}
PR_BODY: ${{ gitea.event.pull_request.body }}
RELEASE_API_URL: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases
+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:
+2 -1
View File
@@ -17,7 +17,8 @@
"drift_cache",
"drift_compression",
"tier_manager",
"graph_shard_bridge"
"graph_shard_bridge",
"memory_selector"
],
"canaries": [
"canary_query_memory",
+3 -3
View File
@@ -1,4 +1,4 @@
{
"BLE001": 351,
"S110": 79
}
"BLE001": 348,
"S110": 80
}
+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"