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
14 changes: 3 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,19 @@ jobs:
os: [ubuntu-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v9.0.0
with:
python-version: "3.13"
enable-cache: false

- name: Run full tests
if: runner.os == 'Linux'
run: >-
uvx --python 3.13 --from pytest==9.1.1
--with pyyaml==6.0.3 --with sqlite-vec==0.1.9
--with fastembed==0.8.0 pytest -q

- name: Run macOS portable tests
if: runner.os == 'macOS'
run: >-
uvx --python 3.13 --from pytest==9.1.1
--with pyyaml==6.0.3 --with sqlite-vec==0.1.9
--with fastembed==0.8.0
pytest -q -k "not test_production_index_rebuild_keeps_proposed_notes_in_vector_discovery"

- name: Validate skill packages
run: bin/work-bundle-skill validate
27 changes: 27 additions & 0 deletions scripts/keep-summarizing/indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,33 @@ def install_sqlite_vec() -> tuple[object | None, str | None]:
return None, f"sqlite-vec unavailable in the uv-managed environment: {exc}"


def sqlite_vec_availability_probe() -> dict[str, object]:
"""Probe import and extension loading independently from a production rebuild."""
try:
sqlite_vec = __import__(SQLITE_VEC_IMPORT)
except ImportError:
return {
"status": "unavailable",
"reason": "sqlite-vec probe unavailable: import failed",
}
conn = sqlite3.connect(":memory:")
try:
conn.enable_load_extension(True)
sqlite_vec.load(conn)
return {"status": "available", "reason": None}
except Exception:
return {
"status": "unavailable",
"reason": "sqlite-vec probe unavailable: temporary load failed",
}
finally:
try:
conn.enable_load_extension(False)
except Exception:
pass
conn.close()


def load_sqlite_vec(conn: sqlite3.Connection) -> tuple[object | None, str | None]:
sqlite_vec, error = install_sqlite_vec()
if sqlite_vec is None:
Expand Down
72 changes: 59 additions & 13 deletions scripts/orchestration/repository_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,20 @@ def _v4_metadata_repository_entries(root: Path, text: str) -> list[dict[str, obj
resolved: list[dict[str, object]] = []
for entry in entries:
repository_id = str(entry.get("id") or "")
binding = local.get(repository_id)
if not binding or not binding.get("project_root"):
continue
binding = local.get(repository_id) or {}
entry.update(binding)
entry["project_root"] = binding["project_root"]
entry["path"] = binding["project_root"]
entry["git_repository"] = True
entry["expected_branch"] = entry.get("default_branch") or binding.get("observed_branch") or ""
# A device observation is current evidence, never the expected task baseline.
entry["observed_head"] = ""
entry["expected_branch"] = entry.get("default_branch") or ""
entry["observed_branch"] = binding.get("observed_branch") or ""
entry["observed_head"] = binding.get("observed_head") or ""
project_root = str(binding.get("project_root") or "")
entry["observation_project_root_status"] = "present" if project_root else "missing"
entry["baseline_status"] = "local-observation"
project_path = Path(str(binding["project_root"])).expanduser().resolve()
codegraph_present = (project_path / ".codegraph").is_dir()
if project_root:
entry["project_root"] = project_root
entry["path"] = project_root
project_path = Path(project_root).expanduser().resolve() if project_root else None
codegraph_present = bool(project_path and (project_path / ".codegraph").is_dir())
entry["codegraph"] = {
"supported": codegraph_present,
"index_present": codegraph_present,
Expand Down Expand Up @@ -259,6 +260,15 @@ def _metadata_repositories(root: Path) -> list[Path]:
return repositories


def _v4_metadata_targets(root: Path) -> list[dict[str, object]]:
targets: list[dict[str, object]] = []
for entry in _metadata_repository_entries(root):
raw = entry.get("project_root") or entry.get("path")
path = Path(str(raw)).expanduser().resolve() if raw else root.resolve()
targets.append({"path": str(path), "source": "project-metadata", "metadata": entry})
return targets


def _enrich_with_metadata(root: Path, targets: list[dict[str, str]]) -> list[dict[str, object]]:
entries: dict[str, dict[str, object]] = {}
for entry in _metadata_repository_entries(root):
Expand Down Expand Up @@ -337,6 +347,11 @@ def resolve_target_repositories(
resolved = _resolve_candidates(root, references)
if resolved:
return _enrich_with_metadata(root, resolved)
metadata_path = root / ".work-bundle" / "project.yaml"
if metadata_path.is_file() and _metadata_scalar(
metadata_path.read_text(encoding="utf-8"), "metadata_version"
) == "4":
return _v4_metadata_targets(root)
return _enrich_with_metadata(
root,
_resolve_candidates(root, ((path, "project-metadata") for path in _metadata_repositories(root))),
Expand Down Expand Up @@ -478,6 +493,23 @@ def inspect_repository_state(
"baseline": baseline,
"changes": [],
}
if metadata and metadata.get("observation_project_root_status") == "missing":
result["status"] = "missing-observation"
result["failure_code"] = "WB_REPOSITORY_OBSERVATION_PROJECT_ROOT_MISSING"
result["metadata"] = {
"repository_id": metadata.get("id"),
"expected_branch": metadata.get("expected_branch") or None,
"actual_branch": None,
"branch_status": "not-observed",
"expected_commit": metadata.get("observed_head") or None,
"actual_commit": None,
"commit_status": "not-observed",
"observation_branch_status": "missing",
"observation_head_status": "missing",
"baseline_status": metadata.get("baseline_status"),
"codegraph": metadata.get("codegraph") or {},
}
return result
if not path.exists():
result["status"] = "inaccessible"
return result
Expand Down Expand Up @@ -508,13 +540,13 @@ def inspect_repository_state(
actual_head = _run_git(path, "rev-parse", "HEAD").stdout.strip()
expected_branch = str(metadata.get("expected_branch") or metadata.get("working_branch") or "")
expected_head = str(metadata.get("observed_head") or metadata.get("last_commit_id") or "")
observed_branch = str(metadata.get("observed_branch") or "")
device_observation = "observation_project_root_status" in metadata
branch_status = "not-applicable"
commit_status = "not-applicable"
if bool(metadata.get("git_repository")):
branch_status = "matched" if expected_branch == actual_branch else "mismatch"
if str(metadata.get("baseline_status") or "") == "local-observation":
commit_status = "current-observation"
elif expected_head:
if expected_head:
commit_status = "matched" if expected_head == actual_head else "stale"
elif str(metadata.get("baseline_status") or "") == "unborn":
commit_status = "unborn"
Expand All @@ -531,6 +563,12 @@ def inspect_repository_state(
"actual_commit": actual_head or None,
"commit_status": commit_status,
"baseline_status": metadata.get("baseline_status"),
"observation_branch_status": (
"matched" if observed_branch == actual_branch else "stale"
) if device_observation and observed_branch else ("missing" if device_observation else "not-applicable"),
"observation_head_status": (
"matched" if expected_head == actual_head else "stale"
) if device_observation and expected_head else ("missing" if device_observation else "not-applicable"),
"codegraph": {
"supported": bool(codegraph_metadata.get("supported")),
"index_present": bool(codegraph_metadata.get("index_present")),
Expand All @@ -543,6 +581,14 @@ def inspect_repository_state(
if branch_status == "mismatch":
result["status"] = "branch-mismatch"
return result
if device_observation and (not observed_branch or not expected_head):
result["status"] = "missing-observation"
result["failure_code"] = "WB_REPOSITORY_OBSERVATION_MISSING"
return result
if device_observation and (observed_branch != actual_branch or expected_head != actual_head):
result["status"] = "stale-observation"
result["failure_code"] = "WB_REPOSITORY_OBSERVATION_STALE"
return result
if commit_status in {"stale", "missing"}:
result["status"] = "stale-baseline"
return result
Expand Down
11 changes: 11 additions & 0 deletions scripts/work-bundle/control_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ def _portable_failures(text: str) -> list[str]:
seen_member_names: set[str] = set()
seen_member_paths: set[str] = set()
root_bindings = 0
composite_member_bindings = 0
for repository in repositories:
repository_id = str(repository.get("id") or "")
if not repository.get("id"):
Expand Down Expand Up @@ -884,21 +885,29 @@ def _portable_failures(text: str) -> list[str]:
failures.append(f"WB_CONTROL_PLANE_MEMBER_BINDING_DUPLICATE:{member_name}")
seen_member_names.add(member_name)
elif mode == "composite":
valid_composite_member = True
if not member_name:
failures.append(f"WB_CONTROL_PLANE_MEMBER_BINDING_INVALID:{repository_id}")
valid_composite_member = False
elif member_name in seen_member_names:
failures.append(f"WB_CONTROL_PLANE_MEMBER_BINDING_DUPLICATE:{member_name}")
valid_composite_member = False
seen_member_names.add(member_name)
if not member_path:
failures.append(f"WB_CONTROL_PLANE_MEMBER_BINDING_INVALID:{repository_id}")
valid_composite_member = False
else:
try:
_validate_member_path(member_path)
except ControlPlaneError as exc:
failures.append(f"{exc.code}:{repository_id}")
valid_composite_member = False
if member_path in seen_member_paths:
failures.append(f"WB_CONTROL_PLANE_MEMBER_PATH_DUPLICATE:{member_path}")
valid_composite_member = False
seen_member_paths.add(member_path)
if valid_composite_member:
composite_member_bindings += 1
else:
failures.append(f"WB_CONTROL_PLANE_MEMBER_BINDING_INVALID:{repository_id}")
if member_name:
Expand All @@ -909,6 +918,8 @@ def _portable_failures(text: str) -> list[str]:
failures.append("WB_CONTROL_PLANE_SINGLE_REPOSITORY_BINDING_INVALID")
if mode == "composite" and root_bindings != 1:
failures.append("WB_CONTROL_PLANE_COMPOSITE_ROOT_BINDING_INVALID")
if mode == "composite" and composite_member_bindings == 0:
failures.append("WB_CONTROL_PLANE_COMPOSITE_MEMBER_REQUIRED")
return failures


Expand Down
49 changes: 48 additions & 1 deletion scripts/work-bundle/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@


class MigrationError(Exception):
def __init__(self, code: str, transaction_record: Path | None = None) -> None:
def __init__(
self,
code: str,
transaction_record: Path | None = None,
*,
result: dict[str, object] | None = None,
) -> None:
self.code = code
self.transaction_record = transaction_record
self.result = result or {}
super().__init__(code)


Expand All @@ -57,6 +64,43 @@ def _run_git(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
)


def validate_migration_proposal(origin: Path, branch: str, base_ref: str) -> dict[str, object]:
"""Validate origin-local worktree and ref availability without writing Git state."""
origin = origin.resolve()
common_dir = _run_git(origin, 'rev-parse', '--path-format=absolute', '--git-common-dir')
if common_dir.returncode:
raise MigrationError('WB_MIGRATION_ORIGIN_GIT_UNAVAILABLE')
evidence: dict[str, object] = {
'working_branch': branch,
'base_ref': base_ref,
'origin_git_common_dir': str(Path(common_dir.stdout.strip()).resolve()),
'changed_files': [],
'git_actions': [],
}
worktrees = _run_git(origin, 'worktree', 'list', '--porcelain')
if worktrees.returncode:
raise MigrationError('WB_MIGRATION_ORIGIN_GIT_UNAVAILABLE', result=evidence)
if f'branch refs/heads/{branch}' in worktrees.stdout.splitlines():
raise MigrationError('WB_WORKTREE_BRANCH_CONFLICT', result=evidence)

resolved = _run_git(origin, 'rev-parse', '--verify', '--quiet', f'{base_ref}^{{commit}}')
if resolved.returncode:
local_branch_available = False
if base_ref.startswith('origin/') and len(base_ref) > len('origin/'):
local_name = base_ref.removeprefix('origin/')
local = _run_git(origin, 'show-ref', '--verify', '--quiet', f'refs/heads/{local_name}')
local_branch_available = local.returncode == 0
evidence['local_branch_available'] = local_branch_available
code = (
'WB_MIGRATION_LOCAL_ORIGIN_BASE_REF_UNAVAILABLE'
if local_branch_available
else 'WB_MIGRATION_BASE_REF_UNAVAILABLE'
)
raise MigrationError(code, result=evidence)
evidence['resolved_base_commit'] = resolved.stdout.strip()
return evidence


def source_git_state(root: Path) -> dict[str, object]:
"""Return bounded Git facts without modifying the repository."""
resolved = root.resolve()
Expand Down Expand Up @@ -201,7 +245,9 @@ def propose_migration(
repository_name: str | None = None,
additional_repository_origins: list[dict[str, object]] | None = None,
) -> dict[str, object]:
origin = (origin or source).resolve()
inspection = inspect_migration(source, target, origin)
proposal_validation = validate_migration_proposal(origin, branch, base_ref)
slug = workspace_slug or target.name
name = repository_name or repository_id
return {
Expand All @@ -215,6 +261,7 @@ def propose_migration(
'dry_run': True,
'changed_files': [],
'git_actions': [],
'proposal_validation': proposal_validation,
'credential_action': 'create-empty-protected-store',
'apply_requires_accepted_baseline': bool(
inspection['source_repository_git']['dirty']
Expand Down
11 changes: 10 additions & 1 deletion scripts/work-bundle/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2547,7 +2547,16 @@ def cmd_migrate_to_multi_repository(args: list[str]) -> int:
additional_repository_origins=additional_origins,
)
except (MigrationError, ValueError, RuntimeError) as exc:
out({'command':'migrate-to-multi-repository','status':'issues-found','failure_code':str(exc)})
payload = {
'command': 'migrate-to-multi-repository',
'status': 'issues-found',
'failure_code': exc.code if isinstance(exc, MigrationError) else str(exc),
}
if isinstance(exc, MigrationError) and exc.result:
payload['result'] = exc.result
payload['changed_files'] = exc.result.get('changed_files', [])
payload['git_actions'] = exc.result.get('git_actions', [])
out(payload)
return 1
out({'command':'migrate-to-multi-repository','status':'passed','result':result})
return 0
Expand Down
30 changes: 26 additions & 4 deletions tests/test_control_plane_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -1228,7 +1228,7 @@ def test_migration_rejects_tracked_protected_control_plane_paths(tmp_path: Path)
assert json.loads(blocked.stdout)["failure_code"] == "WB_CONTROL_PLANE_PROTECTED_PATH_TRACKED"


def test_orchestration_uses_current_local_head_as_observation_not_expected_baseline(tmp_path: Path) -> None:
def test_orchestration_blocks_when_current_local_head_outgrows_device_observation(tmp_path: Path) -> None:
config = config_root(tmp_path / "config-root")
workspace, remote, _ = make_v3_workspace(tmp_path / "fixture")
migrate(config, workspace)
Expand All @@ -1245,13 +1245,20 @@ def test_orchestration_uses_current_local_head_as_observation_not_expected_basel
"--apply",
)
assert attached.returncode == 0
registry = config / "registry/projects.yaml"
before = registry.read_text(encoding="utf-8")
(checkout / "later.txt").write_text("later\n", encoding="utf-8")
git(checkout, "add", "later.txt")
git(checkout, "commit", "-q", "-m", "later local commit")
preflight = run_orch(config, "repository-preflight", "--project-root", str(workspace))
row = json.loads(preflight.stdout)["repository_preflight"]["repositories"][0]
assert row["status"] == "clean"
assert row["metadata"]["expected_commit"] is None
payload = json.loads(preflight.stdout)["repository_preflight"]
row = payload["repositories"][0]
assert payload["status"] == "blocked"
assert row["status"] == "stale-observation"
assert row["failure_code"] == "WB_REPOSITORY_OBSERVATION_STALE"
assert row["metadata"]["commit_status"] == "stale"
assert row["metadata"]["observation_head_status"] == "stale"
assert registry.read_text(encoding="utf-8") == before


def test_attach_materializes_missing_repository_idempotently_without_portable_mutation(tmp_path: Path) -> None:
Expand Down Expand Up @@ -1724,6 +1731,21 @@ def test_v4_composite_schema_requires_exactly_one_named_root(self) -> None:
failures = json.loads(doctor.stdout)["portable"]["failures"]
self.assertIn("WB_CONTROL_PLANE_COMPOSITE_ROOT_BINDING_INVALID", failures)

def test_v4_composite_schema_requires_at_least_one_named_member(self) -> None:
config, workspace, _, _ = init_single_v4(self.tmp_path)
metadata = workspace / ".work-bundle/project.yaml"
metadata.write_text(
metadata.read_text(encoding="utf-8").replace(
" mode: single-repository", " mode: composite"
),
encoding="utf-8",
)

doctor = run_wb(config, "doctor-workspace", str(workspace))
failures = json.loads(doctor.stdout)["portable"]["failures"]

self.assertIn("WB_CONTROL_PLANE_COMPOSITE_MEMBER_REQUIRED", failures)

def test_v4_composite_schema_rejects_duplicate_member_paths(self) -> None:
config, workspace, _, _ = init_single_v4(self.tmp_path)
write_composite_metadata(workspace)
Expand Down
Loading