diff --git a/CHANGELOG.md b/CHANGELOG.md index 696878a..7b11e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes use Semantic Versioning and are recorded here. ## Unreleased +## 1.0.1 - 2026-08-21 + +- Relicensed the repository under the MIT License and updated release SBOM generation and verification to bind the standard SPDX `MIT` identifier. +- Redesigned the README around the two-skill workflow, quick start, compatibility, security model, certification evidence, and clear release-license scope. - Published repository with `main` as default branch and preserved `v1.0.0` tag on exact certified source. - Fixed GitHub Actions pip-cache discovery for `requirements-dev.txt` and normalized Windows short/long temporary-path test aliases. - Updated artifact upload to pinned `actions/upload-artifact` v7.0.1 for native Node.js 24 execution. diff --git a/INSTALL.md b/INSTALL.md index 8a2e777..0173dcf 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -4,7 +4,7 @@ Teamwork requires Python 3.11 or newer and Git 2.22 or newer for the certified u ## Verify and extract a release -Download v1.0.0 ZIP and checksum from the [official GitHub Release](https://github.com/RandyNorthrup/teamwork-skill/releases/tag/v1.0.0). Confirm filenames are `teamwork-skills-1.0.0.zip` and `teamwork-skills-1.0.0.zip.sha256`. +Download v1.0.1 ZIP and checksum from the [official GitHub Release](https://github.com/RandyNorthrup/teamwork-skill/releases/tag/v1.0.1). Confirm filenames are `teamwork-skills-1.0.1.zip` and `teamwork-skills-1.0.1.zip.sha256`. 1. Compare the downloaded ZIP SHA-256 with its `.zip.sha256` sidecar: use `Get-FileHash -Algorithm SHA256` on PowerShell, `sha256sum ` on Linux, or `shasum -a 256 ` on stock macOS. 2. Extract into a new staging directory. Do not merge an unverified archive directly into a live skills directory. @@ -22,7 +22,7 @@ Preflight the destination before copying. If either `teamwork-handoff` or `teamw Rename both verified directories into the skills root. If either rename fails, move any already-renamed directory back into `` and leave no partial live install. Verify the live pair with the same command, replacing the last argument with ``, before restarting harness discovery. Keep both final names unchanged. Never stage beneath a recursively scanned skills root. -These copy/install instructions apply only to recipients authorized by the copyright holder or another applicable agreement; see `LICENSE.txt`. +Teamwork is distributed under the MIT License. Keep the copyright and permission notice with copies or substantial portions of the software; see `LICENSE.txt`. - Codex project: `/.agents/skills/` - Codex user: `/.agents/skills/` diff --git a/LICENSE.txt b/LICENSE.txt index a43b9fa..54838d0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,21 @@ -Copyright (c) 2026. All rights reserved. +MIT License -No license is granted to copy, modify, distribute, sublicense, or sell this software. Internal evaluation and use require permission from the copyright holder or another applicable agreement. +Copyright (c) 2026 Randy Northrup -Third-party tools referenced by the documentation and test process remain under their own licenses. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e820b04..1d21622 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,105 @@ -# Teamwork skills +
-[![certify](https://github.com/RandyNorthrup/teamwork-skill/actions/workflows/certify.yml/badge.svg?branch=main)](https://github.com/RandyNorthrup/teamwork-skill/actions/workflows/certify.yml) +# Teamwork -Teamwork provides two agent-agnostic project continuity skills: +**Portable, secure project continuity for AI coding agents.** -- `teamwork-handoff`: creates or refreshes secure `.teamwork/` payload in repository root, then seals it for transfer. -- `teamwork-resume`: verifies payload, relocates project from payload parent, rebuilds context, and continues first safe action. +[![Certification workflow status](https://github.com/RandyNorthrup/teamwork-skill/actions/workflows/certify.yml/badge.svg?branch=main)](https://github.com/RandyNorthrup/teamwork-skill/actions/workflows/certify.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-2ea44f.svg)](LICENSE.txt) +[![Python 3.11 or newer](https://img.shields.io/badge/python-3.11%2B-3776ab.svg)](https://www.python.org/downloads/) +[![Agent Skills format](https://img.shields.io/badge/format-Agent%20Skills-6f42c1.svg)](https://agentskills.io/) -Harnesses may expose skills as `/teamwork-handoff` and `/teamwork-resume`, or `$teamwork-handoff` and `$teamwork-resume`. Each folder under `skills/` conforms to the open Agent Skills format, is self-contained, and uses Python 3.11+ standard library only. +Hand work to another agent, machine, or session without losing the decisions that make the project understandable. -## Install +
-Build creates a release archive; it does not install anything. Select an explicit Python 3.11+ interpreter (`python3`, `py -3.11`, or an absolute interpreter path): +--- + +## Why Teamwork? + +Long-running projects outlive chat windows and machines. Teamwork turns the context needed for a safe continuation into a portable, verifiable `.teamwork/` payload that stays beside the project without entering Git history. + +`handoff` → `verify and seal` → `transfer project` → `resume` → `continue safely` + +| Skill | Purpose | +| --- | --- | +| `teamwork-handoff` | Creates or refreshes a secure `.teamwork/` payload in the repository root, then seals it for transfer. | +| `teamwork-resume` | Verifies the payload, relocates the project when needed, rebuilds context, and continues with the first safe action. | + +Harnesses may expose these as `/teamwork-handoff` and `/teamwork-resume`, or as `$teamwork-handoff` and `$teamwork-resume`. Both folders follow the open [Agent Skills specification](https://agentskills.io/specification), are self-contained, and use only the Python 3.11+ standard library at runtime. + +## Highlights + +- **Agent-agnostic:** designed for Codex, Claude Code, Gemini CLI, and compatible Agent Skills harnesses. +- **Portable:** moves project knowledge with the working tree across sessions and machines. +- **Tamper-evident:** SHA-256 integrity metadata detects changed payload content. +- **Fail-closed:** resume rejects drafts, incompatible schemas, likely secrets, tracked payloads, and invalid integrity data. +- **Git-safe:** payload stays untracked and is protected by a managed local exclude block. +- **Release-verifiable:** deterministic archives include provenance, file digests, and an SPDX 2.3 SBOM. + +## Quick start + +### 1. Build and verify + +Choose an explicit Python 3.11+ interpreter such as `python3`, `py -3.11`, or an absolute interpreter path. ```text scripts/build_release.py ``` -Verify the `.zip.sha256`, extract to a staging directory, and run ` verify_release.py .`. Install only when verification reports both `ok:true` and `release_grade:true`, then use the two-directory transaction in `INSTALL.md`. Keep folder names unchanged. For current Codex project installs use `/.agents/skills/`; for user installs use `/.agents/skills/`. +Verify the generated `.zip.sha256`, extract the archive into a staging directory, then run: + +```text + verify_release.py . +``` + +Install only when verification reports both `"ok": true` and `"release_grade": true`. Building creates a release archive; it does not install anything. + +### 2. Install both skills + +| Harness | Project or user skills directory | +| --- | --- | +| Codex | `/.agents/skills/` or `/.agents/skills/` | +| Claude Code | `/.claude/skills/` | +| Gemini CLI | `/.gemini/skills/` or `/.agents/skills/` | +| Other harness | Its documented Agent Skills directory | + +Keep both folder names unchanged. Follow [INSTALL.md](INSTALL.md) for exact install, upgrade, project-transfer, rollback, and uninstall procedures. Git 2.22+ is required for the certified untracked-payload guarantee. + +### 3. Handoff and resume -See [INSTALL.md](INSTALL.md) for exact install, upgrade, project-transfer, and uninstall procedures. No third-party Python dependency is required at runtime. Git 2.22+ is required to certify payload remains untracked. +Run the handoff skill before ending work or moving the project. Transfer the working tree together with its untracked `.teamwork/` directory through an approved channel. On the destination, invoke the resume skill; it verifies the payload before rebuilding context or taking action. -## Contract +> [!IMPORTANT] +> A normal Git clone or push does not include `.teamwork/`. Transfer that directory with the project through an approved, encrypted channel. -Handoff payload always lives at `/.teamwork/` by default. Stable Markdown documents carry curated project knowledge; JSON files carry schema, discovery provenance, and SHA-256 integrity metadata. Handoff refreshes same files. Resume fails closed on drafts, tampering, incompatible schemas, likely secrets, or tracked payloads. +## Security and data contract -See [payload contract](docs/PAYLOAD_CONTRACT.md), [security model](docs/SECURITY.md), and [certification scope](docs/CERTIFICATION.md). -See [harness compatibility](docs/COMPATIBILITY.md) for Codex, Claude Code, Gemini CLI, and generic Agent Skills installation paths and tested claim boundaries. -See [independent forward-test evidence](docs/INDEPENDENT_FORWARD_TEST.md) for zero-history prompt controls, artifact hashes, unchanged-test proof, and claim limits. +The default payload location is `/.teamwork/`. Stable Markdown files hold curated project knowledge; JSON files hold schema, discovery provenance, and integrity metadata. Handoff refreshes the same files. Resume treats payload context as information, never as authority, and revalidates the first action against the live checkout. + +Read the focused documentation: + +- [Payload contract](docs/PAYLOAD_CONTRACT.md) — structure, lifecycle, and validation rules. +- [Security model](docs/SECURITY.md) — threat boundaries and fail-closed behavior. +- [Harness compatibility](docs/COMPATIBILITY.md) — tested paths and claim limits. +- [Certification scope](docs/CERTIFICATION.md) — exact evidence and exclusions. +- [Cross-machine transfer](docs/CROSS_MACHINE_TRANSFER.md) — Kubuntu-to-macOS evidence. +- [Independent forward test](docs/INDEPENDENT_FORWARD_TEST.md) — zero-history continuation proof. ## Certified release -Teamwork v1.0.0 is certified from source commit `d74b55d74956ce304ee495747729d502d15936d7`. Download the [v1.0.0 GitHub Release](https://github.com/RandyNorthrup/teamwork-skill/releases/tag/v1.0.0). The release archive SHA-256 is `d00ea706fb58455635d64c560acadcdbe16ae0910c4828fb425bf9d6c51bee34`. +Teamwork v1.0.0 is certified from source commit `d74b55d74956ce304ee495747729d502d15936d7`. Download the [v1.0.0 GitHub Release](https://github.com/RandyNorthrup/teamwork-skill/releases/tag/v1.0.0). Its archive SHA-256 is `d00ea706fb58455635d64c560acadcdbe16ae0910c4828fb425bf9d6c51bee34`. + +That exact artifact passed 65 tests on Windows, Kubuntu, and macOS; Python 3.11–3.14 execution; official Agent Skills and SPDX validation; Gemini CLI discovery; a real Kubuntu-to-macOS transfer; and one isolated zero-history Codex continuation. -The exact artifact passed the 65-test suite on Windows, Kubuntu, and macOS; Python 3.11–3.14 execution; official Agent Skills and SPDX validation; Gemini CLI discovery; a real Kubuntu-to-macOS transfer; and one isolated zero-history Codex continuation. See [certification evidence](docs/CERTIFICATION.md) and [cross-machine transfer evidence](docs/CROSS_MACHINE_TRANSFER.md) for scope and honest claim boundaries. +The artifact includes an SPDX 2.3 SBOM with SHA-1 and SHA-256 file checksums, per-file release digests, and source provenance. Clean builds read the version and allowlisted files from immutable Git blobs in the recorded commit with replacement objects disabled. The SHA-256 sidecar proves integrity, not publisher identity. -The release includes an SPDX 2.3 software bill of materials with SHA-1 and SHA-256 file checksums, per-file release digests, source commit/dirty provenance, and a conservative all-rights-reserved license notice. Clean releases read the version and allowlisted files from immutable Git blobs in the recorded commit with replacement objects disabled, so checkout filters and local replacement refs cannot change artifact bytes. Certification validates the SBOM with the official SPDX Python tools. The SHA-256 sidecar provides integrity, not publisher authentication. +> [!NOTE] +> Published v1.0.0 predates the MIT relicensing and still embeds its earlier proprietary notice. Release archives from v1.0.1 onward carry the MIT License and standard SPDX `MIT` package declaration. -## Develop and certify +## Development -After changing canonical tool or contract, synchronize both self-contained skills: +After changing the canonical tool or contract, synchronize both self-contained skills and run certification: ```powershell scripts/sync_skills.py @@ -47,9 +107,15 @@ After changing canonical tool or contract, synchronize both self-contained skill scripts/certify.py ``` -Skill packages must also pass the pinned official Agent Skills reference validator: +Skill packages must also pass the pinned official Agent Skills validator: ```powershell skills-ref validate skills/teamwork-handoff skills-ref validate skills/teamwork-resume ``` + +See [CHANGELOG.md](CHANGELOG.md) for release history and [VERSION](VERSION) for the current package version. + +## License + +Current source is available under the [MIT License](LICENSE.txt). Copyright © 2026 Randy Northrup. diff --git a/VERSION b/VERSION index 3eefcb9..7dea76e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.0.1 diff --git a/scripts/build_release.py b/scripts/build_release.py index 4b7cf74..e86b5c6 100644 --- a/scripts/build_release.py +++ b/scripts/build_release.py @@ -369,11 +369,11 @@ def build_sbom( "packages": [ { "SPDXID": "SPDXRef-Package-Teamwork", - "copyrightText": "NOASSERTION", + "copyrightText": "Copyright (c) 2026 Randy Northrup", "downloadLocation": "NOASSERTION", "filesAnalyzed": True, - "licenseConcluded": "NOASSERTION", - "licenseDeclared": "LicenseRef-Teamwork-Proprietary", + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", "name": "teamwork-skills", "versionInfo": version, } @@ -386,13 +386,7 @@ def build_sbom( }, *relationships, ], - "hasExtractedLicensingInfos": [ - { - "extractedText": "Proprietary. Use requires authorization from the copyright holder or another applicable agreement.", - "licenseId": "LicenseRef-Teamwork-Proprietary", - "name": "Teamwork Proprietary License", - } - ], + "hasExtractedLicensingInfos": [], "spdxVersion": "SPDX-2.3", } return (json.dumps(document, indent=2, sort_keys=True) + "\n").encode("utf-8") diff --git a/scripts/verify_release.py b/scripts/verify_release.py index ba0e94e..a1fff0d 100644 --- a/scripts/verify_release.py +++ b/scripts/verify_release.py @@ -233,18 +233,13 @@ def verify(root: Path, installed_skills_root: Path | None = None) -> dict[str, o else content_identity(base_hashes) ) created_at = source["created_at"] if source["available"] else "1980-01-01T00:00:00Z" - expected_license = { - "extractedText": "Proprietary. Use requires authorization from the copyright holder or another applicable agreement.", - "licenseId": "LicenseRef-Teamwork-Proprietary", - "name": "Teamwork Proprietary License", - } expected_package = { "SPDXID": "SPDXRef-Package-Teamwork", - "copyrightText": "NOASSERTION", + "copyrightText": "Copyright (c) 2026 Randy Northrup", "downloadLocation": "NOASSERTION", "filesAnalyzed": True, - "licenseConcluded": "NOASSERTION", - "licenseDeclared": "LicenseRef-Teamwork-Proprietary", + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", "name": "teamwork-skills", "versionInfo": manifest["version"], } @@ -258,7 +253,7 @@ def verify(root: Path, installed_skills_root: Path | None = None) -> dict[str, o or sbom["creationInfo"] != {"created": created_at, "creators": ["Tool: teamwork-build-release"]} or sbom["packages"] != [expected_package] - or sbom["hasExtractedLicensingInfos"] != [expected_license] + or sbom["hasExtractedLicensingInfos"] != [] or not isinstance(sbom["files"], list) ): raise RuntimeError("SPDX SBOM semantic binding is invalid") diff --git a/skills/teamwork-handoff/scripts/teamwork_payload.py b/skills/teamwork-handoff/scripts/teamwork_payload.py index 187cd65..4ca01b1 100644 --- a/skills/teamwork-handoff/scripts/teamwork_payload.py +++ b/skills/teamwork-handoff/scripts/teamwork_payload.py @@ -26,7 +26,7 @@ SCHEMA_NAME = "teamwork-payload" SCHEMA_VERSION = "1.0.0" -SKILL_VERSION = "1.0.0" +SKILL_VERSION = "1.0.1" MIN_PYTHON = (3, 11) MIN_GIT = (2, 22) DEFAULT_PAYLOAD_DIR = ".teamwork" diff --git a/skills/teamwork-resume/scripts/teamwork_payload.py b/skills/teamwork-resume/scripts/teamwork_payload.py index 187cd65..4ca01b1 100644 --- a/skills/teamwork-resume/scripts/teamwork_payload.py +++ b/skills/teamwork-resume/scripts/teamwork_payload.py @@ -26,7 +26,7 @@ SCHEMA_NAME = "teamwork-payload" SCHEMA_VERSION = "1.0.0" -SKILL_VERSION = "1.0.0" +SKILL_VERSION = "1.0.1" MIN_PYTHON = (3, 11) MIN_GIT = (2, 22) DEFAULT_PAYLOAD_DIR = ".teamwork" diff --git a/src/teamwork_payload.py b/src/teamwork_payload.py index 187cd65..4ca01b1 100644 --- a/src/teamwork_payload.py +++ b/src/teamwork_payload.py @@ -26,7 +26,7 @@ SCHEMA_NAME = "teamwork-payload" SCHEMA_VERSION = "1.0.0" -SKILL_VERSION = "1.0.0" +SKILL_VERSION = "1.0.1" MIN_PYTHON = (3, 11) MIN_GIT = (2, 22) DEFAULT_PAYLOAD_DIR = ".teamwork" diff --git a/tests/test_teamwork_payload.py b/tests/test_teamwork_payload.py index 1ee7be0..dbd3909 100644 --- a/tests/test_teamwork_payload.py +++ b/tests/test_teamwork_payload.py @@ -1301,7 +1301,10 @@ def test_clean_release_reads_commit_canonical_git_blobs(self) -> None: check=True, ).stdout self.assertEqual(expected_version, version_bytes) - self.assertEqual("1.0.0", build_release.parse_version(version_bytes)) + self.assertEqual( + expected_version.decode("utf-8").strip(), + build_release.parse_version(version_bytes), + ) self.assertEqual( files["teamwork-handoff/scripts/teamwork_payload.py"], files["teamwork-resume/scripts/teamwork_payload.py"], @@ -1370,6 +1373,7 @@ def archive_git(*args: str) -> str: archive_git("config", "user.email", "replace@example.invalid") archive_git("config", "user.name", "Replace Fixture") archive_original = archive_git("rev-parse", "HEAD") + archive_version = archive_git("show", f"{archive_original}:VERSION") (archive_repo / "VERSION").write_text("9.9.9\n", encoding="utf-8") archive_git("commit", "-am", "replacement version") archive_replacement = archive_git("rev-parse", "HEAD") @@ -1378,10 +1382,10 @@ def archive_git(*args: str) -> str: replaced_archive = Path(temporary) / "replace-proof.zip" with mock.patch.object(build_release, "ROOT", archive_repo): result = build_release.build(replaced_archive, require_clean=True) - self.assertEqual("1.0.0", result["version"]) + self.assertEqual(archive_version, result["version"]) with zipfile.ZipFile(replaced_archive) as archive: manifest = json.loads(archive.read("release-manifest.json")) - self.assertEqual("1.0.0", manifest["version"]) + self.assertEqual(archive_version, manifest["version"]) self.assertEqual(archive_original, manifest["source"]["commit"]) archive_git("replace", "-d", archive_original) @@ -1391,10 +1395,10 @@ def archive_git(*args: str) -> str: filtered_archive = Path(temporary) / "working-filter-proof.zip" with mock.patch.object(build_release, "ROOT", archive_repo): result = build_release.build(filtered_archive, require_clean=True) - self.assertEqual("1.0.0", result["version"]) + self.assertEqual(archive_version, result["version"]) with zipfile.ZipFile(filtered_archive) as archive: manifest = json.loads(archive.read("release-manifest.json")) - self.assertEqual("1.0.0", manifest["version"]) + self.assertEqual(archive_version, manifest["version"]) self.assertEqual(archive_original, manifest["source"]["commit"]) with ( mock.patch.object(build_release, "ROOT", archive_repo), @@ -1403,9 +1407,11 @@ def archive_git(*args: str) -> str: return_code = build_release.main(["--require-clean", "--json"]) self.assertEqual(0, return_code, output.getvalue()) default_result = json.loads(output.getvalue()) - self.assertEqual("1.0.0", default_result["version"]) + self.assertEqual(archive_version, default_result["version"]) self.assertEqual( - (archive_repo / "dist" / "teamwork-skills-1.0.0.zip").resolve(), + ( + archive_repo / "dist" / f"teamwork-skills-{archive_version}.zip" + ).resolve(), Path(default_result["archive"]).resolve(), ) self.assertFalse( @@ -1528,7 +1534,7 @@ def test_manifest_schema_declares_strict_current_dialect(self) -> None: self.assertEqual("urn:teamwork:manifest:1.0.0", schema["$id"]) self.assertFalse(schema["additionalProperties"]) self.assertEqual( - "1.0.0", (ROOT / "VERSION").read_text(encoding="utf-8").strip() + "1.0.1", (ROOT / "VERSION").read_text(encoding="utf-8").strip() ) def test_release_archive_is_reproducible_and_self_verifying(self) -> None: @@ -1563,6 +1569,15 @@ def test_release_archive_is_reproducible_and_self_verifying(self) -> None: self.assertIn("LICENSE.txt", names) self.assertIn("SBOM.spdx.json", names) self.assertIn("verify_release.py", names) + self.assertTrue( + archive.read("LICENSE.txt") + .decode("utf-8") + .startswith("MIT License") + ) + sbom = json.loads(archive.read("SBOM.spdx.json")) + self.assertEqual("MIT", sbom["packages"][0]["licenseDeclared"]) + self.assertEqual("MIT", sbom["packages"][0]["licenseConcluded"]) + self.assertEqual([], sbom["hasExtractedLicensingInfos"]) manifest = json.loads(archive.read("release-manifest.json")) self.assertEqual( "https://agentskills.io/specification", @@ -1849,7 +1864,7 @@ def initialize_git(repo: Path) -> None: capture_output=True, check=False, ) - self.assertEqual("1.0.0", installed_version.stdout.strip()) + self.assertEqual("1.0.1", installed_version.stdout.strip()) for skill in ("teamwork-handoff", "teamwork-resume"): shutil.rmtree(harness_root / skill) self.assertEqual([], list(harness_root.iterdir()))