Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions skills/codex-refactor-loop/SKILL.md

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions skills/codex-refactor-loop/authorizations/runtime-exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,24 @@ in `SKILL.md` and the tests.
<!-- Refactor (issue-264): Old: mirror allowed skip on one fresh pidfile wrapper.
New: mirror narrows skip to pid alive + fresh heartbeat + current fingerprint + zero duplicate canonical live wrapper for the same static allowlist command. -->
<!-- Refactor (issue-298): Old: mirror covered only write-side restart helper health semantics. New: mirror adds read-only daemon-status projection over the same facts while write repair/reload remains restart-daemons. -->
- allowed: cron or launchd helper maintains singleton wrappers, actor-owned heartbeat leases, helper-private launch fingerprints at `.refactor-loop/locks/<daemon>.fingerprint.json`, and helper-private `DaemonProcessInventory` for the existing static daemon allowlist; pid alive plus fresh heartbeat plus current fingerprint plus zero duplicate canonical live wrapper for the same resolved static allowlist command is the only skip condition, missing, malformed, or mismatched fingerprint data fails closed to restart, and duplicate canonical wrappers fail closed to repair/reconcile before restart; runs 24h log retention. `consensus-rnd-cli daemon-status --json` is a read-only daemon-status projection over the same static allowlist, pid/heartbeat/fingerprint readers, cached active-controller status, and `DaemonProcessInventory`; repair/reload remains restart-daemons.
- allowed: cron or launchd helper maintains singleton wrappers, actor-owned heartbeat leases, helper-private launch fingerprints at `.refactor-loop/locks/<daemon>.fingerprint.json`, and helper-private `DaemonProcessInventory` for the existing static daemon allowlist; pid alive plus fresh heartbeat plus current fingerprint plus zero duplicate canonical live wrapper for the same resolved static allowlist command is the only skip condition, missing, malformed, or mismatched fingerprint data fails closed to restart, and duplicate canonical wrappers fail closed to repair/reconcile before restart; runs canonical RuntimeRetention before daemon freshness checks. `consensus-rnd-cli daemon-status --json` is a read-only daemon-status projection over the same static allowlist, pid/heartbeat/fingerprint readers, cached active-controller status, and `DaemonProcessInventory`; repair/reload remains restart-daemons.
- forbidden: no host-defined daemon registry, generic process supervisor, GitHub/git lifecycle authority, codex spawn, commit, push, merge, label, archive, index, new daemon, issue lifecycle, PR lifecycle, tag, release, wrapper sidecar heartbeat writer, public start/stop/restart/reload lifecycle verb, or generic lifecycle authority.
- verification: `test_restart_daemons.py`, `test_anti_stop_restart_helper_contract.py`, `test_cli_command_router.py`, `test_log_retention.py`, `test_runtime_exception_authorization_sources.py`
- verification: `test_restart_daemons.py`, `test_anti_stop_restart_helper_contract.py`, `test_cli_command_router.py`, `test_runtime_retention.py`, `test_runtime_exception_authorization_sources.py`
- no_new_runtime_authority: This mirror only replaces the missing ignored judge-log authorization path.

<a id="runtime-retention-437"></a>
## runtime-retention-437

- surface: `consensus-rnd-cli runtime-retention`
- source_issue: `#437`
- source_round: `r4`
- source_marker: `META_JUDGE_DONE:consensus:structural:choose canonical runtime-retention owner with one-release log-retention alias and #437 narrow local-GC carveout`
- skill_anchor: `#named-runtime-exception--runtime-retentionper-437`
- allowed: active-controller owner only; host opt-in is `$RUNTIME_RETENTION_ENABLE=true`; `RuntimeRetention` is the only canonical owner; `consensus-rnd-cli log-retention` is a one-release compatibility alias with the same handler and authority; delete only planner-eligible generated regular files under `$REPO_ROOT/.refactor-loop/{logs,prompts,runs}`; same-inode compact `$REPO_ROOT/.refactor-loop/.controller-pending-events.log`; consume `.refactor-loop/state/runtime-retention-plan.json` as the planner proof for stale `$REPO_ROOT/.worktrees/<name>` entries with `no_in_flight`, `no_open_issue_or_pr`, `no_dirty`, `no_local_ahead`, and `merged_or_missing_safe`, recheck local git read projections, then run only `git worktree remove <path>` and `git worktree prune`.
- forbidden: no GitHub write, no issue/PR create/edit/close/merge, no label mutation, no tag/release, no `git fetch`, no branch deletion, no commit, no push, no reset, no rebase, no merge, no tag, no archive/index durable fact source, no host config edit, no `.refactor-loop/host.env` as host production SSOT, no worktree cleanup without planner proof, no public lifecycle command, no daemon ownership expansion, and no generic lifecycle actor.
- verification: `test_runtime_retention.py`, `test_log_retention.py`, `test_cli_command_router.py`, `test_restart_daemons.py`, `test_runtime_exception_authorization_sources.py`, `test_skill_reference_anchors.py`, `test_anti_stop_restart_helper_contract.py`
- no_new_runtime_authority: This mirror narrows local GC to RuntimeRetention only; it does not authorize GitHub lifecycle, git branch lifecycle, release lifecycle, generic cleanup, host production config ownership, or a second retention owner.

<a id="phase9-router-open-state-gate-229"></a>
## phase9-router-open-state-gate-229

Expand Down
3 changes: 3 additions & 0 deletions skills/codex-refactor-loop/host.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export UPDATE_CHECK_INTERVAL_SECONDS="21600"
# Default: GitHub release/tag read timeout seconds.
export UPDATE_CHECK_TIMEOUT_SECONDS="5"

# Default/noop: RuntimeRetention local cleanup opt-in. False/empty does nothing.
export RUNTIME_RETENTION_ENABLE="false"

# Default: minimum recent merges required by auto_release_gate stability.
export RELEASE_AUTO_MIN_MERGES="1"

Expand Down
15 changes: 12 additions & 3 deletions skills/codex-refactor-loop/scripts/codex_refactor_loop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from .release.gate import main as release_gate_main
from .release.required_checks import main as release_required_checks_main
from .restart import main as restart_main
from .retention import main as retention_main
from .runtime_retention import main as runtime_retention_main
from .sync.dev import main as dev_sync_main
from .phase9.router import main as phase9_router_main
from .update_check import main as update_check_main
Expand Down Expand Up @@ -81,7 +81,7 @@ class CommandSpec:
"restart-daemons": CommandSpec(
restart_main,
"run the Python daemon restart helper",
("spawn-daemon", "write-state", "delete-log"),
("spawn-daemon", "write-state", "delete-runtime"),
),
"daemon-status": CommandSpec(
daemon_status_main,
Expand Down Expand Up @@ -167,7 +167,16 @@ class CommandSpec:
("read-source", "read-state"),
),
"check-manifest": CommandSpec(manifest_main, "run manifest version sync check", ("read-source",)),
"log-retention": CommandSpec(retention_main, "run daemonless log retention", ("delete-log",)),
"runtime-retention": CommandSpec(
runtime_retention_main,
"run canonical RuntimeRetention for skill-private generated artifacts",
("delete-runtime", "git-worktree"),
),
"log-retention": CommandSpec(
runtime_retention_main,
"one-release compatibility alias for runtime-retention",
("delete-runtime", "git-worktree"),
),
"check-project-rules": CommandSpec(
project_rules.main,
"check host project rules fixed points and write patch artifact when needed",
Expand Down
19 changes: 12 additions & 7 deletions skills/codex-refactor-loop/scripts/codex_refactor_loop/restart.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .active_controller import require_active_controller, write_active_controller_status
from .context import LoopContext, LoopContextError
from .gh_accounting import accounting_env
from .retention import retain_logs
from .runtime_retention import retain_runtime, runtime_retention_enabled
from .update_check import maybe_run_update_check


Expand Down Expand Up @@ -260,7 +260,7 @@ def run(self) -> int:
return 0
self._acquire_restart_lock()
try:
self._run_log_retention()
self._run_runtime_retention()
for name, command in DAEMON_COMMANDS:
self.start_daemon(name, command)
finally:
Expand Down Expand Up @@ -342,14 +342,19 @@ def _prepare_dirs(self) -> None:
for path in (self.ctx.paths.refactor_loop / "locks", self.ctx.paths.heartbeats, self.ctx.paths.logs):
path.mkdir(parents=True, exist_ok=True)

def _run_log_retention(self) -> None:
def _run_runtime_retention(self) -> None:
try:
deleted, kept, target, missing = retain_logs(self.ctx.repo_root)
result = retain_runtime(self.ctx.repo_root, enabled=runtime_retention_enabled(self.ctx))
except Exception:
self._log("log_retention warning: helper failed; continuing daemon restart")
self._log("runtime_retention warning: helper failed; continuing daemon restart")
return
suffix = " missing=true" if missing else ""
self._log(f"log_retention: ttl_hours=24 deleted={deleted} kept={kept} target={target}{suffix}")
suffix = " missing=true" if result.missing else ""
self._log(
"runtime_retention: "
f"enabled={str(result.enabled).lower()} ttl_hours=24 deleted={result.deleted} kept={result.kept} "
f"compacted_events={str(result.compacted_events).lower()} removed_worktrees={result.removed_worktrees} "
f"pruned_worktrees={str(result.pruned_worktrees).lower()} target={result.target}{suffix}"
)

def _run_update_check(self) -> None:
try:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,62 +1,14 @@
"""Daemonless 24 hour log retention for .refactor-loop/logs."""
"""Compatibility alias for RuntimeRetention."""

from __future__ import annotations

import os
import sys
import time
from pathlib import Path
from typing import Sequence

from .context import LoopContext, LoopContextError


RETENTION_TTL_HOURS = 24


def retain_logs(repo_root: Path, *, now: float | None = None) -> tuple[int, int, Path, bool]:
repo_real = repo_root.resolve()
log_dir = repo_real / ".refactor-loop" / "logs"
if log_dir != repo_real / ".refactor-loop" / "logs":
raise RuntimeError(f"log retention target escaped .refactor-loop/logs: {log_dir}")
if not log_dir.is_dir():
return 0, 0, log_dir, True
cutoff = int(now if now is not None else time.time()) - RETENTION_TTL_HOURS * 60 * 60
deleted = 0
kept = 0
for path in log_dir.iterdir():
try:
if path.is_symlink() or not path.is_file():
kept += 1
continue
if path.suffix != ".log":
kept += 1
continue
if int(path.stat().st_mtime) < cutoff:
path.unlink(missing_ok=True)
deleted += 1
else:
kept += 1
except OSError:
kept += 1
return deleted, kept, log_dir, False
from .runtime_retention import main as runtime_retention_main


def main(argv: Sequence[str] | None = None) -> int:
del argv
try:
ctx = LoopContext.load(cwd=os.getcwd())
except LoopContextError as exc:
sys.stderr.write(f"FATAL: {exc}\n")
return 2
try:
deleted, kept, target, missing = retain_logs(ctx.repo_root)
except RuntimeError as exc:
sys.stderr.write(f"FATAL: {exc}\n")
return 2
suffix = " missing=true" if missing else ""
print(f"log_retention: ttl_hours={RETENTION_TTL_HOURS} deleted={deleted} kept={kept} target={target}{suffix}")
return 0
return runtime_retention_main(argv)


if __name__ == "__main__":
Expand Down
Loading