Skip to content
Merged
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
12 changes: 7 additions & 5 deletions skills/loop-engineering/references/hosts.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ Keep monitoring bounded and report changes rather than repeating unchanged rows.
## Installation

Keep this skill as the canonical source. Inspect `scripts/install_audit.py --help`
before consolidation. `--canonical <skill-dir> --link-identical` replaces only
identical copies; any divergent root rejects all writes. Check `--root <dir>
--dry-run` before an authorized `--apply`; nonstandard roots must be explicit.
Exit 0 is clean, 2 usage error, 3 divergence. Installation permission is separate
from permission to edit source. Never overwrite a divergent installed copy.
before consolidation. Run with `--canonical <skill-dir> --root <dir>` to audit
without writes; nonstandard roots must be explicit. After authorization, add
`--link-identical` to replace only identical copies. Any divergent root or
unavailable Git probe rejects the initial repair batch. Exit 0 is clean, 1 means
attention is needed (copy, divergence, stale or unverified evidence, or failed
repair), and 2 is a usage/setup error. Installation permission is separate from
permission to edit source. Never overwrite a divergent installed copy.
6 changes: 4 additions & 2 deletions skills/loop-engineering/references/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ not verify external truth, execute actions, dispatch workers, or schedule wakeup

Check the subject and the question: repository, identity, revision, query and
runtime must match the claim. An empty result needs a known discovery/control
path; an incorrectly scoped query is not negative evidence. Two values derived
from the same source are not independent confirmation.
path; an incorrectly scoped query is not negative evidence. A failed or
unlaunchable probe is unavailable evidence, not proof of absence; do not use it
to authorize repair. Two values derived from the same source are not independent
confirmation.

For an uncertain implementation, record the hypothesis, observable falsifier and
replay check once in the task. Report decisions and concise rationale, not private
Expand Down
23 changes: 14 additions & 9 deletions skills/loop-engineering/scripts/install_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,14 @@ def classify(
return result | {"status": status, "digest": digest}


class GitProbeError(RuntimeError):
def __init__(self, directory: pathlib.Path, reason: str):
self.directory = str(directory)
super().__init__(f"git probe {reason} in {directory}")


def git_output(directory: pathlib.Path, *arguments: str) -> str | None:
"""Read-only git call; None when git is missing or the command fails."""
"""Read Git output; None for completed misses, an error for unavailable probes."""
try:
result = subprocess.run(
["git", "-C", str(directory), *arguments],
Expand All @@ -90,10 +96,10 @@ def git_output(directory: pathlib.Path, *arguments: str) -> str | None:
timeout=15,
check=False,
)
except subprocess.TimeoutExpired:
raise
except (OSError, subprocess.SubprocessError):
return None
except subprocess.TimeoutExpired as exc:
raise GitProbeError(directory, f"timed out after {exc.timeout}s") from exc
except (OSError, subprocess.SubprocessError) as exc:
raise GitProbeError(directory, f"could not run: {exc}") from exc
return result.stdout.strip() if result.returncode == 0 else None


Expand Down Expand Up @@ -334,12 +340,11 @@ def main() -> int:
args = build_parser().parse_args()
try:
return audit(args)
except subprocess.TimeoutExpired as exc:
except GitProbeError as exc:
# A failed observation is neither an absent Git repository nor clean drift.
# Initial audits of every root finish before any optional repair starts.
directory = str(exc.cmd[2])
message = f"git probe timed out after {exc.timeout}s in {directory}"
render([{"path": directory, "status": "unverified", "error": message}], args.json)
message = str(exc)
render([{"path": exc.directory, "status": "unverified", "error": message}], args.json)
print(f"install-audit: {message}; retry the audit before relying on it", file=sys.stderr)
return 1

Expand Down
25 changes: 17 additions & 8 deletions skills/loop-engineering/tests/test_install_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ def make_copy(self, name: str) -> pathlib.Path:
shutil.copytree(self.canonical, destination)
return destination

def assert_timeout_refuses_repairs(self, roots, should_timeout):
def assert_probe_failure_refuses_repairs(self, roots, should_fail, fault=None, expected="timed out"):
original_run = subprocess.run
def run_with_timeout(command, **kwargs):
if should_timeout(command):
raise subprocess.TimeoutExpired(command, 15)
if should_fail(command):
raise fault if fault is not None else subprocess.TimeoutExpired(command, 15)
return original_run(command, **kwargs)
argv = [str(SCRIPT), "--canonical", str(self.canonical), "--json", "--link-identical"]
for root in roots:
Expand All @@ -81,28 +81,37 @@ def run_with_timeout(command, **kwargs):
self.assertEqual(rc, 1)
result = json.loads(out.getvalue())
self.assertEqual(result[0]["status"], "unverified")
self.assertIn("timed out", result[0]["error"])
self.assertIn("timed out", err.getvalue())
self.assertIn(expected, result[0]["error"])
self.assertIn(expected, err.getvalue())
self.assertNotIn("no-git", out.getvalue())
for root in roots:
self.assertTrue(root.is_dir())
self.assertFalse(root.is_symlink())
self.assertFalse(list(self.root.rglob("*.backup-*")))

def test_unlaunchable_git_is_unverified_and_refuses_all_repairs(self):
first, later = self.make_copy("first"), self.make_copy("later")
for fault in (FileNotFoundError("git missing"), PermissionError("git not executable")):
for failing_root in (self.canonical, later):
with self.subTest(fault=type(fault).__name__, root=failing_root):
self.assert_probe_failure_refuses_repairs(
[first, later], lambda cmd: pathlib.Path(cmd[2]) == failing_root.resolve(),
fault=fault, expected="could not run")

def test_canonical_timeout_refuses_all_repairs(self):
copied = self.make_copy("copied")
self.assert_timeout_refuses_repairs([copied], lambda cmd: pathlib.Path(cmd[2]) == self.canonical.resolve())
self.assert_probe_failure_refuses_repairs([copied], lambda cmd: pathlib.Path(cmd[2]) == self.canonical.resolve())

def test_later_root_timeout_refuses_earlier_identical_repair(self):
first, later = self.make_copy("first"), self.make_copy("later")
self.assert_timeout_refuses_repairs([first, later], lambda cmd: pathlib.Path(cmd[2]) == later.resolve())
self.assert_probe_failure_refuses_repairs([first, later], lambda cmd: pathlib.Path(cmd[2]) == later.resolve())

def test_drift_query_timeout_is_not_missing_remote_or_unknown(self):
work, installed = self.make_clone_pair()
self.canonical = work / "skills/loop-engineering"
for operation in ("cat-file", "rev-list"):
with self.subTest(operation=operation):
self.assert_timeout_refuses_repairs(
self.assert_probe_failure_refuses_repairs(
[installed / "skills/loop-engineering"], lambda cmd: cmd[3] == operation)

def test_audit_classifies_source_link_copy_and_absence(self) -> None:
Expand Down
Loading