diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a1f7748 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +* @legrab +/.github/CODEOWNERS @legrab \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c8ca58f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + structural: + name: Structural validation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install validation dependency + run: python -m pip install --disable-pip-version-check pyyaml + - name: Validate repository structure + run: python scripts/ci-validate.py + - name: Validate POSIX installer syntax + run: sh -n scripts/install.sh + - name: Run installer smoke test + run: bash scripts/ci-install-test.sh + + powershell: + name: PowerShell installer smoke test + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Validate PowerShell syntax + shell: pwsh + run: | + $errors = $null + [void][System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path "scripts/install.ps1"), [ref]$null, [ref]$errors) + if ($errors.Count -gt 0) { $errors | Format-List; exit 1 } + - name: Run installer smoke test + shell: pwsh + run: | + $root = Join-Path $env:RUNNER_TEMP "learning-flow-ci" + New-Item -ItemType Directory -Force -Path $root | Out-Null + & "$PWD/scripts/install.ps1" ` + -Target $root ` + -Repository $env:GITHUB_REPOSITORY ` + -Ref $env:GITHUB_SHA ` + -Profile minimal ` + -Mode fail ` + -SkipRootAgents + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if (-not (Test-Path "$root/learning-flow")) { + throw "Installer did not create the learning-flow component." + } diff --git a/MANIFEST.txt b/MANIFEST.txt index adbeeec..a4618dc 100644 --- a/MANIFEST.txt +++ b/MANIFEST.txt @@ -1,10 +1,10 @@ .gitattributes 40 .gitignore 274 -AGENTS.md 3010 -CHANGELOG.md 10259 +AGENTS.md 3172 +CHANGELOG.md 11414 docs/DESIGN_NOTES.md 17684 docs/INITIALIZE_LEARNING_FLOW.md 9396 -docs/README.md 1185 +docs/README.md 2325 docs/references/REFERENCE_INTEGRATION.md 4551 docs/references/REFERENCE_REVIEW_AWESOME_AGENT_SKILLS.md 2317 docs/references/REFERENCE_REVIEW_BEST_OF_AGENT_HARNESSES.md 2540 @@ -12,63 +12,63 @@ docs/references/REFERENCE_REVIEW_GOOSE.md 2486 docs/references/REFERENCE_REVIEW_LITT.md 2069 docs/references/REFERENCE_REVIEW_POCOK.md 2155 LICENSE 2213 -README.md 8266 +README.md 8940 sample/common/.agents/skills/agentic-workflow/SKILL.md 3653 sample/common/.agents/skills/learn-anything/agents/openai.yaml 246 -sample/common/.agents/skills/learn-anything/SKILL.md 3606 -sample/common/agentic-flow/.managed-files 157 +sample/common/.agents/skills/learn-anything/SKILL.md 3225 +sample/common/agentic-flow/.managed-files 170 sample/common/agentic-flow/.managed-skills 32 sample/common/agentic-flow/.template-version 6 -sample/common/agentic-flow/AGENTS.md 5205 +sample/common/agentic-flow/AGENTS.md 5253 sample/common/agentic-flow/CONFIGURE.md 4051 sample/common/agentic-flow/LEARN.md 1822 -sample/common/agentic-flow/LOCAL.md 3157 -sample/common/agentic-flow/README.md 1237 +sample/common/agentic-flow/LOCAL.md 3513 +sample/common/agentic-flow/README.md 1624 sample/common/agentic-flow/REFERENCE_INTEGRATION.md 2183 sample/common/agentic-flow/ROOT_INTEGRATION.md 2932 sample/common/agentic-flow/SETTINGS.md 544 sample/common/agentic-flow/WORKFLOW.md 3309 -sample/common/local/learning-history.md 625 -sample/profiles/full/.agents/skills/analogous-feature/SKILL.md 1389 -sample/profiles/full/.agents/skills/challenge-debugging/SKILL.md 1587 +sample/common/local/learning-history.md 986 +sample/profiles/full/.agents/skills/analogous-feature/SKILL.md 1408 +sample/profiles/full/.agents/skills/challenge-debugging/SKILL.md 1426 sample/profiles/full/.agents/skills/challenge-debugging/templates/challenge.md 428 -sample/profiles/full/.agents/skills/change-explainer/SKILL.md 1482 +sample/profiles/full/.agents/skills/change-explainer/SKILL.md 1308 sample/profiles/full/.agents/skills/change-explainer/templates/change-explainer.md 290 -sample/profiles/full/.agents/skills/learning-bootstrap/SKILL.md 1875 -sample/profiles/full/.agents/skills/repository-orientation/SKILL.md 1547 -sample/profiles/full/.agents/skills/safe-refactor/SKILL.md 1392 -sample/profiles/full/.agents/skills/ticket-learning-path/SKILL.md 1633 +sample/profiles/full/.agents/skills/learning-bootstrap/SKILL.md 1522 +sample/profiles/full/.agents/skills/repository-orientation/SKILL.md 1382 +sample/profiles/full/.agents/skills/safe-refactor/SKILL.md 1373 +sample/profiles/full/.agents/skills/ticket-learning-path/SKILL.md 1519 sample/profiles/full/.agents/skills/ticket-learning-path/templates/ticket-learning-path.md 348 sample/profiles/full/learning-flow/.managed-files 137 sample/profiles/full/learning-flow/.managed-skills 132 sample/profiles/full/learning-flow/.template-profile 5 sample/profiles/full/learning-flow/.template-version 6 -sample/profiles/full/learning-flow/AGENTS.md 5302 -sample/profiles/full/learning-flow/BOOTSTRAP.md 2097 -sample/profiles/full/learning-flow/MAP.md 945 -sample/profiles/full/learning-flow/PLAYBOOKS.md 1157 -sample/profiles/full/learning-flow/README.md 2991 +sample/profiles/full/learning-flow/AGENTS.md 4274 +sample/profiles/full/learning-flow/BOOTSTRAP.md 1788 +sample/profiles/full/learning-flow/MAP.md 1464 +sample/profiles/full/learning-flow/PLAYBOOKS.md 1327 +sample/profiles/full/learning-flow/README.md 2955 sample/profiles/full/learning-flow/REPOSITORIES.md 439 -sample/profiles/full/learning-flow/TAKEAWAYS.md 570 -sample/profiles/full/learning-flow/UNDERSTANDING_CHECKS.md 1627 -sample/profiles/minimal/.agents/skills/repository-learning/SKILL.md 3258 +sample/profiles/full/learning-flow/TAKEAWAYS.md 970 +sample/profiles/full/learning-flow/UNDERSTANDING_CHECKS.md 2010 +sample/profiles/minimal/.agents/skills/repository-learning/SKILL.md 3574 sample/profiles/minimal/learning-flow/.managed-files 87 sample/profiles/minimal/learning-flow/.managed-skills 20 sample/profiles/minimal/learning-flow/.template-profile 8 sample/profiles/minimal/learning-flow/.template-version 6 -sample/profiles/minimal/learning-flow/AGENTS.md 3762 -sample/profiles/minimal/learning-flow/MAP.md 811 -sample/profiles/minimal/learning-flow/README.md 1648 -sample/profiles/minimal/learning-flow/TAKEAWAYS.md 551 -sample/README.md 1600 +sample/profiles/minimal/learning-flow/AGENTS.md 2905 +sample/profiles/minimal/learning-flow/MAP.md 1229 +sample/profiles/minimal/learning-flow/README.md 1974 +sample/profiles/minimal/learning-flow/TAKEAWAYS.md 973 +sample/README.md 2113 sample/root/AGENTS.md 1339 sample/root/AGENTS.pointer.md 296 -scripts/install.bat 1434 +scripts/install.bat 1477 scripts/install.ps1 29480 scripts/install.sh 24986 -scripts/README.md 2660 -skill-evals/agentic-cases.yaml 6691 -skill-evals/conversation-cases.yaml 2633 -skill-evals/full-cases.yaml 2977 -skill-evals/minimal-cases.yaml 2377 -skill-evals/README.md 1002 +scripts/README.md 3579 +skill-evals/agentic-cases.yaml 7109 +skill-evals/conversation-cases.yaml 3653 +skill-evals/full-cases.yaml 4248 +skill-evals/minimal-cases.yaml 3208 +skill-evals/README.md 1110 diff --git a/scripts/check_manifest.py b/scripts/check_manifest.py new file mode 100644 index 0000000..cde5619 --- /dev/null +++ b/scripts/check_manifest.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +from pathlib import Path, PurePosixPath +import sys + + +ROOT = Path(__file__).resolve().parents[1] + + +def fail(message): + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def check_manifest(update=False): + manifest = ROOT / "MANIFEST.txt" + + if not manifest.is_file(): + fail("MANIFEST.txt is missing") + + lines = manifest.read_text(encoding="utf-8").splitlines() + declared = {} + updated_lines = [] + changed = False + + for line in lines: + if not line.strip(): + updated_lines.append(line) + continue + + parts = line.rsplit("\t", 1) + if len(parts) != 2: + fail(f"Malformed manifest entry: {line!r}") + + path, size = parts + pp = PurePosixPath(path) + + if not path or pp.is_absolute() or ".." in pp.parts: + fail(f"Unsafe manifest path: {path!r}") + + try: + size = int(size) + except ValueError: + fail(f"Invalid manifest size: {line!r}") + + if path in declared: + fail(f"Duplicate manifest entry: {path!r}") + + declared[path] = size + + target = ROOT.joinpath(*pp.parts) + + if not target.is_file(): + fail(f"Manifest references missing file: {path}") + + actual = target.stat().st_size + + if actual != size: + if update: + print( + f"Updating manifest size for {path}: " + f"{size} -> {actual}" + ) + updated_lines.append(f"{path}\t{actual}") + changed = True + else: + fail( + f"Manifest size differs for {path}: " + f"expected {size}, actual {actual}" + ) + else: + updated_lines.append(line) + + if update and changed: + manifest.write_text( + "\n".join(updated_lines) + "\n", + encoding="utf-8", + ) \ No newline at end of file diff --git a/scripts/ci-install-test.sh b/scripts/ci-install-test.sh new file mode 100644 index 0000000..4af0a7c --- /dev/null +++ b/scripts/ci-install-test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +target="$(mktemp -d)" +trap 'rm -rf "$target"' EXIT +repository="${GITHUB_REPOSITORY:-legrab/codebase-learning-flow}" +ref="${GITHUB_SHA:-main}" +bash "$repo_root/scripts/install.sh" --target "$target" --repository "$repository" --ref "$ref" --profile minimal --mode fail --skip-root-agents +test -d "$target/learning-flow" +mkdir -p "$target/.local" +printf '%s\n' "CI sentinel" > "$target/.local/ci-sentinel" +bash "$repo_root/scripts/install.sh" --target "$target" --repository "$repository" --ref "$ref" --profile minimal --mode update --skip-root-agents +grep -Fxq "CI sentinel" "$target/.local/ci-sentinel" +echo "Installer smoke test passed." diff --git a/scripts/ci-validate.py b/scripts/ci-validate.py new file mode 100644 index 0000000..be8bbdc --- /dev/null +++ b/scripts/ci-validate.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Structural repository checks. + +The package MANIFEST is a package manifest, not a repository inventory: +declared entries must exist and have the recorded size, while repository +infrastructure files may exist outside the package. +""" + +from pathlib import Path, PurePosixPath +import re +import sys +import zipfile + +from check_manifest import check_manifest + + +ROOT = Path(__file__).resolve().parents[1] + + +def fail(message): + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def repo_files(): + return [ + p + for p in ROOT.rglob("*") + if p.is_file() and ".git" not in p.parts + ] + + +def check_yaml(): + import yaml + + for path in repo_files(): + if path.suffix.lower() in {".yml", ".yaml"}: + try: + yaml.safe_load(path.read_text(encoding="utf-8")) + except Exception as exc: + fail( + f"Invalid YAML in " + f"{path.relative_to(ROOT)}: {exc}" + ) + + +def check_skill_structure(): + for path in repo_files(): + if ( + path.name != "SKILL.md" + or ".agents" not in path.parts + or "skills" not in path.parts + ): + continue + + text = path.read_text(encoding="utf-8") + match = re.match( + r"\A---\s*\n(.*?)\n---\s*", + text, + re.DOTALL, + ) + + if not match or not re.search( + r"^name:\s*\S+", + match.group(1), + re.MULTILINE, + ): + fail( + f"Invalid skill frontmatter: " + f"{path.relative_to(ROOT)}" + ) + + +def check_managed(): + for path in repo_files(): + if path.name == ".managed-files": + for line in path.read_text(encoding="utf-8").splitlines(): + entry = line.strip() + + if ( + entry + and not entry.startswith("#") + and not (path.parent / entry).is_file() + ): + fail( + f"{path.relative_to(ROOT)} " + f"references missing file {entry!r}" + ) + + elif path.name == ".managed-skills": + for line in path.read_text(encoding="utf-8").splitlines(): + name = line.strip() + + if ( + name + and not name.startswith("#") + and not re.fullmatch( + r"[A-Za-z0-9.\_-]+", + name, + ) + ): + fail( + f"Unsafe skill name in " + f"{path.relative_to(ROOT)}: {name!r}" + ) + + +def check_markdown_links(): + pattern = re.compile(r"\[[^\]]+\]\(([^)]+)\)") + root = ROOT.resolve() + + for path in repo_files(): + if path.suffix.lower() != ".md": + continue + + for target in pattern.findall( + path.read_text(encoding="utf-8") + ): + if not target or target.startswith( + ("#", "http://", "https://", "mailto:") + ): + continue + + target = target.split("#", 1)[0] + candidate = (path.parent / target).resolve() + + try: + candidate.relative_to(root) + except ValueError: + fail( + f"Link escapes repository: " + f"{path.relative_to(ROOT)} -> {target}" + ) + + if not candidate.exists(): + fail( + f"Broken Markdown link: " + f"{path.relative_to(ROOT)} -> {target}" + ) + + +def check_zip_paths(): + for path in repo_files(): + if path.suffix.lower() != ".zip": + continue + + try: + with zipfile.ZipFile(path) as archive: + for name in archive.namelist(): + member = PurePosixPath(name) + + if member.is_absolute() or ".." in member.parts: + fail( + f"Unsafe ZIP path: " + f"{path.relative_to(ROOT)} -> {name}" + ) + except zipfile.BadZipFile as exc: + fail( + f"Invalid ZIP archive " + f"{path.relative_to(ROOT)}: {exc}" + ) + + +for check in ( + check_manifest, + check_yaml, + check_skill_structure, + check_managed, + check_markdown_links, + check_zip_paths, +): + check() + +print("Structural validation passed.") \ No newline at end of file diff --git a/scripts/manifest-update.py b/scripts/manifest-update.py new file mode 100644 index 0000000..71406fb --- /dev/null +++ b/scripts/manifest-update.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +from check_manifest import check_manifest + + +check_manifest(update=True) \ No newline at end of file