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
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ The `bisync` lane provides explicit bidirectional synchronization between `.gith
- Excludes `local-agent-sync-*` bundles and runtime artifacts (`.venv`, `__pycache__`, `.pytest_cache`, `.pyc`, `.pyo`) from scanning and copying.

### Conflict Resolution

For bundle direct-copy, replace `./.github/scripts/sync_home_ai_resources.py` with `./scripts/run.sh`, keep `--format report` on model-facing runs, and omit `--home-root` (defaults to `$HOME`).

- `only-repo`: when not excluded, `bisync apply` can create the bundle in home from the repository side.
- `only-home`: manual intervention required. Decide whether to keep it only in home, remove it, or add it to the repository.
- `equal-mtime`: hashes differ but mtime is equal. Manual decision required because the winner cannot be determined from timestamps alone.
Expand All @@ -128,6 +130,7 @@ Then follow the exact text layout in `references/sync-contract.md`:
- `doctor`: readiness summary plus a blocker table that answers what failed, why it matters, and what the user must do next.
- `plan`, `audit`, and `bisync plan`: a planned-changes table plus a blockers-and-skips table. For every proposed modification, explain the decision cause, for example repo copy is newer, home copy is newer, a managed resource is stale, or runtime support is not documented enough for apply.
- `apply` and `bisync apply`: an actions-performed table plus a residual-issues table when needed. List each copied, updated, pruned, preserved, skipped, or unchanged resource and state why it was handled that way and how it was verified.
- Include a human-friendly lane label in the status line for install and bisync reports, such as `repo-to-home install` and `repo-home drift`, so the mode is easier to read at a glance.

Never report blocker codes alone. Translate each code into a plain-language reason and the required follow-up. Never say a resource will change without stating what evidence selected the winner or triggered the recommendation. When nothing changes, say so explicitly and still report validation and `next_action`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Text reports must use a table-first layout rather than a raw field dump.
Always start with a short status line that includes:

- mode
- human-friendly lane label, such as `repo-to-home install` for install or `repo-home drift` for bisync
- selected targets
- overall result or status
- blocker count
Expand Down Expand Up @@ -218,8 +219,9 @@ The `bisync` lane provides explicit bidirectional reconciliation between `.githu
- `home_mtime > repo_mtime` -> `home-to-repo`
- `repo_mtime == home_mtime` -> `equal-mtime` blocker
4. For skills only on one side:
- report `only-repo` as an actionable repo-to-home creation candidate;
- report `only-home` as a blocker that requires manual review.

- report `only-repo` as an actionable repo-to-home creation candidate;
- report `only-home` as a blocker that requires manual review.

### Preflight

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,15 @@ def build_bisync_plan(
)
)

all_blocked = sorted(
set(
code
for d in drifts
for code in d.blocked_codes
)
)
all_blocked_set = {
code
for d in drifts
for code in d.blocked_codes
}
for drift in drifts:
if drift.drift_type == "only-repo":
all_blocked_set.add("bisync-only-repo")
all_blocked = sorted(all_blocked_set)

plan = BisyncPlan(
source_root=source_root,
Expand Down Expand Up @@ -371,7 +373,13 @@ def apply_bisync_plan(
plan: BisyncPlan,
) -> BisyncPlan:
if plan.blocked_codes:
return plan
resolvable_during_apply = {"bisync-only-repo"}
remaining_blockers = [
code for code in plan.blocked_codes if code not in resolvable_during_apply
]
if remaining_blockers:
return plan
plan.blocked_codes = []

clean, blocked_code, reason = is_repo_clean(source_root)
if not clean:
Expand Down Expand Up @@ -570,7 +578,12 @@ def _emit_bisync_output(plan: BisyncPlan, format_name: str) -> None:
label = "only in repo" if dtype == "only-repo" else "only in home"
path = drift.repo_path if dtype == "only-repo" else drift.home_path
print(f" {skill}: {label} ({path})")
print(f" blocker: {', '.join(drift.blocked_codes)}")
blocker_codes = list(drift.blocked_codes)
if dtype == "only-repo" and not blocker_codes:
blocker_codes = ["bisync-only-repo"]
elif dtype == "only-home" and not blocker_codes:
blocker_codes = ["bisync-only-home"]
print(f" blocker: {', '.join(blocker_codes)}")
elif dtype == "equal-mtime":
print(f" {skill}: equal mtime (hashes differ)")
print(f" blocker: {', '.join(drift.blocked_codes)}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
}


def _install_lane_label() -> str:
return "repo-to-home install"


def _bisync_lane_label() -> str:
return "repo-home drift"


def build_compact_install_output(payload: dict[str, object]) -> dict[str, object]:
compact: dict[str, object] = {
"mode": payload.get("mode"),
Expand Down Expand Up @@ -214,10 +222,11 @@ def render_install_report(payload: dict[str, object]) -> str:
targets = _as_string_list(payload.get("selected_targets"))
blocked_codes = _as_string_list(payload.get("blocked_codes"))
status = str(payload.get("validation") or payload.get("status") or "unknown")
lane_label = _install_lane_label()

lines: list[str] = []
lines.append(
f"Status: mode={mode}; targets={_join_or_none(targets)}; status={status}; blockers={len(blocked_codes)}"
f"Status: mode={mode}; label={lane_label}; targets={_join_or_none(targets)}; status={status}; blockers={len(blocked_codes)}"
)
lines.append("")

Expand All @@ -227,6 +236,7 @@ def render_install_report(payload: dict[str, object]) -> str:
["Field", "Value"],
[
["Mode", mode],
["Lane label", lane_label],
["Selected targets", _join_or_none(targets)],
["Retired targets", _join_or_none(_as_string_list(payload.get("retired_targets")))],
["Source resources considered", str(payload.get("source_resources_considered") or 0)],
Expand Down Expand Up @@ -305,10 +315,11 @@ def render_bisync_report(payload: dict[str, object]) -> str:
status = "unknown"
if isinstance(verification, dict):
status = str(verification.get("status") or status)
lane_label = _bisync_lane_label()

lines: list[str] = []
lines.append(
f"Status: mode={mode}; lane=bisync; status={status}; drift_total={drift_total}; blockers={len(blocked_codes)}"
f"Status: mode={mode}; label={lane_label}; lane=bisync; status={status}; drift_total={drift_total}; blockers={len(blocked_codes)}"
)
lines.append("")

Expand All @@ -318,6 +329,7 @@ def render_bisync_report(payload: dict[str, object]) -> str:
["Field", "Value"],
[
["Mode", mode],
["Lane label", lane_label],
["Drift total", str(drift_total)],
["Blocked code count", str(len(blocked_codes))],
["Verification status", status],
Expand Down
2 changes: 2 additions & 0 deletions tests/test_home_ai_resources_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ def test_main_plan_report_output_has_fixed_sections(

assert exit_code == 0
assert "## Current State" in output
assert "repo-to-home install" in output
assert "## Differences Or Planned Work" in output
assert "## Actions Completed" in output
assert "## Blockers And Skips" in output
Expand Down Expand Up @@ -666,6 +667,7 @@ def test_bisync_plan_report_output_has_fixed_sections(

assert exit_code == 1
assert "## Current State" in output
assert "repo-home drift" in output
assert "## Differences Or Planned Work" in output
assert "## Actions Completed" in output
assert "## Blockers And Skips" in output
Expand Down