Files
boshu2__agentops/scripts/sync-gc-pack.py
Bo 247fcccb5f GC 3.3: define the one-loop factory role pack (#961)
## Outcome

Defines the GC 3.3 factory as one semantic intelligence loop with
deterministic delivery:

- Fable Mayor plus a bounded, nonbinding ambiguity adviser
- Sol-high for plan binding and fresh exact-subject validation
- Terra-high as the default writer and Opus 4.8 medium as explicit
overflow/repair
- Luna dormant and unroutable until later qualification
- no model-driven delivery-policy/refinery loop
- native GC sling/claim/scale-from-zero semantics retained
- exact provider/model/reasoning/fallback runtime attestation
- legacy composed program/refinery entry points and opposite-provider
lifecycle roles removed

Controller dispatch to the Fable adviser stays fail-closed until GC33-4
proves process/filesystem isolation. This does not claim that manual
operator sling to the named on-demand target is impossible.

## Evidence

Fresh Sol-high `verdict.v2`: PASS
(`bb31806fc5e0714363a9e539121ee7aa19231431fb04ddfee1fce8376b57d272`)
over subject manifest
`119b7a6db2380af47d87a24aced60d96858dd9e413a54dc640376e27767e8d1e` (52
paths, 18 deletions).

- focused acceptance: 124 tests PASS
- projection owner: 4 tests PASS
- bootstrap fixtures: 25 Bats tests PASS
- complete GC static contract: PASS
- factory/executor doctors: PASS
- generated projection check: PASS
- exact official GC v1.3.5 and Beads v1.1.0 provenance replay: PASS
- `git diff --check`: PASS

No city, model session, supervisor, tmux session, Dolt server, or canary
was started for this pre-canary bead.
2026-07-21 06:00:33 -04:00

265 lines
8.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Project selected generated AgentOps skills into the Gas City role pack."""
from __future__ import annotations
import argparse
from dataclasses import dataclass
import hashlib
import json
import os
from pathlib import Path
import shutil
import stat
import sys
import tempfile
SCHEMA_VERSION = "agentops.gc-skill-projection.v1"
MANIFEST_REL = Path("packs/agentops-executor/assets/generated-skill-manifest.json")
PROJECTIONS = (
(
"implement-codex",
Path("skills/implement"),
Path("packs/agentops-executor/agents/implementer/skills/implement"),
),
(
"implement-claude",
Path("skills/implement"),
Path("packs/agentops-executor/agents/implementer-claude/skills/implement"),
),
(
"using-gc",
Path("skills/using-gc"),
Path("packs/agentops-executor/skills/using-gc"),
),
(
"validate-codex",
Path("skills/validate"),
Path("packs/agentops-executor/agents/validator/skills/validate"),
),
)
# Removed projection roots stay explicit until the release that deletes their
# last tracked bytes. This prevents a regenerated manifest from making an old
# provider role invisible to --check.
RETIRED_DESTINATIONS = (
Path("packs/agentops-executor/agents/validator-claude"),
)
EXCLUDED_NAMES = {
".DS_Store",
".agentops-generated.json",
".agentops-manifest.json",
"prompt.md",
}
EXCLUDED_SUFFIXES = {".pyc", ".pyo"}
EXCLUDED_DIRS = {"__pycache__"}
class ProjectionError(RuntimeError):
pass
@dataclass(frozen=True)
class ProjectedFile:
relative: Path
payload: bytes
mode: int
@property
def sha256(self) -> str:
return hashlib.sha256(self.payload).hexdigest()
def is_source_bookkeeping(relative: Path) -> bool:
return (
any(part in EXCLUDED_DIRS for part in relative.parts)
or relative.name in EXCLUDED_NAMES
or relative.suffix in EXCLUDED_SUFFIXES
)
def read_tree(root: Path, *, exclude_bookkeeping: bool) -> dict[str, ProjectedFile]:
if not root.is_dir():
raise ProjectionError(f"missing projection directory: {root}")
files: dict[str, ProjectedFile] = {}
for path in sorted(root.rglob("*")):
relative = path.relative_to(root)
if exclude_bookkeeping and is_source_bookkeeping(relative):
continue
if path.is_symlink():
raise ProjectionError(f"unsupported symlink in projection: {path}")
if path.is_dir():
continue
if not path.is_file():
raise ProjectionError(f"unsupported projection entry: {path}")
key = relative.as_posix()
files[key] = ProjectedFile(
relative=relative,
payload=path.read_bytes(),
mode=stat.S_IMODE(path.stat().st_mode),
)
if not files:
raise ProjectionError(f"projection source has no runtime files: {root}")
return files
def manifest_bytes(
sources: dict[str, dict[str, ProjectedFile]],
) -> bytes:
rows: list[dict[str, str]] = []
projection_by_skill = {skill: (source, destination) for skill, source, destination in PROJECTIONS}
for skill, files in sources.items():
source_root, destination_root = projection_by_skill[skill]
for relative, projected in files.items():
source = (source_root / relative).as_posix()
destination = (destination_root / relative).as_posix()
rows.append(
{
"source": source,
"destination": destination,
"source_sha256": projected.sha256,
"sha256": projected.sha256,
"mode": f"{projected.mode:04o}",
}
)
rows.sort(key=lambda row: row["destination"])
manifest = {
"schema_version": SCHEMA_VERSION,
"source": "skills",
"generator": "scripts/sync-gc-pack.py",
"files": rows,
}
return (json.dumps(manifest, indent=2, sort_keys=False) + "\n").encode("utf-8")
def compare_tree(
skill: str,
expected: dict[str, ProjectedFile],
destination: Path,
) -> list[str]:
if not destination.is_dir():
return [f"{skill}: missing destination {destination}"]
try:
actual = read_tree(destination, exclude_bookkeeping=False)
except ProjectionError as exc:
return [f"{skill}: {exc}"]
errors: list[str] = []
expected_paths = set(expected)
actual_paths = set(actual)
for relative in sorted(expected_paths - actual_paths):
errors.append(f"{skill}: missing generated file {relative}")
for relative in sorted(actual_paths - expected_paths):
errors.append(f"{skill}: stale generated file {relative}")
for relative in sorted(expected_paths & actual_paths):
if actual[relative].payload != expected[relative].payload:
errors.append(f"{skill}: content drift {relative}")
if actual[relative].mode != expected[relative].mode:
errors.append(f"{skill}: mode drift {relative}")
return errors
def replace_tree(destination: Path, files: dict[str, ProjectedFile]) -> None:
destination.parent.mkdir(parents=True, exist_ok=True)
stage_root = Path(tempfile.mkdtemp(prefix=f".{destination.name}.gc-sync-", dir=destination.parent))
stage = stage_root / destination.name
try:
stage.mkdir()
for relative in sorted(files):
projected = files[relative]
target = stage / projected.relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(projected.payload)
target.chmod(projected.mode)
if destination.exists():
shutil.rmtree(destination)
os.replace(stage, destination)
finally:
shutil.rmtree(stage_root, ignore_errors=True)
def atomic_write(path: Path, payload: bytes) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
temporary = Path(temporary_name)
try:
with os.fdopen(fd, "wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
finally:
temporary.unlink(missing_ok=True)
def sync(repo_root: Path, *, check: bool) -> int:
repo_root = repo_root.resolve()
sources: dict[str, dict[str, ProjectedFile]] = {}
try:
for skill, source, _destination in PROJECTIONS:
sources[skill] = read_tree(repo_root / source, exclude_bookkeeping=True)
except (OSError, ProjectionError) as exc:
print(f"sync-gc-pack: {exc}", file=sys.stderr)
return 2
expected_manifest = manifest_bytes(sources)
if check:
errors: list[str] = []
for skill, _source, destination in PROJECTIONS:
errors.extend(compare_tree(skill, sources[skill], repo_root / destination))
manifest_path = repo_root / MANIFEST_REL
try:
actual_manifest = manifest_path.read_bytes()
except FileNotFoundError:
errors.append(f"manifest: missing {manifest_path}")
except OSError as exc:
errors.append(f"manifest: cannot read {manifest_path}: {exc}")
else:
if actual_manifest != expected_manifest:
errors.append(f"manifest: generated content drift {manifest_path}")
for destination in RETIRED_DESTINATIONS:
retired = repo_root / destination
if retired.exists():
errors.append(f"retired projection: stale destination {retired}")
if errors:
for error in errors:
print(f"sync-gc-pack: {error}", file=sys.stderr)
return 1
print("sync-gc-pack: GC skill projection is current")
return 0
try:
for skill, _source, destination in PROJECTIONS:
replace_tree(repo_root / destination, sources[skill])
for destination in RETIRED_DESTINATIONS:
retired = repo_root / destination
if retired.exists():
shutil.rmtree(retired)
atomic_write(repo_root / MANIFEST_REL, expected_manifest)
except OSError as exc:
print(f"sync-gc-pack: {exc}", file=sys.stderr)
return 2
print("sync-gc-pack: projected implement, using-gc, validate")
return 0
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="report drift without writing")
parser.add_argument(
"--repo-root",
type=Path,
default=Path(__file__).resolve().parents[1],
help=argparse.SUPPRESS,
)
return parser.parse_args()
def main() -> int:
args = parse_args()
return sync(args.repo_root, check=args.check)
if __name__ == "__main__":
raise SystemExit(main())