Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions .github/scripts/ci-test-sharding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
124 changes: 124 additions & 0 deletions .github/scripts/release_notes.py
Original file line number Diff line number Diff line change
@@ -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-<slug>.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())
234 changes: 234 additions & 0 deletions .github/tests/test_detached_supervision.py
Original file line number Diff line number Diff line change
@@ -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()
Loading