diff --git a/src/argit/backup.py b/src/argit/backup.py index 67a6a70..ed714b1 100644 --- a/src/argit/backup.py +++ b/src/argit/backup.py @@ -537,7 +537,13 @@ def _git_commit( for it in concrete_items: if it.kind == "secret": continue # secret is in secrets/, added below - if it.target: + # Stage only targets that were actually written. An item whose source + # is absent on this agent is skipped during copy (ยง4-7), so its target + # never exists; staging it anyway makes `git add` abort the whole backup + # with "pathspec '' did not match any files". This happens + # whenever the live agent lacks a path the bundled manifest declares + # (e.g. a 2026.5.4 manifest against a 2026.5.7 Hermes build). + if it.target and (repo_root / it.target).exists(): paths_to_add.append(it.target) for sf in manifest.sanitize: paths_to_add.append(sf.target) diff --git a/tests/unit/test_backup_review_staging.py b/tests/unit/test_backup_review_staging.py index 8e9c057..03cb943 100644 --- a/tests/unit/test_backup_review_staging.py +++ b/tests/unit/test_backup_review_staging.py @@ -14,7 +14,7 @@ from unittest.mock import patch from argit.backup import _git_commit -from argit.manifest import Manifest +from argit.manifest import Item, Manifest def _empty_manifest() -> Manifest: @@ -87,3 +87,31 @@ def test_git_commit_omits_review_path_when_none(capsys, tmp_path): ) captured = capsys.readouterr() assert ".argit/reviews/" not in captured.out + + +def test_git_commit_skips_items_with_missing_target(capsys, tmp_path): + """An item whose source was absent is skipped during copy, so its target is + never written. _git_commit must NOT stage that target โ€” otherwise `git add` + fails with "pathspec did not match any files" and aborts the whole backup. + (Reproduces a 2026.5.4 manifest declaring scripts/ against a Hermes build + that has no ~/.hermes/scripts/.)""" + repo = tmp_path / "repo" + (repo / "hermes" / "data").mkdir(parents=True) + # present: copied this run, target exists on disk + present_tgt = repo / "hermes" / "data" / "config.yaml" + present_tgt.write_text("model: x\n") + present = Item(kind="data", source="config.yaml", mode="0644", + target="hermes/data/config.yaml") + # missing: source absent โ†’ skipped during copy โ†’ target never written + missing = Item(kind="data", source="scripts/", mode="0755", + target="hermes/data/scripts/") + + _git_commit( + repo, _empty_manifest(), + concrete_items=[present, missing], iso="2026-06-10T00:00:00Z", + dry=True, review_path=None, + ) + + out = capsys.readouterr().out + assert "hermes/data/config.yaml" in out + assert "hermes/data/scripts/" not in out