diff --git a/.github/scripts/ci-test-sharding.md b/.github/scripts/ci-test-sharding.md index 6d0e6a0a..4c0c6fe4 100644 --- a/.github/scripts/ci-test-sharding.md +++ b/.github/scripts/ci-test-sharding.md @@ -55,13 +55,10 @@ estimated cost, place each on the currently-lightest shard. duplicated test). This invariant is unit-tested in `test_ci_shard.py`, which the `ci-policy` workflow runs — a broken partition can't merge. -## Keep the two copies in sync +## Canonical location -`.github/` is control-plane and is **not** projected by labkit, so the upstream -`operatorstack/intelligence-flow` monorepo carries its own copy of `ci_shard.py` -and its own sharded `runtime-windows` job in -`.github/workflows/boatstack-lab.yml`. When you change the controller here, -mirror it there (and vice versa). +This repository owns `ci_shard.py` and the sharded runtime workflow. There is no +upstream mirror or external CI authority. ## Operational note diff --git a/.github/scripts/release_notes.py b/.github/scripts/release_notes.py new file mode 100644 index 00000000..5f802dd4 --- /dev/null +++ b/.github/scripts/release_notes.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Validate Boatstack's append-only release-note contract.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +from pathlib import Path + + +NAME_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$") +HEADING_PATTERN = re.compile(r"^### [^\s].+$") +RELEASE_NOTES = Path("release-notes") + + +def validate_release_note(path: Path) -> None: + if not NAME_PATTERN.fullmatch(path.name): + raise ValueError(f"{path}: name must match YYYY-MM-DD-.md") + try: + content = path.read_text(encoding="utf-8") + except UnicodeDecodeError as error: + raise ValueError(f"{path}: release note must be UTF-8") from error + if not content.endswith("\n"): + raise ValueError(f"{path}: release note must end with a newline") + lines = content.splitlines() + if not lines or not HEADING_PATTERN.fullmatch(lines[0]): + raise ValueError(f"{path}: first line must be a level-three Markdown heading") + if not any(line.strip() for line in lines[1:]): + raise ValueError(f"{path}: release note must describe user impact") + + +def validate_directory(repo: Path) -> None: + root = repo / RELEASE_NOTES + if not root.is_dir(): + raise ValueError(f"{root}: release-note directory is missing") + unexpected = sorted( + path for path in root.iterdir() + if not path.is_file() or path.is_symlink() or path.suffix != ".md" + ) + if unexpected: + raise ValueError(f"{unexpected[0]}: only direct Markdown files are allowed") + notes = sorted(root.glob("*.md")) + if not notes: + raise ValueError(f"{root}: at least one release note is required") + for note in notes: + validate_release_note(note) + + +def git(repo: Path, *args: str) -> str: + result = subprocess.run( + ["git", *args], cwd=repo, text=True, capture_output=True, check=False + ) + if result.returncode != 0: + raise ValueError(result.stderr.strip() or f"git {' '.join(args)} failed") + return result.stdout.strip() + + +def check_policy(repo: Path, base: str, head: str) -> None: + output = git(repo, "diff", "--name-status", "--no-renames", base, head) + changes: list[tuple[str, Path]] = [] + for line in output.splitlines(): + if line: + status, value = line.split("\t", 1) + changes.append((status, Path(value))) + if not changes: + return + note_changes = [ + (status, path) + for status, path in changes + if path.is_relative_to(RELEASE_NOTES) + ] + rewritten = [f"{status}\t{path}" for status, path in note_changes if status != "A"] + if rewritten: + raise ValueError( + "release notes are append-only; add a correction fragment instead:\n " + + "\n ".join(rewritten) + ) + added = [repo / path for status, path in note_changes if status == "A"] + if not added: + raise ValueError("Boatstack changes require a new file under release-notes/") + for note in sorted(added): + validate_release_note(note) + + +def preflight(repo: Path, remote: str, base_branch: str, head: str) -> None: + dirty = git(repo, "status", "--porcelain", "--untracked-files=all") + if dirty: + raise ValueError("commit or remove uncommitted changes before preflight") + git(repo, "fetch", "--quiet", remote, base_branch) + check_policy(repo, f"refs/remotes/{remote}/{base_branch}", head) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + validate = subparsers.add_parser("validate") + validate.add_argument("--repo", type=Path, default=Path(".")) + check = subparsers.add_parser("check-policy") + check.add_argument("--repo", type=Path, required=True) + check.add_argument("--base", required=True) + check.add_argument("--head", required=True) + before = subparsers.add_parser("preflight") + before.add_argument("--repo", type=Path, required=True) + before.add_argument("--remote", default="origin") + before.add_argument("--base-branch", default="main") + before.add_argument("--head", default="HEAD") + args = parser.parse_args() + try: + repo = args.repo.resolve() + validate_directory(repo) + if args.command == "check-policy": + check_policy(repo, args.base, args.head) + elif args.command == "preflight": + preflight(repo, args.remote, args.base_branch, args.head) + except ValueError as error: + print(f"BLOCKED: {error}") + return 1 + print("PASS: Boatstack release-note contract is satisfied") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/tests/test_detached_supervision.py b/.github/tests/test_detached_supervision.py new file mode 100644 index 00000000..59db4b6a --- /dev/null +++ b/.github/tests/test_detached_supervision.py @@ -0,0 +1,234 @@ +"""End-to-end evaluation of Detached Supervision. + +Unlike the Go unit conformance tests, this harness builds the real +``boatstack-helper`` binary once and drives it against actual scratch git +repositories — attach, activate, guard, and detach — asserting at every step that +the plant/controller boundary holds: no Boatstack-owned file ever lands in the +target repository or its ``.git``, and the developer's own host config is never +clobbered. It is the "actually set up repos and evaluate the system works" check. + +Run (from repo root): + python -m unittest discover -s labs/12-product-engineering-loop/tests -p 'test_*.py' +""" + +from __future__ import annotations + +import json +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +REPO = Path(__file__).resolve().parents[2] +SKILL = REPO / "boatstack" + +FORBIDDEN_IN_REPO = [ + ".product-loop", + ".boatstack-project.json", + ".claude", + ".cursor", + ".codex", + ".gemini", + ".agents", + ".github/PULL_REQUEST_TEMPLATE/boatstack.md", +] + +DESTRUCTIVE_EVENT = json.dumps( + { + "hook_event_name": "PreToolUse", + "tool_name": "Bash", + "tool_input": {"command": "git reset --hard HEAD~1"}, + } +) + + +class DetachedSupervisionEndToEnd(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.build_temp = tempfile.TemporaryDirectory() + cls.binary = Path(cls.build_temp.name) / ( + "boatstack-helper.exe" if os.name == "nt" else "boatstack-helper" + ) + env = dict(os.environ) + env["GOCACHE"] = str(Path(cls.build_temp.name) / "go-cache") + env["GOMODCACHE"] = str(Path(cls.build_temp.name) / "go-mod") + result = subprocess.run( + ["go", "build", "-o", str(cls.binary), "./cmd/boatstack-helper"], + cwd=SKILL, + env=env, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise RuntimeError(result.stdout + result.stderr) + + @classmethod + def tearDownClass(cls) -> None: + cls.build_temp.cleanup() + + def setUp(self) -> None: + self.work = tempfile.TemporaryDirectory() + self.addCleanup(self.work.cleanup) + base = Path(self.work.name) + self.state_root = base / "state" + self.user_root = base / "user" + self.repo = base / "app" + for path in (self.state_root, self.user_root, self.repo): + path.mkdir() + self._git("init", "-b", "main") + self._git("config", "user.name", "Boatstack Test") + self._git("config", "user.email", "boatstack@example.invalid") + self._git("remote", "add", "origin", "https://github.com/acme/app.git") + (self.repo / "README.md").write_text("# app\n") + (self.repo / "go.mod").write_text("module app\n\ngo 1.22\n") + self._git("add", ".") + self._git("commit", "-m", "init") + + # --- helpers ------------------------------------------------------------- + + def _git(self, *args: str) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + ["git", "-C", str(self.repo), *args], text=True, capture_output=True + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return result + + def _env(self) -> dict: + env = dict(os.environ) + env["BOATSTACK_STATE_ROOT"] = str(self.state_root) + env["BOATSTACK_USER_CONFIG_ROOT"] = str(self.user_root) + return env + + def run_helper(self, *args: object, expected: int = 0, stdin: str | None = None): + result = subprocess.run( + [str(self.__class__.binary), *map(str, args)], + cwd=str(self.repo), + text=True, + capture_output=True, + input=stdin, + env=self._env(), + ) + self.assertEqual( + result.returncode, expected, f"{args}\nSTDOUT:{result.stdout}\nSTDERR:{result.stderr}" + ) + return result + + def helper_json(self, *args: object) -> dict: + return json.loads(self.run_helper(*args).stdout) + + def porcelain(self) -> str: + return subprocess.run( + ["git", "-C", str(self.repo), "status", "--porcelain=v1", "--untracked-files=all"], + text=True, + capture_output=True, + ).stdout.strip() + + def assert_repo_uncontaminated(self) -> None: + for forbidden in FORBIDDEN_IN_REPO: + self.assertFalse( + (self.repo / forbidden).exists(), + f"Boatstack file leaked into the repo: {forbidden}", + ) + + # --- tests --------------------------------------------------------------- + + def test_attach_leaves_repository_pristine_and_state_external(self) -> None: + before = self.porcelain() + result = self.helper_json("attach", "--repo", ".", "--mode", "detached") + self.assertEqual(result["verification_status"], "VERIFIED") + + self.assertEqual(self.porcelain(), before, "attach changed the working tree") + self.assert_repo_uncontaminated() + + control_root = Path(result["control_root"]) + self.assertTrue((control_root / ".product-loop" / "project.json").exists()) + self.assertTrue((control_root / "binding.json").exists()) + self.assertTrue((self.state_root / "boatstack" / "registry.json").exists()) + # The external shared runtime slot was populated so the guard has a helper. + runtimes = self.state_root / "boatstack" / "runtimes" + self.assertTrue(runtimes.exists() and any(runtimes.rglob("boatstack-helper*"))) + + status = self.helper_json("detached-status", "--repo", ".") + self.assertTrue(status["attached"] and status["verified"]) + + def test_activate_installs_guard_preserving_user_hooks(self) -> None: + self.run_helper("attach", "--repo", ".", "--mode", "detached") + + claude_config = self.user_root / ".claude" / "settings.json" + claude_config.parent.mkdir(parents=True, exist_ok=True) + claude_config.write_text( + json.dumps( + { + "theme": "dark", + "hooks": { + "PreToolUse": [ + {"matcher": "Bash", "hooks": [{"type": "command", "command": "my-own.sh"}]} + ] + }, + } + ) + ) + + installed = self.helper_json("activate", "--repo", ".") + self.assertEqual(installed["verification_status"], "VERIFIED") + + text = claude_config.read_text() + self.assertIn("my-own.sh", text) + self.assertIn("ambient-safety-hook", text) + self.assertIn("theme", text) + + # Idempotent: re-activating changes nothing. + again = self.helper_json("activate", "--repo", ".", "--host", "claude") + self.assertTrue(all(host["action"] == "unchanged" for host in again["hosts"])) + + # Deactivate removes only the ambient guard. + self.run_helper("deactivate", "--repo", ".", "--host", "claude") + after = claude_config.read_text() + self.assertNotIn("ambient-safety-hook", after) + self.assertIn("my-own.sh", after) + + def test_ambient_guard_enforces_managed_and_noops_unmanaged(self) -> None: + # Unattached: the developer-level guard must not control this repository. + unmanaged = self.run_helper("ambient-safety-hook", "--host", "claude", "--repo", ".", stdin=DESTRUCTIVE_EVENT) + self.assertNotIn('"permissionDecision":"deny"', unmanaged.stdout) + + # Attached: the same destructive command is denied by the same engine. + self.run_helper("attach", "--repo", ".", "--mode", "detached") + managed = self.run_helper("ambient-safety-hook", "--host", "claude", "--repo", ".", stdin=DESTRUCTIVE_EVENT) + self.assertIn('"permissionDecision":"deny"', managed.stdout) + + def test_detached_work_keeps_repo_product_only(self) -> None: + self.run_helper("attach", "--repo", ".", "--mode", "detached") + + context = self.helper_json("context", "--repo", ".", "--operation", "build", "--host", "claude") + self.assertEqual(context["mode"], "detached") + self.assertTrue(context["attached"]) + self.assertNotEqual(context.get("next_operation", ""), "") + + # Boatstack operations are read-only against the plant: the repo is pristine. + self.assertEqual(self.porcelain(), "") + self.assert_repo_uncontaminated() + + # The only change that ever appears in the repo is product work. + (self.repo / "feature.txt").write_text("product work\n") + self.assertEqual(self.porcelain(), "?? feature.txt") + + def test_detach_removes_external_state_and_restores_embedded(self) -> None: + attached = self.helper_json("attach", "--repo", ".", "--mode", "detached") + control_root = Path(attached["control_root"]) + self.assertTrue(control_root.exists()) + + removed = self.helper_json("detach", "--repo", ".") + self.assertEqual(removed["verification_status"], "VERIFIED") + self.assertTrue(removed["state_removed"]) + self.assertFalse(control_root.exists()) + + status = self.helper_json("detached-status", "--repo", ".") + self.assertFalse(status["attached"]) + self.assert_repo_uncontaminated() + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/tests/test_repository_contract.py b/.github/tests/test_repository_contract.py new file mode 100644 index 00000000..e83fd664 --- /dev/null +++ b/.github/tests/test_repository_contract.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import json +import os +import re +import subprocess +import tempfile +import unittest +import xml.etree.ElementTree as ET +from pathlib import Path + + +REPO = Path(__file__).resolve().parents[2] +RUNTIME = REPO / "boatstack" +CONFIG = REPO / "project.example.json" + + +class RepositoryContract(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.build = tempfile.TemporaryDirectory() + cls.helper = Path(cls.build.name) / ( + "boatstack-helper.exe" if os.name == "nt" else "boatstack-helper" + ) + result = subprocess.run( + ["go", "build", "-o", str(cls.helper), "./cmd/boatstack-helper"], + cwd=RUNTIME, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise RuntimeError(result.stdout + result.stderr) + + @classmethod + def tearDownClass(cls) -> None: + cls.build.cleanup() + + def run_command(self, *args: object, cwd: Path | None = None, expected: int = 0): + result = subprocess.run( + [*map(str, args)], cwd=cwd, text=True, capture_output=True + ) + self.assertEqual(result.returncode, expected, result.stdout + result.stderr) + return result + + def run_helper(self, *args: object, expected: int = 0): + return self.run_command(self.helper, *args, expected=expected) + + def test_active_workflows_have_no_intelligence_flow_path(self) -> None: + workflows = REPO / ".github" / "workflows" + self.assertFalse((workflows / "sync-upstream.yml").exists()) + for workflow in workflows.glob("*.yml"): + value = workflow.read_text() + self.assertNotIn("operatorstack/intelligence-flow", value, workflow) + self.assertNotIn("sync/intelligence-flow-", value, workflow) + self.assertNotIn("UPSTREAM.json", value, workflow) + + def test_release_authority_uses_boatstack_revision(self) -> None: + release = (REPO / ".github" / "workflows" / "release.yml").read_text() + automatic = (REPO / ".github" / "workflows" / "auto-release.yml").read_text() + self.assertIn('source_commit="$(git rev-parse HEAD)"', release) + self.assertNotIn("IMPORT_PROVENANCE.json", release) + self.assertNotIn("UPSTREAM.json", release) + self.assertIn('workflows: ["Verify Boatstack distribution"]', automatic) + self.assertIn("github.event.workflow_run.event == 'push'", automatic) + self.assertIn("github.event.workflow_run.head_branch == 'main'", automatic) + self.assertIn("repositories: boatstack", automatic) + + def test_current_public_surface_is_boatstack_owned(self) -> None: + current = [REPO / "README.md", REPO / "CONTRIBUTING.md", *sorted((REPO / "docs").glob("*"))] + forbidden = ( + "Generated from operatorstack/intelligence-flow", + "Edit the upstream public source", + "edit in Intelligence Flow", + "generated content distribution", + ) + for path in current: + if not path.is_file() or path.suffix not in {".md", ".json"}: + continue + value = path.read_text() + for phrase in forbidden: + self.assertNotIn(phrase, value, path) + self.assertTrue((REPO / "IMPORT_PROVENANCE.json").is_file()) + self.assertFalse((REPO / "UPSTREAM.json").exists()) + + def test_document_links_claims_and_assets_are_valid(self) -> None: + def anchors(document: Path) -> set[str]: + result = set() + for heading in re.findall(r"^#{1,6}\s+(.+?)\s*$", document.read_text(), re.MULTILINE): + plain = re.sub(r"<[^>]+>", "", heading).strip().lower() + plain = re.sub(r"[^\w\s-]", "", plain) + result.add(re.sub(r"\s+", "-", plain)) + return result + + documents = [REPO / "README.md", *sorted((REPO / "docs").glob("*.md"))] + for document in documents: + for target in re.findall(r"\[[^\]]+\]\(([^)]+)\)", document.read_text()): + if target.startswith(("http://", "https://", "#", "mailto:")): + continue + relative, _, anchor = target.partition("#") + resolved = (document.parent / relative).resolve() + self.assertTrue(resolved.exists(), f"broken link {target} in {document}") + if anchor and resolved.suffix == ".md": + self.assertIn(anchor, anchors(resolved), f"broken anchor {target}") + + configuration = (REPO / "docs" / "configuration.md").read_text() + for example in re.findall(r"```json\n(.*?)\n```", configuration, re.DOTALL): + json.loads(example) + + claims = json.loads((REPO / "docs" / "public-claims.json").read_text()) + self.assertNotIn("source_commit", claims) + allowed = set(claims["statuses"]) + for claim in claims["claims"]: + self.assertIn(claim["status"], allowed) + self.assertRegex(claim["last_verified_version"], r"^v\d+\.\d+\.\d+$") + readable, _, anchor = claim["readable_evidence"].partition("#") + readable_path = REPO / "docs" / readable + self.assertTrue(readable_path.is_file(), claim["id"]) + self.assertIn(anchor, anchors(readable_path), claim["id"]) + for evidence in claim["implementation"] + claim["verification"]: + self.assertTrue((REPO / "docs" / evidence).resolve().is_file(), evidence) + + for name in ("boatstack-mark.svg", "boatstack-journey.svg", "boatstack-portability.svg"): + path = REPO / "assets" / name + root = ET.parse(path).getroot() + self.assertEqual(root.attrib.get("role"), "img", name) + value = path.read_text() + self.assertIn(" None: + paths = [ + REPO / "docs" / "account-recovery-walkthrough.md", + RUNTIME / "testdata" / "reviewer-pr-body.md", + ] + for path in paths: + value = path.read_text() + for private in ("TaxWeave", "/Users/", "bigboateng", "cursor_password_reset_button_addition"): + self.assertNotIn(private, value, path) + + def test_export_and_drift_contract(self) -> None: + with tempfile.TemporaryDirectory() as temp: + target = Path(temp) + self.run_helper("export", "--repo", target, "--config", CONFIG, "--adapter-name", "boatstack", "--write") + checked = self.run_helper("export", "--repo", target, "--config", CONFIG, "--adapter-name", "boatstack", "--check") + self.assertIn("PASS", checked.stdout) + self.assertTrue((target / ".cursor" / "commands" / "auto-plan.md").is_file()) + self.assertTrue((target / ".agents" / "skills" / "boatstack" / "SKILL.md").is_file()) + (target / ".cursor" / "commands" / "auto-plan.md").write_text("drift\n") + drift = self.run_helper("export", "--repo", target, "--config", CONFIG, "--adapter-name", "boatstack", "--check", expected=1) + self.assertIn("drift", (drift.stdout + drift.stderr).lower()) + + def test_plan_activation_and_pr_preview_contract(self) -> None: + with tempfile.TemporaryDirectory() as temp: + repo = Path(temp) + self.run_command("git", "init", "-b", "main", cwd=repo) + self.run_command("git", "config", "user.name", "Boatstack Test", cwd=repo) + self.run_command("git", "config", "user.email", "boatstack@example.invalid", cwd=repo) + (repo / ".product-loop").mkdir() + config = json.loads(CONFIG.read_text()) + config["project"]["default_branch"] = "main" + (repo / ".product-loop" / "project.json").write_text(json.dumps(config) + "\n") + (repo / "README.md").write_text("# Fixture\n") + self.run_command("git", "add", ".", cwd=repo) + self.run_command("git", "commit", "-m", "base", cwd=repo) + bare = repo / ".git" / "origin.git" + self.run_command("git", "init", "--bare", bare) + self.run_command("git", "remote", "add", "origin", bare, cwd=repo) + self.run_command("git", "push", "-u", "origin", "main", cwd=repo) + self.run_command("git", "switch", "-c", "feat/direct", cwd=repo) + (repo / "feature.txt").write_text("value\n") + self.run_command("git", "add", "feature.txt", cwd=repo) + self.run_command("git", "commit", "-m", "feature", cwd=repo) + context = json.loads(self.run_helper("pr-context", "--repo", repo).stdout) + self.assertEqual(context["mode"], "ad-hoc") + template = self.run_helper("pr-context", "--repo", repo, "--format", "template") + self.assertIn("boatstack_pr_version: 4", template.stdout) + self.assertIn("## Review order", template.stdout) + + demo = REPO / "labs" / "diagram-json" + checked = self.run_helper("check-plan", "--plan", demo / "plan.md") + self.assertIn("PASS", checked.stdout) + + def test_installers_verify_downloads_and_support_updates(self) -> None: + shell = (REPO / "install.sh").read_text() + powershell = (REPO / "install.ps1").read_text() + for expected in ("sha256sum", "BOATSTACK_INTEGRATIONS", "BOATSTACK_MODE", "BOATSTACK_VERSION", "--repair"): + self.assertIn(expected, shell) + for expected in ("Get-FileHash", "BOATSTACK_INTEGRATIONS", "BOATSTACK_MODE", "BOATSTACK_VERSION", "--repair"): + self.assertIn(expected, powershell) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index bf779fcd..870bb70a 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -30,16 +30,16 @@ jobs: owner: operatorstack repositories: boatstack permission-contents: write - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: ref: main fetch-depth: 0 token: ${{ steps.app-token.outputs.token }} - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v7 with: go-version-file: boatstack/go.mod cache-dependency-path: boatstack/go.mod - - name: Detect release-bearing projection + - name: Detect release-bearing change id: classify shell: bash run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 967269d5..32b439dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,10 @@ on: permissions: contents: read +concurrency: + group: boatstack-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: # Unix runs the full suite serially (~1-2 min) and is the unsharded correctness # reference. Windows is sharded in `test-windows` (see below). @@ -19,64 +23,21 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - name: Verify upstream sync contract - shell: bash - run: | - workflow=".github/workflows/sync-upstream.yml" - current="labs/12-product-engineering-loop" - retired="examples/12-product-engineering-loop" - count="$(grep -cF "$current" "$workflow")" - if [[ "$count" != "2" ]]; then - echo "Expected exactly two sync references to $current; found $count." >&2 - exit 1 - fi - if grep -Fq "$retired" "$workflow"; then - echo "Sync workflow still references retired path $retired." >&2 - exit 1 - fi - title='Sync Boatstack from Intelligence Flow Labs @ $short' - title_count="$(grep -cF "$title" "$workflow")" - if [[ "$title_count" != "2" ]]; then - echo "Expected commit and PR titles to use $title; found $title_count." >&2 - exit 1 - fi - - name: Detect projected runtime - id: runtime - shell: bash - run: | - if [[ -f boatstack/go.mod ]]; then - echo "go=true" >> "$GITHUB_OUTPUT" - else - echo "go=false" >> "$GITHUB_OUTPUT" - fi - - uses: actions/setup-go@v5 - if: steps.runtime.outputs.go == 'true' + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version-file: boatstack/go.mod - cache-dependency-path: boatstack/go.mod - - run: go test ./... - if: steps.runtime.outputs.go == 'true' + cache-dependency-path: boatstack/go.sum + - name: Show toolchain + run: go version working-directory: boatstack - - run: go build ./cmd/boatstack-helper - if: steps.runtime.outputs.go == 'true' + - name: Test runtime + run: go test ./... + working-directory: boatstack + - name: Build helper + run: go build ./cmd/boatstack-helper working-directory: boatstack - - uses: actions/setup-python@v5 - if: steps.runtime.outputs.go != 'true' - with: - python-version: "3.11" - - name: Verify legacy projection during migration - if: steps.runtime.outputs.go != 'true' - env: - PYTHONUTF8: "1" - run: python3 -m unittest discover -s tests -v - - name: Compile legacy projection during migration - if: steps.runtime.outputs.go != 'true' - env: - PYTHONUTF8: "1" - run: python3 -m compileall -q boatstack - name: Validate Bash installer - if: steps.runtime.outputs.go == 'true' shell: bash run: bash -n install.sh @@ -94,22 +55,12 @@ jobs: shard: [0, 1, 2, 3, 4, 5] runs-on: windows-latest steps: - - uses: actions/checkout@v4 - - name: Detect projected runtime - id: runtime - shell: bash - run: | - if [[ -f boatstack/go.mod ]]; then - echo "go=true" >> "$GITHUB_OUTPUT" - else - echo "go=false" >> "$GITHUB_OUTPUT" - fi + - uses: actions/checkout@v7 # Windows `go test`/`go build` is dominated by Microsoft Defender scanning # the many small files the Go toolchain emits during compile/link. Excluding # the Go caches, the workspace, and go.exe is a major wall-clock lever; # sharding on top of this is what gets the suite under 5 min. - name: Exclude Go caches from Microsoft Defender (Windows) - if: steps.runtime.outputs.go == 'true' shell: pwsh run: | $targets = @( @@ -127,17 +78,18 @@ jobs: } } try { Add-MpPreference -ExclusionProcess 'go.exe' -ErrorAction Stop } catch {} - - uses: actions/setup-go@v5 - if: steps.runtime.outputs.go == 'true' + - uses: actions/setup-go@v7 with: go-version-file: boatstack/go.mod - cache-dependency-path: boatstack/go.mod + cache-dependency-path: boatstack/go.sum + - name: Show toolchain + run: go version + working-directory: boatstack # Enumerate tests, pick this shard's balanced subset, and run only those. # `go test -list` and `go test -run` share the warm GOCACHE, so the second # compile is a cache hit. An empty shard is a clean skip — never # `go test -run ''`, which would run the whole suite. - name: Test shard ${{ matrix.shard }} - if: steps.runtime.outputs.go == 'true' shell: bash working-directory: boatstack run: | @@ -150,11 +102,11 @@ jobs: go test -run "$regex" ./... # Windows-only, non-test validation runs once (on shard 0), not per shard. - name: Build helper - if: steps.runtime.outputs.go == 'true' && matrix.shard == '0' + if: matrix.shard == '0' working-directory: boatstack run: go build ./cmd/boatstack-helper - name: Validate PowerShell installer - if: steps.runtime.outputs.go == 'true' && matrix.shard == '0' + if: matrix.shard == '0' shell: pwsh run: | $tokens = $null @@ -165,77 +117,28 @@ jobs: exit 1 } - auto-merge-sync: - if: >- - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository && - startsWith(github.head_ref, 'sync/intelligence-flow-') - needs: [test, test-windows] + repository-contract: + name: repository-contract runs-on: ubuntu-latest steps: - - name: Create repository automation token - id: app-token - uses: actions/create-github-app-token@v3 + - uses: actions/checkout@v7 with: - client-id: ${{ vars.BOATSTACK_APP_CLIENT_ID }} - private-key: ${{ secrets.BOATSTACK_APP_PRIVATE_KEY }} - owner: operatorstack - repositories: boatstack - permission-contents: write - permission-pull-requests: write - - uses: actions/checkout@v4 + fetch-depth: 0 + - uses: actions/setup-python@v6 with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Verify generated projection provenance - env: - APP_SLUG: ${{ steps.app-token.outputs.app-slug }} - HEAD_BRANCH: ${{ github.head_ref }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - shell: bash - run: | - source_repo="$(jq -r '.source.repository' UPSTREAM.json)" - source_commit="$(jq -r '.source.commit' UPSTREAM.json)" - short="${source_commit:0:12}" - [[ "$PR_AUTHOR" == "${APP_SLUG}[bot]" ]] - [[ "$source_repo" == "operatorstack/intelligence-flow" ]] - [[ "$HEAD_BRANCH" == "sync/intelligence-flow-$short" ]] - - name: Merge verified generated PR - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - PR_URL: ${{ github.event.pull_request.html_url }} - run: gh pr merge "$PR_URL" --squash - # After a sync lands, close any older sync PR it supersedes. A sync PR that - # failed CI is never merged by this job, so without this it lingers as an - # orphan once a newer sync overtakes it (see the pile-up that motivated this). - # intelligence-flow is public, so the default token can read its history for - # the ancestry check; closing uses the app token (pull-requests: write). - - name: Check out Intelligence Flow for ancestry - uses: actions/checkout@v4 + python-version: "3.11" + - uses: actions/setup-go@v7 with: - repository: operatorstack/intelligence-flow - fetch-depth: 0 - path: intelligence-flow - - name: Close superseded sync PRs + go-version-file: boatstack/go.mod + cache-dependency-path: boatstack/go.sum + - name: Require an append-only release message + if: github.event_name == 'pull_request' + run: >- + python .github/scripts/release_notes.py check-policy + --repo . + --base "${{ github.event.pull_request.base.sha }}" + --head "${{ github.event.pull_request.head.sha }}" + - name: Verify repository contract env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - shell: bash - run: | - merged="$(jq -r '.source.commit' UPSTREAM.json)" - if [[ -z "$merged" || "$merged" == "null" ]]; then - echo "No recorded source commit; skipping supersession cleanup." - exit 0 - fi - gh pr list --state open --json number,headRefName \ - --jq '.[] | select(.headRefName | startswith("sync/intelligence-flow-")) | [.number, .headRefName] | @tsv' \ - | while IFS=$'\t' read -r number branch; do - short="${branch#sync/intelligence-flow-}" - # Skip the sync that just merged (its own branch), not a supersession. - [[ "${merged:0:12}" == "$short" ]] && continue - # Close only PRs whose source is an ancestor of the merged source — - # i.e. already included. A newer, not-yet-merged sync (descendant) is - # left untouched. - if git -C intelligence-flow merge-base --is-ancestor "$short" "$merged" 2>/dev/null; then - echo "Closing superseded sync PR #$number ($short)." - gh pr close "$number" --comment "Superseded by the sync at ${merged:0:12}, which already includes this PR's source ($short). Closing the stale sync PR automatically." - fi - done + PYTHONUTF8: "1" + run: python -m unittest discover -s .github/tests -p 'test_*.py' -v diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e722151..e36fd034 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,8 +35,8 @@ jobs: goarch: arm64 asset: boatstack-helper_windows_arm64.exe steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 with: go-version-file: boatstack/go.mod cache-dependency-path: boatstack/go.mod @@ -49,7 +49,7 @@ jobs: ASSET: ${{ matrix.asset }} VERSION: ${{ github.ref_name }} run: | - source_commit="$(jq -r '.source.commit' UPSTREAM.json)" + source_commit="$(git rev-parse HEAD)" mkdir -p dist cd boatstack go build -trimpath \ @@ -67,7 +67,7 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: actions/download-artifact@v4 diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml deleted file mode 100644 index 2366bb4a..00000000 --- a/.github/workflows/sync-upstream.yml +++ /dev/null @@ -1,134 +0,0 @@ -# Generated by labkit (python -m labkit gen). Do not edit by hand. -# Edit labs//publish.config.json and regenerate; drift fails `labkit doctor`. -# Install into operatorstack/boatstack at .github/workflows/sync-upstream.yml (bootstrap step). -name: Sync from Intelligence Flow - -on: - schedule: - - cron: "2 */6 * * *" - workflow_dispatch: - inputs: - source_commit: - description: Exact Intelligence Flow commit to project (defaults to main) - required: false - type: string - -permissions: - contents: write - pull-requests: write - -concurrency: - group: sync-intelligence-flow - cancel-in-progress: false - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - name: Create Operator Stack Publisher token - id: app-token - uses: actions/create-github-app-token@v3 - with: - client-id: ${{ vars.BOATSTACK_APP_CLIENT_ID }} - private-key: ${{ secrets.BOATSTACK_APP_PRIVATE_KEY }} - owner: operatorstack - repositories: | - intelligence-flow - boatstack - permission-contents: write - permission-pull-requests: write - - name: Check out Boatstack - uses: actions/checkout@v4 - with: - path: public-repo - token: ${{ steps.app-token.outputs.token }} - - name: Check out Intelligence Flow - uses: actions/checkout@v4 - with: - repository: operatorstack/intelligence-flow - ref: ${{ inputs.source_commit || 'main' }} - fetch-depth: 0 - path: intelligence-flow - token: ${{ steps.app-token.outputs.token }} - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install labkit - shell: bash - run: python3 -m pip install --quiet ./intelligence-flow/labkit - - name: Generate projection - id: generate - shell: bash - run: | - source_commit="$(git -C intelligence-flow log -1 --format=%H -- labs/12-product-engineering-loop)" - current_commit="$(jq -r '.source.commit // empty' public-repo/UPSTREAM.json 2>/dev/null || echo '')" - if [[ -n "$current_commit" ]] && - ! git -C intelligence-flow merge-base --is-ancestor "$current_commit" "$source_commit"; then - echo "Ignoring stale request; Boatstack already records $current_commit." - echo "stale=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - python3 -m labkit project \ - --config intelligence-flow/labs/12-product-engineering-loop/publish.config.json \ - --repo public-repo \ - --source-commit "$source_commit" \ - --write - echo "source_commit=$source_commit" >> "$GITHUB_OUTPUT" - echo "stale=false" >> "$GITHUB_OUTPUT" - - name: Open generated pull request - if: steps.generate.outputs.stale != 'true' - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - SOURCE_COMMIT: ${{ steps.generate.outputs.source_commit }} - shell: bash - run: | - cd public-repo - if [[ -z "$(git status --porcelain)" ]]; then - echo "Boatstack already matches Intelligence Flow." - exit 0 - fi - git add -A - rewritten=() - while IFS= read -r note; do rewritten+=("$note"); done < <( - git diff --cached --name-only --diff-filter=MD --no-renames -- 'release-notes/*.md') - if (( ${#rewritten[@]} > 0 )); then - echo "BLOCKED: Boatstack release notes are append-only:" >&2 - printf ' %s\n' "${rewritten[@]}" >&2 - exit 1 - fi - added=() - while IFS= read -r note; do added+=("$note"); done < <( - git diff --cached --name-only --diff-filter=A --no-renames -- 'release-notes/*.md' | LC_ALL=C sort) - if (( ${#added[@]} == 0 )); then - echo "BLOCKED: projected changes require a release note in release-notes/." >&2 - exit 1 - fi - body_file="$(mktemp)" - { - echo "## What this sync releases"; echo - for note in "${added[@]}"; do cat "$note"; echo; done - echo "
Projection provenance"; echo - echo "Generated from \`operatorstack/intelligence-flow@$SOURCE_COMMIT\`." - echo "Review provenance, tests, and examples before merging."; echo - echo "
" - } > "$body_file" - short="${SOURCE_COMMIT:0:12}" - branch="sync/intelligence-flow-$short" - existing="$(gh pr list --head "$branch" --state open --json url --jq '.[0].url')" - git config user.name "${{ steps.app-token.outputs.app-slug }}[bot]" - git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com" - git switch -c "$branch" - git commit -m "Sync Boatstack from Intelligence Flow Labs @ $short" - git push --force --set-upstream origin "$branch" - if [[ -z "$existing" ]]; then - pr_url="$(gh pr create --base main --head "$branch" \ - --title "Sync Boatstack from Intelligence Flow Labs @ $short" \ - --body-file "$body_file")" - echo "Opened generated PR: $pr_url" - else - pr_url="$existing" - echo "Updated existing PR: $existing" - fi - gh pr merge "$pr_url" --auto --squash - echo "Native auto-merge requested; branch protection owns merge eligibility." diff --git a/.gitignore b/.gitignore index 8e893ad0..45341097 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ dist/ .DS_Store .venv/ venv/ +__pycache__/ +*.py[cod] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 951e2a41..663b5743 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,18 +1,16 @@ - - # Contributing -Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/4f00d6d7338b12116fd5757238dd45a2ae344237/labs/12-product-engineering-loop). +Boatstack is developed directly in this repository. Propose runtime, workflow, documentation, test, and presentation changes here. -The Boatstack repository receives product/runtime changes through a generated pull request. Review the PR's `UPSTREAM.json`, tests, adapter diff, and context-size change; do not hand-edit generated output on `main`. `.github/workflows` is the exception: it is Boatstack's executable control plane, excluded from scheduled projection and changed only through a separate manually reviewed Boatstack PR. +Every pull request must pass the cross-platform runtime checks and the repository contract. Review tests, adapter changes, public claims, and context-size changes with the product diff. -Repository-specific examples and outcome reports can be proposed upstream as new evidence. A failure becomes a durable move only after its mechanism and non-regression gate are documented. +Repository-specific examples and outcome reports can be proposed here as new evidence. A failure becomes a durable move only after its mechanism and non-regression gate are documented. ## Public-facing changes Any user-facing upgrade must state the user problem, supporting observation or requirement, current evidence status, and the README or guide it changes. If no public document changes, explain why the behavior is internal. Material public claims must appear in `docs/public-claims.json` and link to a readable explanation. -Every Intelligence Flow change that touches the Boatstack lab must add one release-level Markdown fragment under `labs/12-product-engineering-loop/boatstack-distribution/release-notes/`. Name it `YYYY-MM-DD-.md`, begin with a level-three heading, and describe user impact rather than commits, diffs, or test commands. Fragments are append-only after merge; publish a new correction fragment instead of rewriting history. +Every Boatstack pull request must add one release-level Markdown fragment under `release-notes/`. Name it `YYYY-MM-DD-.md`, begin with a level-three heading, and describe user impact rather than commits, diffs, or test commands. Fragments are append-only after merge; publish a new correction fragment instead of rewriting history. Write each fragment in Simplified Technical English, the same standard the README follows. Keep sentences short. Use the active voice and the present tense. State one idea per sentence, put the condition first, and choose the simple, common word. Write for a reader who translates or skims the note. diff --git a/UPSTREAM.json b/IMPORT_PROVENANCE.json similarity index 100% rename from UPSTREAM.json rename to IMPORT_PROVENANCE.json diff --git a/README.md b/README.md index 7646a190..dfcf4acf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ - -

Boatstack stacked-bar mark

@@ -214,4 +212,4 @@ The installer previews generated paths, verifies the platform helper, offers opt Boatstack is an open-source research prototype. Its workflow and enforcement behavior are tested. But the current record does not prove improved delivery success. The next evaluation is a paired feature benchmark with the same model, task, and budget. -Exact Intelligence Flow provenance and generated file hashes are recorded in [`UPSTREAM.json`](UPSTREAM.json). +Boatstack is developed directly in this repository. The immutable provenance of the final historical import is recorded in [`IMPORT_PROVENANCE.json`](IMPORT_PROVENANCE.json). diff --git a/boatstack/AGENTS.md b/boatstack/AGENTS.md index 264c9e4e..edecea1b 100644 --- a/boatstack/AGENTS.md +++ b/boatstack/AGENTS.md @@ -1,14 +1,12 @@ -# Agent guide — Boatstack product-engineering-loop +# Agent guide — Boatstack runtime -Read this before opening a PR that touches anything under -`labs/12-product-engineering-loop/`. +Read this before opening a PR that touches anything under `boatstack/`. -## Every PR that changes this lab REQUIRES a new release note +## Every Boatstack PR REQUIRES a new release note -CI runs `scripts/release_notes.py check-policy` (the **Generated distribution** -check). It fails the PR if the diff touches **any** file under -`labs/12-product-engineering-loop/` — source, tests, docs, scripts, anything — -without **adding** a new release-note fragment. This check is **not** part of +CI runs `.github/scripts/release_notes.py check-policy` in the +`repository-contract` job. It fails any PR without **adding** a new release-note +fragment. This check is **not** part of `go test ./...`, so a green local test run does **not** mean you are done. Skipping the note costs a full CI round trip (fail → add note → push → re-run). @@ -17,7 +15,7 @@ the note costs a full CI round trip (fail → add note → push → re-run). Create one new file per PR: ``` -labs/12-product-engineering-loop/boatstack-distribution/release-notes/YYYY-MM-DD-.md +release-notes/YYYY-MM-DD-.md ``` Contract (enforced by `validate_release_note`): @@ -37,28 +35,27 @@ Commit your change **and** the note, then run the same policy CI runs: ``` # format check on the notes directory -python3 labs/12-product-engineering-loop/scripts/release_notes.py \ - validate --root labs/12-product-engineering-loop/boatstack-distribution/release-notes +python3 .github/scripts/release_notes.py validate --repo . # append-only + "note present for lab changes" against origin/main (needs a clean, # committed tree — it inspects the committed PR diff, not the working tree) -python3 labs/12-product-engineering-loop/scripts/release_notes.py \ +python3 .github/scripts/release_notes.py \ preflight --repo . --base-branch main ``` `preflight` fetches `origin/main` and checks the committed diff. `PASS` means the -**Generated distribution** check will pass; `BLOCKED` prints exactly what to fix. +`repository-contract` check will pass; `BLOCKED` prints exactly what to fix. ## Other checks that are not in `go test` -- **Repository conformance** and **Runtime** (windows/macos/ubuntu) run in CI. - Locally, always run `go build ./...`, `go vet ./...`, and `go test ./...` from - `product-engineering-loop`, plus `python3 -m unittest tests.test_product_loop` - from `labs/12-product-engineering-loop` for the Python surface. +- **repository-contract** and the runtime jobs (Windows/macOS/Ubuntu) run in CI. + Locally, run `go build ./...`, `go vet ./...`, and `go test ./...` from + `boatstack/`, plus `python3 -m unittest discover -s .github/tests -p 'test_*.py'` + from the repository root. ## PR body honesty -Do not write "no release note required" for a change under this lab — a note is +Do not write "no release note required" for a Boatstack change — a note is always required. State which note you added. ## Boundary Conformance Requirement diff --git a/boatstack/BUG-worktree-delivery-state.md b/boatstack/BUG-worktree-delivery-state.md index 9dd68427..8c38c4ec 100644 --- a/boatstack/BUG-worktree-delivery-state.md +++ b/boatstack/BUG-worktree-delivery-state.md @@ -1,6 +1,6 @@ # Bug: shipped features re-register as ambiguous plan candidates from worktrees -**Component:** `boatstack-helper` (labs/12-product-engineering-loop/product-engineering-loop) +**Component:** `boatstack-helper` (`boatstack/`) **Observed in:** v0.7.45 (commit 91e33a95) **Severity:** blocks `next-status` (BLOCKED / AMBIGUOUS) for any repo that has shipped >1 feature and uses worktree delivery mode. **Goal of fix:** stop miscounting shipped features as unshipped, WITHOUT deleting the committed diff --git a/boatstack/references/portability.md b/boatstack/references/portability.md index 2d7f360e..3d9ce2ef 100644 --- a/boatstack/references/portability.md +++ b/boatstack/references/portability.md @@ -14,10 +14,10 @@ The source of truth is `.product-loop/`: Host-specific files are compiled adapters: -- Cursor: `.cursor/rules/product-engineering-loop.mdc` and `.cursor/commands/*.md`; -- Claude Code: `.claude/skills/product-engineering-loop/SKILL.md`; -- Codex: `.agents/skills/product-engineering-loop/SKILL.md`; -- GitHub: `.github/PULL_REQUEST_TEMPLATE/product-engineering-loop.md`. +- Cursor: `.cursor/rules/boatstack.mdc` and `.cursor/commands/*.md`; +- Claude Code: `.claude/skills/boatstack/SKILL.md`; +- Codex: `.agents/skills/boatstack/SKILL.md`; +- GitHub: `.github/PULL_REQUEST_TEMPLATE/boatstack.md`. Adapters point to the canonical package; they do not copy its full reasoning. This keeps behavior consistent while letting each host expose its native invocation surface. diff --git a/boatstack/release.go b/boatstack/release.go index 5c34ced9..16d7d57a 100644 --- a/boatstack/release.go +++ b/boatstack/release.go @@ -12,8 +12,8 @@ import ( var stableReleaseVersion = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)$`) -// ReleaseClassification separates a projected documentation sync from a -// change that alters the installed Boatstack delivery harness. +// ReleaseClassification separates a documentation-only change from a change +// that alters the installed Boatstack delivery harness. type ReleaseClassification struct { Required bool Paths []string @@ -29,7 +29,7 @@ func isReleaseBearingPath(value string) bool { return false } for _, exact := range []string{ - ".gitignore", "CONTRIBUTING.md", "README.md", "UPSTREAM.json", + ".gitignore", "CONTRIBUTING.md", "README.md", "IMPORT_PROVENANCE.json", "project.example.json", } { if path == exact { @@ -66,7 +66,7 @@ func ClassifyReleasePaths(paths []string) ReleaseClassification { return ReleaseClassification{Required: len(releasePaths) > 0, Paths: releasePaths} } -// ClassifyReleaseDiff reads the exact projected Git diff used by the release +// ClassifyReleaseDiff reads the exact Boatstack Git diff used by the release // workflow and applies the same deterministic path policy as unit tests. func ClassifyReleaseDiff(repo, base, head string) (ReleaseClassification, error) { if strings.TrimSpace(base) == "" || strings.TrimSpace(head) == "" { diff --git a/boatstack/release_test.go b/boatstack/release_test.go index 3aff5df8..49d737b4 100644 --- a/boatstack/release_test.go +++ b/boatstack/release_test.go @@ -10,7 +10,7 @@ import ( func TestClassifyReleasePaths(t *testing.T) { documentation := []string{ "README.md", "docs/getting-started.md", "assets/boatstack-mark.svg", - "release-notes/2026-07-18-copy.md", "UPSTREAM.json", + "release-notes/2026-07-18-copy.md", "IMPORT_PROVENANCE.json", "boatstack/export_test.go", "boatstack/testdata/example.txt", ".github/workflows/sync-upstream.yml", "automation/release-policy.md", } diff --git a/docs/account-recovery-walkthrough.md b/docs/account-recovery-walkthrough.md index 6173b698..449d88f7 100644 --- a/docs/account-recovery-walkthrough.md +++ b/docs/account-recovery-walkthrough.md @@ -1,5 +1,3 @@ - - # Example: account recovery in a passwordless product **For:** someone who wants to see why Boatstack asks questions before code. diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 6ff1f7af..1c9faea9 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -1,5 +1,3 @@ - - # Evidence-engineered coding Boatstack is a mathematically modeled coding node, not a prescribed loop. It leaves implementation open and makes authority, evidence, and accepted outcomes observable at the node boundary. @@ -146,6 +144,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`4f00d6d7338b12116fd5757238dd45a2ae344237`](https://github.com/operatorstack/intelligence-flow/tree/4f00d6d7338b12116fd5757238dd45a2ae344237/labs/12-product-engineering-loop). +The current moves were derived from an audited benchmark corpus and product-repository studies. Current implementation and verification evidence lives in this repository; the final historical import is recorded in `IMPORT_PROVENANCE.json`. The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/generated-files.md b/docs/generated-files.md index 62c656e8..b83e3163 100644 --- a/docs/generated-files.md +++ b/docs/generated-files.md @@ -1,5 +1,3 @@ - - # What Boatstack adds to a repository **For:** anyone reviewing an installation or feature PR. @@ -17,7 +15,7 @@ Boatstack creates installation state once and feature evidence repeatedly. Keepi | `.github/PULL_REQUEST_TEMPLATE/boatstack.md` | Fallback PR structure | Commit | | `.cursor/hooks.json`, `.claude/settings.json`, `.codex/hooks.json` | Boatstack fragments merged with existing host settings | Review and commit | | `.product-loop/bin/` | Verified worktree-local helper and install lock | Never commit; it is ignored and hydrates automatically | -| `release-notes/*.md` | Canonical user-facing messages reused by sync PRs and tagged releases | Generated; edit in Intelligence Flow | +| `release-notes/*.md` | Canonical user-facing messages reused by pull requests and tagged releases | Add directly in Boatstack; append-only | The installer prints the exact staging command and runs `doctor`. Put this state in `chore/install-boatstack`, review it once, and merge it before feature work. diff --git a/docs/getting-started.md b/docs/getting-started.md index 52357f8f..4197a7ae 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,5 +1,3 @@ - - # Install Boatstack and ship a first feature **For:** a product builder or engineer using Cursor, Codex, or Claude Code. diff --git a/docs/public-claims.json b/docs/public-claims.json index b7a2cd64..8c836c6c 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,5 @@ { "schema_version": 1, - "source_commit": "4f00d6d7338b12116fd5757238dd45a2ae344237", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +11,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "human-decisions", @@ -23,7 +22,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "validation-provenance", @@ -34,7 +33,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "irreversible-operations", @@ -46,7 +45,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "reviewer-ready-pr", @@ -57,7 +56,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "phase-scoped-delivery", @@ -68,7 +67,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "model-neutral-contract", @@ -79,7 +78,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "cross-model-failures", @@ -90,7 +89,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "lower-cost-outcomes", @@ -101,7 +100,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "git-worktree-activation", @@ -112,7 +111,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" }, { "id": "visible-updates", @@ -123,7 +122,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:4f00d6d7338b12116fd5757238dd45a2ae344237" + "last_verified_version": "v0.7.120" } ] } diff --git a/docs/public-surface.md b/docs/public-surface.md index 4f52a5fb..3ccc1be1 100644 --- a/docs/public-surface.md +++ b/docs/public-surface.md @@ -1,5 +1,3 @@ - - # Boatstack public-surface contract Boatstack's README is a product-builder homepage, not the complete manual. Public presentation may change freely inside this contract while the runtime remains deterministic. diff --git a/docs/research-and-design.md b/docs/research-and-design.md index d9f31520..e2215144 100644 --- a/docs/research-and-design.md +++ b/docs/research-and-design.md @@ -9,9 +9,9 @@ The proposed product is not a large prompt and not a Codex-, Cursor-, Claude-, o 3. a human approval boundary between planning and executable work; 4. a separate evidence-gated loop for improving the protocol itself. -The initial implementation is in [`product-engineering-loop/`](product-engineering-loop/). Its exporter generates Cursor rules/commands, Claude Code and Codex skills, and a GitHub PR template from one source. +The implementation is in [`boatstack/`](../boatstack/). Its exporter generates Cursor rules/commands, Claude Code and Codex skills, and a GitHub PR template from one source. -The public [Boatstack](https://github.com/operatorstack/boatstack) repository is a compiled distribution, not a second source of product/runtime truth. `scripts/build_boatstack.py` projects this package into a branded README, evidence-engineered-coding explanation, worked example, tests, and installable skill; `UPSTREAM.json` binds every projected file to its Intelligence Flow commit. The README and beginner guides remain ordinary human-authored Markdown copied byte-for-byte, while the Go helper embeds only install-time workflow references and templates. A machine-readable public claim record keeps homepage wording tied to observations, safeguards, tests, and explicit evaluation status. A Boatstack-owned scheduled workflow polls this public source and proposes content changes by PR. Boatstack's `.github/workflows` directory is a deliberately separate control-plane slice: it originates and changes in Boatstack through ordinary, manually reviewed PRs and is never emitted, owned, or removed by the Intelligence Flow projector. +The public [Boatstack](https://github.com/operatorstack/boatstack) repository is the source of product and runtime truth. The README, guides, tests, installable skill, and Go helper change together through ordinary reviewed pull requests. A machine-readable public claim record keeps homepage wording tied to observations, safeguards, tests, and explicit evaluation status. The repository contract checks these surfaces directly, while the cross-platform runtime jobs verify the helper and installers. `IMPORT_PROVENANCE.json` preserves the immutable record of the final historical import; it has no publishing authority. ## Outcome sizing and where value emerges @@ -67,8 +67,8 @@ This still allows a repository owner to choose any model or routing service. It The evidence was audited in two reproducible passes: -- [`BENCHMARK_CORPUS_AUDIT.md`](BENCHMARK_CORPUS_AUDIT.md): **3,571** historical per-trial results across 19 run/corpus groups; 3,540 signal streams; no unreadable results. -- [`BENCHMARK_SUBMISSION_2_1_AUDIT.md`](BENCHMARK_SUBMISSION_2_1_AUDIT.md): all **445** July Terminal-Bench 2.1 submission trials and all 445 signal streams. +- [`benchmark-corpus-audit.md`](benchmark-corpus-audit.md): **3,571** historical per-trial results across 19 run/corpus groups; 3,540 signal streams; no unreadable results. +- [`benchmark-submission-audit.md`](benchmark-submission-audit.md): all **445** July Terminal-Bench 2.1 submission trials and all 445 signal streams. Combined mechanical coverage is **4,016 trial results** and **3,985 signal streams**. The JSON companions preserve group-level outcomes, terminal reasons, protocol errors, timeouts, and loop-event aggregates. @@ -100,7 +100,7 @@ Those are **summary-only evidence** in this design. They are not represented as | Mid-run aggregates changed direction | Qwen board interpretation moved as task coverage deepened | Compare paired completed coverage and uncertainty, not early aggregate rank | | External failure invited destructive recovery | A sanitized partial schema apply failure led to an invented reset path before review removed it | Treat recovery authority as a deterministic boundary: preserve state, diagnose read-only, transact or fix forward | -Sources: [`RESEARCH_LOG.md`](../11-harbor-submit/RESEARCH_LOG.md), [`EXPERIMENT_GEMINI20_2026-07-15.md`](../11-harbor-submit/EXPERIMENT_GEMINI20_2026-07-15.md), [`ZERO_TO_QWEN.md`](../11-harbor-submit/ZERO_TO_QWEN.md), and [`docs/12-self-verification-fidelity.md`](../../docs/12-self-verification-fidelity.md). +Public evidence: [`benchmark-corpus-audit.md`](benchmark-corpus-audit.md) and [`benchmark-submission-audit.md`](benchmark-submission-audit.md). `IMPORT_PROVENANCE.json` records the final historical import that supplied the earlier private research references. The irreversible-operation boundary is a **PROPOSED** Move. The incident supports the target failure mechanism, while the benchmark campaign supports deterministic enforcement over stronger wording. Neither proves the new guard's net effect. Promotion requires a paired unguarded baseline, destructive and safe corpora, real host events, bounded latency, secret-free denials, and no workflow regression. diff --git a/docs/safety.md b/docs/safety.md index dc2c4ad9..d437e67a 100644 --- a/docs/safety.md +++ b/docs/safety.md @@ -1,5 +1,3 @@ - - # Irreversible-operation safety Boatstack leaves implementation open while removing high-confidence irreversible side effects from the coding agent's reachable action space. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3c37d54a..61183379 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,3 @@ - - # Troubleshooting Boatstack **For:** someone blocked during installation or a feature. diff --git a/docs/validation-and-evidence.md b/docs/validation-and-evidence.md index 2f67798a..03c7dfb2 100644 --- a/docs/validation-and-evidence.md +++ b/docs/validation-and-evidence.md @@ -1,5 +1,3 @@ - - # Validation and evidence ## External-write safety evidence diff --git a/docs/why-these-steps.md b/docs/why-these-steps.md index 082580f1..af3c6c03 100644 --- a/docs/why-these-steps.md +++ b/docs/why-these-steps.md @@ -1,5 +1,3 @@ - - # Why Boatstack has these steps **For:** anyone who wants to see the work behind Boatstack's safeguards. @@ -19,7 +17,7 @@ Those labels prevent an implementation test from being presented as proof that t **What Boatstack does.** Cursor, Codex, and Claude Code receive adapters for the same path from planning through PR preparation. The durable state behind that path—source plan, specification, human answers, accepted gaps, approval, evidence, and review findings—lives in the repository instead of belonging to one model or chat session. Models and skills may contribute work without changing the completion requirements. -**How we check it.** Export tests verify that all supported host adapters expose the same lifecycle and reference the same canonical repository artifact contract. Projection tests verify that the public workflow, adapters, and artifact definitions are generated from one upstream source. +**How we check it.** Export tests verify that all supported host adapters expose the same lifecycle and reference the same canonical repository artifact contract. Repository-contract tests verify the public workflow, adapters, and artifact definitions directly in Boatstack. **What it does not mean.** Boatstack does not copy private chat history or move a command already in progress between agents. Portability covers the workflow and saved repository state available at the next transition. @@ -81,7 +79,7 @@ Those labels prevent an implementation test from being presented as proof that t **What Boatstack does.** Boatstack keeps one planning, approval, validation, review, and shipping contract across models. It reacts to observable conditions such as unanswered decisions, risk, reversibility, tool outcomes, convergence, and evidence. This lets a repository owner choose a lower-cost, general, or frontier model without silently changing what “ready” means. -**How we check it.** Export and projection tests verify the same workflow and gate vocabulary across supported coding hosts. The benchmark audits preserve the recorded trial coverage and the research record traces the model-dependent bottlenecks and failure mechanisms behind this design. +**How we check it.** Export and repository-contract tests verify the same workflow and gate vocabulary across supported coding hosts. The benchmark audits preserve the recorded trial coverage and the research record traces the model-dependent bottlenecks and failure mechanisms behind this design. **Status:** the model-neutral contract is verified and the cross-model failure patterns are observed. Whether Boatstack improves correctness, cost, or delivery time for lower-cost models is **still being evaluated**; it is not a claim that models perform equally. diff --git a/install.ps1 b/install.ps1 index e49ac616..3a81555d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,4 +1,4 @@ -# Generated from operatorstack/intelligence-flow. +# Boatstack installer maintained in operatorstack/boatstack. [CmdletBinding()] param( [switch]$Repair, diff --git a/install.sh b/install.sh index 90a4cca7..1433cd8d 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Generated from operatorstack/intelligence-flow. +# Boatstack installer maintained in operatorstack/boatstack. set -euo pipefail repository="operatorstack/boatstack" diff --git a/release-notes/2026-08-09-independent-repository.md b/release-notes/2026-08-09-independent-repository.md new file mode 100644 index 00000000..70b53284 --- /dev/null +++ b/release-notes/2026-08-09-independent-repository.md @@ -0,0 +1,3 @@ +### Boatstack now develops and verifies changes in its own repository + +Boatstack no longer synchronizes product changes from another repository. Direct pull requests now run the cross-platform runtime suite, repository contract, release-note policy, and end-to-end detached supervision checks before merge. Verified runtime changes on `main` continue to publish patch releases.