diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6e4cda6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true + +[*.py] +indent_style = space +indent_size = 4 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2df1088 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Change and affected skills + +Describe the user-visible change, sibling dependencies and compatibility impact. + +## Evidence + +Record commands, results and exact tested commit. Distinguish structural checks, runtime tests and visual review. Name checks not run and why. + +## Boundaries + +Confirm that activation keys, consent, private assets, approval gates and specialist entry points remain intact. Identify any intentional exception. No registry release, visibility change or live external action is implied. + +## Family consistency + +For shared changes, identify affected sibling PRs and the orchestrator drift-audit result. Do not claim coordinated merge until each PR is confirmed merged. diff --git a/.github/workflows/package-contract.yml b/.github/workflows/package-contract.yml new file mode 100644 index 0000000..c115ce6 --- /dev/null +++ b/.github/workflows/package-contract.yml @@ -0,0 +1,23 @@ +name: Package contract +on: [push, pull_request] +permissions: + contents: read +jobs: + validate-package: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Test the package contract + run: python -m unittest discover -s tests -p 'test_package_*.py' -v + - name: Validate the full checkout + run: python scripts/validate_package.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d95b554 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# CompleteTech LLC Skills: maintainer instructions + +Read [SKILL.md](SKILL.md) for specialist behavior, [ONBOARDING.md](ONBOARDING.md) for first use, [BRANDING.md](BRANDING.md) for presentation and handoffs, and [CONTRIBUTING.md](CONTRIBUTING.md) before changing this checkout. + +Preserve the activation key, existing CLI paths, templates, dependencies, approval gates and consent boundaries. A common package does not require identical generators. Keep private assets private and use synthetic fixtures. Never scan personal logs, submit certificate data, install schedules, send messages or publish artifacts merely to test alignment. + +Run `python -m unittest discover -s tests -p 'test_package_*.py' -v` and `python scripts/validate_package.py`, then the existing Quality checks. Do not disable failing checks or replace verified evidence with successful rendering. Describe any checks not run. + +Shared files are maintained together through the orchestrator's catalog and drift audit. Update affected siblings in focused PRs; review exact heads and checks before merging. Registry publishing and visibility changes are separate actions. Never claim a PR exists or merged without its successful GitHub response. diff --git a/BRANDING.md b/BRANDING.md new file mode 100644 index 0000000..97df1de --- /dev/null +++ b/BRANDING.md @@ -0,0 +1,30 @@ +# CompleteTech LLC Skills: branding and handoffs + +[Start here](ONBOARDING.md) · [Asset rights](BRAND_ASSETS.md) · [Specialist instructions](SKILL.md) + +## Resolve identity before rendering + +An approved engagement brand takes precedence over a starter example. Preserve the operator's explicit settings; ask about unresolved identity instead of silently applying company branding. The ledger remains neutral by default. CompleteTech examples identify this library, not permission to represent another business. + +Use the existing local `assets/logo.png` for authorized CompleteTech output. Do not synthesize replacement marks, fetch remote artwork, redistribute fonts, or transfer private seals/signatures to public repositories. The MIT code license does not replace the asset policy. + +## Starter palette + +These established library colors are a reference, not new runtime defaults: + +| Token | Value | +|---|---| +| Accent | `#1E3A8A` | +| Ink | `#0F172A` | +| Soft accent | `#EEF2FF` | +| Muted text | `#64748B` | +| Border | `#E2E8F0` | +| Alternating row | `#F8FAFC` | + +Keep specialist layouts: invoices, envelopes, certificates, proposals and dashboards have different constraints. Use existing renderer options, generator configuration or the ledger's explicit branding preset, as documented in this skill's README and `--help`. This document does not introduce a universal brand-import flag. + +## Handoff to the next skill + +Carry verified facts and their sources, artifact path/version, approved identity and local logo path, palette overrides, confidentiality, intended audience, approval owner/status, blockers and next specialist. Preserve these in the orchestrator's existing `project_state`; do not replace its workflow schema with an incompatible parallel state format. + +Missing approval stays unknown or draft. Rendering, anonymization, a clean package check or a reused brand does not authorize sending, publication, billing, signing, attendance claims or launch. Review the actual output for clipping, contrast, placeholders and factual accuracy before approved use. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..62909d3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing to CompleteTech LLC Skills + +[Start here](ONBOARDING.md) · [Branding and handoffs](BRANDING.md) · [Maintainer instructions](AGENTS.md) · [Specialist instructions](SKILL.md) + +## Compatibility + +Keep each skill independently usable. Preserve activation keys, root generator CLIs, templates, runtime dependencies, consent and approval gates. The ledger's standard-library core and neutral branding are intentional differences, not gaps to remove. + +## Verification + +From a full checkout using Python 3.12, the package CI baseline: + +```bash +python -m unittest discover -s tests -p 'test_package_*.py' -v +python scripts/validate_package.py +``` + +Also run the existing quality and regression commands in [README.md](README.md). Package checks are read-only and do not execute specialist generators. They validate metadata, required files, local documentation links and PNG chunk integrity, not decoded artwork or document truth. Do not weaken original Quality checks to obtain a green result. + +## Coordinated changes + +Keep `skill-package.json`, onboarding and actual entry points synchronized. Shared validator, tests, CI, maintainer instructions and branding guidance must match the orchestrator's shared-file audit. Use focused PRs and report exact checks, failures and limitations. Respect branch protection, review requirements and changed heads. + +Use synthetic fixtures and new output paths instead of overwriting committed previews. Never commit secrets, personal logs, real account mappings, client documents or unapproved quotes. Code remains under [LICENSE](LICENSE); branding remains under [BRAND_ASSETS.md](BRAND_ASSETS.md). Do not redistribute private assets or fonts. Registry releases and repository visibility changes require separate authorization. diff --git a/ONBOARDING.md b/ONBOARDING.md new file mode 100644 index 0000000..24e6c08 --- /dev/null +++ b/ONBOARDING.md @@ -0,0 +1,64 @@ +# Start here: agentic-proposal-skill + +

diff --git a/scripts/validate_package.py b/scripts/validate_package.py new file mode 100644 index 0000000..e7dbf4c --- /dev/null +++ b/scripts/validate_package.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Read-only, standard-library validation of a full CompleteTech skill checkout.""" +from __future__ import annotations +import argparse +import json +import re +import struct +import zlib +from pathlib import Path +from urllib.parse import unquote, urlsplit + +FAMILY = "completetech-skills" +FIELDS = set("schema_version family repository skill_name kind entrypoints example_inputs network_mode private".split()) +FILES = ("README.md", "SKILL.md", "LICENSE", "BRAND_ASSETS.md", "ONBOARDING.md", "CONTRIBUTING.md", "BRANDING.md", "AGENTS.md", "requirements.txt", "agents/openai.yaml") +NAVIGATION = ("CompleteTech LLC Skills", "[Start here](ONBOARDING.md)", "[Contributing](CONTRIBUTING.md)", "assets/logo.png") +KINDS = ("catalog-renderer", "config-generator", "orchestrator", "usage-ledger") +NETWORK_MODES = ("local", "operator-selected-hosts", "optional-receipts") + + +def unique_object(pairs: list[tuple[str, object]]) -> dict: + result: dict = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate JSON key: {key}") + result[key] = value + return result + + +def local_file(root: Path, value: object) -> Path: + if not isinstance(value, str) or not value or any(ord(c) < 32 for c in value): + raise ValueError(f"invalid package path: {value!r}") + if "\\" in value or ":" in value or value.startswith("/") or any(p in ("", ".", "..") for p in value.split("/")): + raise ValueError(f"unsafe package path: {value!r}") + path = (root / value).resolve() + if not path.is_relative_to(root.resolve()) or not path.is_file(): + raise ValueError(f"missing or escaping package file: {value}") + return path + + +def validate_png(path: Path) -> None: + """Check bounded chunks and CRCs; decoding and visual review remain separate.""" + with path.open("rb") as handle: + data = handle.read(16 * 1024 * 1024 + 1) + if len(data) > 16 * 1024 * 1024 or data[:8] != b"\x89PNG\r\n\x1a\n": + raise ValueError("invalid or oversized PNG") + offset, header, image = 8, False, False + while offset + 12 <= len(data): + length = struct.unpack_from(">I", data, offset)[0] + kind = data[offset + 4:offset + 8] + end = offset + length + 12 + if end > len(data): + raise ValueError("truncated PNG chunk") + payload = data[offset + 8:end - 4] + if zlib.crc32(kind + payload) != struct.unpack_from(">I", data, end - 4)[0]: + raise ValueError("PNG checksum mismatch") + if not header and kind != b"IHDR": + raise ValueError("PNG must start with IHDR") + if kind == b"IHDR": + if header or length != 13: + raise ValueError("invalid PNG header") + width, height, depth, color, compression, filtering, interlace = struct.unpack(">IIBBBBB", payload) + depths = {0: (1, 2, 4, 8, 16), 2: (8, 16), 3: (1, 2, 4, 8), 4: (8, 16), 6: (8, 16)} + if not 0 < width < 2**31 or not 0 < height < 2**31 or depth not in depths.get(color, ()) or compression or filtering or interlace not in (0, 1): + raise ValueError("invalid PNG parameters") + header = True + elif kind == b"IDAT": + image = image or bool(length) + elif kind == b"IEND": + if length or not image or end != len(data): + raise ValueError("invalid PNG end") + return + offset = end + raise ValueError("incomplete PNG") + + +def validate(root: Path) -> list[str]: + errors: list[str] = [] + try: + manifest = json.loads(local_file(root, "skill-package.json").read_text(encoding="utf-8"), object_pairs_hook=unique_object) + if not isinstance(manifest, dict) or set(manifest) != FIELDS: + raise ValueError("manifest fields must match schema 1") + name, repo = manifest["skill_name"], manifest["repository"] + if type(manifest["schema_version"]) is not int or manifest["schema_version"] != 1 or manifest["family"] != FAMILY: + errors.append("invalid schema_version or family") + if not isinstance(name, str) or not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) or len(name) > 64: + errors.append("invalid skill_name") + if not isinstance(repo, str) or repo not in (f"CompleteTech-LLC/{name}", f"CompleteTech-LLC/{name}-skill"): + errors.append("repository does not match skill_name") + if manifest["kind"] not in KINDS or manifest["network_mode"] not in NETWORK_MODES or type(manifest["private"]) is not bool: + errors.append("invalid kind, network_mode or private") + paths: list[object] = [*FILES, "assets/logo.png"] + if manifest["kind"] == "config-generator": + paths.append("config.ini") + for field in ("entrypoints", "example_inputs"): + values = manifest[field] + if not isinstance(values, list) or not values or not all(isinstance(v, str) for v in values): + errors.append(f"{field} must be a nonempty string array") + else: + if len(set(values)) != len(values): + errors.append(f"duplicate {field}") + paths.extend(values) + for value in paths: + try: + local_file(root, value) + except (OSError, ValueError, RuntimeError) as exc: + errors.append(str(exc)) + skill = local_file(root, "SKILL.md").read_text(encoding="utf-8") + header = re.match(r"\A---\n(.*?)\n---(?:\n|\Z)", skill, re.S) + names = re.findall(r'''(?m)^name:\s*['"]?([a-z0-9-]+)['"]?\s*$''', header.group(1)) if header else [] + if names != [name]: + errors.append("SKILL.md name must match manifest") + readme = local_file(root, "README.md").read_text(encoding="utf-8") + errors.extend(f"missing README navigation: {item}" for item in NAVIGATION if item not in readme) + validate_png(local_file(root, "assets/logo.png")) + for document in ("ONBOARDING.md", "CONTRIBUTING.md", "BRANDING.md", "AGENTS.md"): + text = local_file(root, document).read_text(encoding="utf-8") + for target in re.findall(r"\[[^\]]+\]\(([^)]+)\)", text): + parsed = urlsplit(target) + if target.startswith("#") or parsed.scheme in ("https", "http", "mailto"): + continue + if parsed.scheme or parsed.netloc: + raise ValueError(f"unsupported documentation link: {target}") + local_file(root, unquote(parsed.path)) + except (OSError, UnicodeError, ValueError, RuntimeError, RecursionError) as exc: + errors.append(str(exc)) + return errors + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + issues = validate(parser.parse_args().root) + print("\n".join(issues) if issues else "Package contract OK; no specialist commands executed") + raise SystemExit(bool(issues)) diff --git a/skill-package.json b/skill-package.json new file mode 100644 index 0000000..1359c29 --- /dev/null +++ b/skill-package.json @@ -0,0 +1,11 @@ +{ + "schema_version": 1, + "family": "completetech-skills", + "repository": "CompleteTech-LLC/agentic-proposal-skill", + "skill_name": "agentic-proposal-skill", + "kind": "catalog-renderer", + "entrypoints": ["scripts/render_proposal.py", "scripts/render_pdf.py"], + "example_inputs": ["assets/examples/example.md"], + "network_mode": "local", + "private": false +} diff --git a/tests/test_package_contract.py b/tests/test_package_contract.py new file mode 100644 index 0000000..41b1c54 --- /dev/null +++ b/tests/test_package_contract.py @@ -0,0 +1,121 @@ +"""Synthetic package regressions; no specialist code or personal data is used.""" +import importlib.util +import json +import struct +import tempfile +import unittest +import zlib +from pathlib import Path + +SPEC = importlib.util.spec_from_file_location("package", Path(__file__).resolve().parents[1] / "scripts/validate_package.py") +assert SPEC and SPEC.loader +package = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(package) + + +def png_fixture(): + def chunk(kind, data): + return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data)) + return b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(b"\0\0\0\0")) + chunk(b"IEND", b"") + + +class PackageContractTests(unittest.TestCase): + def setUp(self): + temp = tempfile.TemporaryDirectory() + self.addCleanup(temp.cleanup) + self.root = Path(temp.name) / "checkout" + self.manifest: dict[str, object] = dict(schema_version=1, family=package.FAMILY, repository="CompleteTech-LLC/demo-skill", skill_name="demo-skill", kind="catalog-renderer", entrypoints=["scripts/render.py"], example_inputs=["example.md"], network_mode="local", private=False) + for name in (*package.FILES, "scripts/render.py", "example.md"): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("demo\n", encoding="utf-8") + (self.root / "assets").mkdir() + (self.root / "assets/logo.png").write_bytes(png_fixture()) + (self.root / "SKILL.md").write_text("---\nname: demo-skill\n---\n", encoding="utf-8") + (self.root / "README.md").write_text("\n".join(package.NAVIGATION), encoding="utf-8") + self.save() + + def save(self): + (self.root / "skill-package.json").write_text(json.dumps(self.manifest), encoding="utf-8") + + def test_valid_and_no_execution(self): + (self.root / "scripts/render.py").write_text("raise RuntimeError('do not execute')", encoding="utf-8") + self.assertEqual(package.validate(self.root), []) + + def test_bad_values(self): + for field, value in (("schema_version", True), ("family", "other"), ("skill_name", "../escape"), ("repository", "other/demo"), ("kind", []), ("network_mode", {}), ("private", "false"), ("entrypoints", []), ("entrypoints", [None]), ("example_inputs", "example.md"), ("example_inputs", ["example.md", "example.md"])): + with self.subTest(field=field, value=value): + original = self.manifest[field] + self.manifest[field] = value + self.save() + self.assertTrue(package.validate(self.root)) + self.manifest[field] = original + + def test_json_and_missing_fields(self): + for text in ("{", "[]", "null", "{}", '{"private":true,"private":false}'): + (self.root / "skill-package.json").write_text(text, encoding="utf-8") + self.assertTrue(package.validate(self.root)) + + def test_paths_and_links(self): + for value in ("../escape", "/escape", "C:/escape", "a\\b", "./example.md", "a//b", "bad\npath", "missing.md"): + with self.subTest(path=value): + self.manifest["entrypoints"] = [value] + self.save() + self.assertTrue(package.validate(self.root)) + self.manifest["entrypoints"] = ["scripts/render.py"] + self.save() + for target in ("missing.md", "%2e%2e/outside", "//outside/file", "file:///etc/passwd", "file:example.md"): + (self.root / "ONBOARDING.md").write_text(f"[Bad]({target})", encoding="utf-8") + self.assertTrue(package.validate(self.root)) + + def test_escaping_symlink(self): + target = self.root / "example.md" + outside = self.root.parent / "outside.md" + outside.write_text("outside", encoding="utf-8") + target.unlink() + try: + target.symlink_to(outside) + except OSError as exc: + self.skipTest(str(exc)) + self.assertTrue(package.validate(self.root)) + + def test_png_corruption(self): + image = png_fixture() + for data in (b"not png", image[:8], image[:-12], image + b"junk", image[:-1] + bytes([image[-1] ^ 1])): + (self.root / "assets/logo.png").write_bytes(data) + self.assertTrue(package.validate(self.root)) + + def test_missing_required_files(self): + for name in (*package.FILES, "assets/logo.png"): + with self.subTest(path=name): + path = self.root / name + data = path.read_bytes() + path.unlink() + self.assertTrue(package.validate(self.root)) + path.write_bytes(data) + + def test_config_generator_requires_base(self): + self.manifest["kind"] = "config-generator" + self.save() + self.assertTrue(package.validate(self.root)) + (self.root / "config.ini").write_text("[demo]\n", encoding="utf-8") + self.assertEqual(package.validate(self.root), []) + + def test_names_and_navigation(self): + (self.root / "SKILL.md").write_text("---\nname: wrong\n---\n", encoding="utf-8") + self.assertTrue(package.validate(self.root)) + (self.root / "SKILL.md").write_bytes(b'---\r\nname: "demo-skill"\r\n---\r\n') + self.assertEqual(package.validate(self.root), []) + (self.root / "README.md").write_text("no navigation", encoding="utf-8") + self.assertTrue(package.validate(self.root)) + + def test_ledger_install_name_and_links(self): + self.manifest.update(skill_name="ai-usage-ledger", repository="CompleteTech-LLC/ai-usage-ledger-skill", kind="usage-ledger", network_mode="operator-selected-hosts") + self.save() + (self.root / "SKILL.md").write_text("---\nname: ai-usage-ledger\n---\n", encoding="utf-8") + (self.root / "ONBOARDING.md").write_text("[Read](README.md#demo) [Web](https://example.com) [Here](#here)", encoding="utf-8") + self.assertEqual(package.validate(self.root), []) + + +if __name__ == "__main__": + unittest.main()