From d04b7a667cef5477ba7c783f2e27687da1d119d4 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 29 Oct 2026 13:31:53 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(cortex):=20birth=20=E2=80=94=20registe?= =?UTF-8?q?r=20a=20scaffolded=20Science=20project=20in=20one=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cortex birth --category active|development --assistant A --summary S` derives the projects.yaml row from /// (remote from git origin, ral_root from the rows' RAL base, sync_verbs from the hpc/sync dispatch case, the ledger file) and prints it. --apply cuts claude/birth- from origin/main, writes through the Cortex's own `cortex.py birth`, commits, pushes only when `ledger_merge.py classify --base origin/main` calls it a birth, and returns to main; any failed write or commit abandons the branch and leaves main clean (exit 1). Scaffolding stays with the assistant's start-new-project; the /cortex skill gains "Start a new project". Refs PyAutoLabs/PyAutoBrain#545 Co-Authored-By: Claude Opus 5.5 --- agents/conductors/cortex/AGENTS.md | 1 + agents/conductors/cortex/_cortex.py | 238 ++++++++++++++++++++++++++++ skills/cortex/cortex.md | 25 +++ tests/test_cortex_conductor.py | 172 ++++++++++++++++++++ 4 files changed, 436 insertions(+) diff --git a/agents/conductors/cortex/AGENTS.md b/agents/conductors/cortex/AGENTS.md index 03468f74..e64a8b8b 100644 --- a/agents/conductors/cortex/AGENTS.md +++ b/agents/conductors/cortex/AGENTS.md @@ -31,6 +31,7 @@ note`). | `checkin [--dry-run \| --apply] [--push \| --no-push] [--project KEY]` | **Where is my science?** | the door — the sequence below, ending in a summary keyed by project; reaches no cluster; exit **1** when the tree does not check | | `census [--json]` | What is the Cortex holding? | per project: status, summary, runs by state, log length, last update; totals (active projects, runs open, runs running); the check-in stamp; the problems `cortex.py check` would report | | `drift [--science-root PATH] [--json]` | **Does the register match the Science tree?** | the laptop Science tree (`//`, root derived from the rows' `local_path`s) against `projects.yaml`: errors — a folder in `active/` or `development/` with no row, a non-retired row whose folder is gone, a folder whose category disagrees with its status, a derived root that does not exist while other rows' folders do (the tree moved); warnings — an unregistered folder in `complete/` or `archive/`, a `ral_root` in a different category. Writes nothing. Exit **1** on an error · 0 otherwise, including off the laptop (a one-line skip). `census` and `checkin` print its one-line summary; `census()` itself stays drift-free so the CI board is machine-independent | +| `birth --category active\|development --assistant A --summary S [--apply] [--no-push]` | **Register a scaffolded project** | derives the `projects.yaml` row from `///` (remote from its git origin, `ral_root` = the rows' RAL base + category + key, `sync_verbs` from its `hpc/sync` dispatch `case`, the ledger file) and prints it. `--apply` (needs the Cortex clean on `main`): cut `claude/birth-` from `origin/main`, write through `scripts/cortex.py birth`, commit, push only when `ledger_merge.py classify --base origin/main` calls it a birth, return to `main`. Scaffolding is the assistant's `start-new-project`, never this verb; it creates no remote and reaches no cluster. Exit **2** refused · **1** git or write failed | | `dashboard --check` | Are the committed pages current? | exit 0 current · **1 stale** · 2 no checkout · 3 unreadable tree | | `dashboard --apply` | — | writes `dashboard.md` + `dashboard.html` + `state.json` — the organ-cockpit feed (`board/_state.py` v1): red on `cortex.py check` problems, yellow while a run is running or the check-in is stale (the feed's 180-min rule, judged at render), items carry `checkin_payload` / `resume_payload`; `--check` ignores its `updated` | | `issue [--project KEY] [--apply]` | What sits at the top of each project's issue? | the fenced ledger block (`cortex.py`'s `issue_block`) per project with an `Issue:`; `--apply` writes it into the issue body through `gh` (replacing the block between the markers, or prepending it); never creates an issue; exit **1** without `gh` | diff --git a/agents/conductors/cortex/_cortex.py b/agents/conductors/cortex/_cortex.py index a2c411a7..02a58f78 100644 --- a/agents/conductors/cortex/_cortex.py +++ b/agents/conductors/cortex/_cortex.py @@ -35,6 +35,9 @@ [--dry-run|--apply] [--push|--no-push] [--project KEY]` (the door, reaches no cluster) · `census [--json]` · `drift [--science-root PATH] [--json]` (the laptop Science tree against `projects.yaml`; reads, never writes) · +`birth --category active|development --assistant A --summary S +[--apply] [--no-push]` (a scaffolded project's row and ledger, written by +the Cortex's own `cortex.py birth` on `claude/birth-`) · `dashboard --check|--apply` · `issue [--project KEY] [--apply]`. Exit codes: 0 ok · 1 dashboard drift (the `dashboard_refresh.yml` contract), @@ -1239,6 +1242,221 @@ def cmd_drift(root: Path, mod, a) -> int: return RC_DRIFT if report["errors"] else RC_OK +# ------------------------------------------------------------ the birth --- +# Scaffolding belongs to the domain assistant (`start-new-project`); the +# birth is the Cortex half — a row and a ledger for a project folder that +# already exists. The conductor derives the row and hands it to the Cortex's +# own `scripts/cortex.py birth`, the verb that writes; it writes nothing of +# its own here but a git branch. +BIRTH_BRANCH_PREFIX = "claude/birth-" +BIRTH_CATEGORIES = ("active", "development") +_DISPATCH_RE = re.compile(r'^case "\$\{?cmd\}?" in$(.*?)^esac$', re.M | re.S) +_LABEL_RE = re.compile(r"^\s+([a-z][a-z0-9-]*)(?:\|[^)\n]*)?\)", re.M) +_GITHUB_RE = re.compile(r"github\.com[:/]([\w.-]+/[\w.-]+?)(?:\.git)?/?$") +LEDGER_CANDIDATES = ("wiki/project/state.md", "README.md", "AGENTS.md") + + +def sync_verbs_of(cli: Path) -> list[str]: + """The verbs a project's sync CLI dispatches: the labels of its + `case "$cmd" in … esac` block, in order — `help` aside, which is the + CLI's usage, not a verb a conductor runs. Empty when there is none.""" + if not cli.is_file(): + return [] + m = _DISPATCH_RE.search(cli.read_text(encoding="utf-8", errors="replace")) + verbs: list[str] = [] + for verb in _LABEL_RE.findall(m.group(1)) if m else []: + if verb != "help" and verb not in verbs: + verbs.append(verb) + return verbs + + +def remote_of(local: Path) -> str: + """`owner/repo` of the project's GitHub `origin`, else `none`.""" + r = _git(local, "remote", "get-url", "origin") + m = _GITHUB_RE.search(r.stdout.strip()) if r.returncode == 0 else None + return m.group(1) if m else "none" + + +def ral_base_from(projects: dict) -> str | None: + """The RAL base the rows agree on: the most common `` of the + non-retired `ral_root`s shaped `//`.""" + bases = [str(Path(ral).parent.parent) for row in projects.values() + if row.get("status") != "retired" + for ral in [_norm(row.get("ral_root", ""))] + if ral and Path(ral).parent.name in SCIENCE_CATEGORIES] + return max(sorted(set(bases)), key=bases.count) if bases else None + + +def birth_row(projects: dict, key: str, category: str, assistant: str, *, + science_root: str | None = None, partition: str = "ral", + sync_verbs: list | None = None) -> tuple[dict, list[str]]: + """`(row, problems)` — the `projects.yaml` row a birth would write for + `//`, derived from the folder itself and from + the other rows. Any problem refuses the birth.""" + problems: list[str] = [] + root = science_root or science_root_from(projects) + if category not in BIRTH_CATEGORIES: + problems.append(f"a project is born into {' or '.join(BIRTH_CATEGORIES)}/, " + f"not {category}/") + if not root: + return {}, problems + ["no row places a project in a Science category, so " + "the Science root is unknown — pass --science-root"] + local = Path(_norm(root)) / category / key + if key in projects: + problems.append(f"{key} is already a projects.yaml key") + if any(_real(local_root(r)) == _real(str(local)) for r in projects.values()): + problems.append(f"{local} is already some row's local_path") + if not local.is_dir(): + problems.append(f"{local} does not exist — scaffold it first (the " + "assistant's start-new-project)") + verbs = list(sync_verbs or sync_verbs_of(local / "hpc" / "sync")) + if local.is_dir() and not verbs: + problems.append(f"no verbs found in {local}/hpc/sync (its `case \"$cmd\" " + "in` block) — scaffold hpc/ or pass --sync-verbs") + base = ral_base_from(projects) + if not base: + problems.append("no row's ral_root is shaped //, " + "so the RAL base is unknown") + ledger = next((c for c in LEDGER_CANDIDATES if (local / c).is_file()), + LEDGER_CANDIDATES[0]) + row = { + "remote": remote_of(local) if local.is_dir() else "none", + "local_path": str(local), + "ral_root": f"{base}/{category}/{key}" if base else "", + "mirror": "none", + "sync_cli": "hpc/sync", + "sync_verbs": verbs, + "ledger": ledger, + "assistant": assistant, + "partition": partition, + "status": "active", + } + return row, problems + + +def _birth_argv(root: Path, key: str, row: dict, a) -> list[str]: + argv = [sys.executable, str(root / "scripts" / "cortex.py"), "birth", key, + "--root", str(root), "--summary", a.summary, "--issue", a.issue, + "--sync-verbs", ",".join(row["sync_verbs"])] + for field in ("local_path", "ral_root", "remote", "mirror", "sync_cli", + "ledger", "assistant", "partition", "status"): + argv += [f"--{field.replace('_', '-')}", row[field]] + if a.note: + argv += ["--note", a.note] + return argv + + +def _abandon_birth(root: Path, branch: str) -> None: + """Leave the Cortex as the birth found it: nothing staged or written, + back on `main`, the birth branch gone.""" + _git(root, "reset", "-q") + _git(root, "checkout", "--", ".") + _git(root, "clean", "-fdq", "--", "projects") + _git(root, "checkout", "main") + _git(root, "branch", "-D", branch) + + +def cmd_birth(root: Path, mod, a) -> int: + """Register a scaffolded Science project: a row and a ledger, on + `claude/birth-`. Dry run by default; never creates a remote and + never reaches RAL.""" + projects, problems = mod.load_projects(root) + if problems: + print("cortex birth: projects.yaml does not check — fix it first", file=sys.stderr) + return RC_DRIFT + row, refused = birth_row(projects, a.key, a.category, a.assistant, + science_root=a.science_root or None, + partition=a.partition, + sync_verbs=[v for v in a.sync_verbs.split(",") if v] or None) + print(f"== Cortex birth — {a.key} ({'apply' if a.apply else 'dry run'}) ==") + for field, value in row.items(): + shown = f"[{', '.join(value)}]" if isinstance(value, list) else value + print(f" {field + ':':<14} {shown}") + words = len(a.summary.split()) + if words > mod.SUMMARY_MAX_WORDS: + refused.append(f"--summary is {words} words; the ledger title takes at most " + f"{mod.SUMMARY_MAX_WORDS}") + if refused: + for p in refused: + print(f" cannot: {p}") + return RC_USAGE + if not a.apply: + print(f"\nwould cut {BIRTH_BRANCH_PREFIX}{a.key} from origin/main, write this row " + f"and projects/{a.key}.md through scripts/cortex.py birth, and " + f"{'push it' if a.push else 'leave it committed locally'}. " + "Run with --apply to do it.") + return RC_OK + + ok, why = push_preflight(root) + if not ok: + print(f"cortex birth: REFUSED — {why}; a birth is cut from a clean `main`", + file=sys.stderr) + return RC_DRIFT + branch = f"{BIRTH_BRANCH_PREFIX}{a.key}" + mine: list[dict] = [] + for step in (("fetch", "origin"), ("checkout", "-b", branch, "origin/main")): + r = _git(root, *step) + if r.returncode != 0: + print(f"cortex birth: REFUSED — `git {' '.join(step)}`: " + f"{r.stderr.strip()[:200]}", file=sys.stderr) + _git(root, "checkout", "main") + return RC_DRIFT + rc = RC_OK + try: + r = subprocess.run(_birth_argv(root, a.key, row, a), capture_output=True, text=True) + if r.returncode != 0: + print((r.stderr or r.stdout).strip(), file=sys.stderr) + _abandon_birth(root, branch) + return RC_DRIFT + print(r.stdout.strip()) + for step in (("add", "--", "projects.yaml", f"projects/{a.key}.md"), + ("commit", "-m", f"cortex: birth of {a.key}\n\n{a.summary}\n")): + g = _git(root, *step) + if g.returncode != 0: + print(f"cortex birth: `git {step[0]}` failed — nothing committed, " + f"the branch is dropped: {(g.stderr or g.stdout).strip()[:200]}", + file=sys.stderr) + _abandon_birth(root, branch) + return RC_DRIFT + # The path-only classifier calls projects.yaml code; against a base it + # recognises an add-only row-plus-ledger diff as a birth. + birth = subprocess.run( + [sys.executable, str(root / "scripts" / "ledger_merge.py"), "classify", + "--base", "origin/main"], cwd=str(root), capture_output=True, text=True + ).returncode == 0 + mine = [f for f in drift_report(mod.load_projects(root)[0], + a.science_root or None)["findings"] + if f["key"] == a.key or f["path"] == row["local_path"]] + if not birth: + rc = RC_DRIFT + print(f"push: REFUSED — the classifier does not call this a birth; " + f"`{branch}` is committed locally for a human") + elif a.push: + pushed = _git(root, "push", "-u", "origin", branch) + if pushed.returncode != 0: + rc = RC_DRIFT + print(f"push: `{branch}` pushed — `ledger_merge.yml` lands a birth by itself" + if pushed.returncode == 0 else + f"push: FAILED — {pushed.stderr.strip()[:200]}; the commit is on `{branch}`") + else: + print(f"push: off — `{branch}` is committed locally") + finally: + _git(root, "checkout", "main") + + print(drift_line({"science_root": row["local_path"], "present": True, + "findings": mine, "errors": sum(f["severity"] == "error" for f in mine), + "warnings": sum(f["severity"] == "warning" for f in mine)})) + print("\nNext, by hand:") + if row["remote"] == "none": + owners = [r["remote"].split("/")[0] for r in projects.values() + if "/" in (r.get("remote") or "")] + owner = max(sorted(set(owners)), key=owners.count) if owners else "" + print(f" gh repo create {owner}/{a.key} --private --source {row['local_path']}") + print(f" cd {row['local_path']} && hpc/sync push # the RAL tree, {row['ral_root']}") + print(f" git -C {root} pull # once the birth has landed on main") + return rc + + def run_jobs(row: dict) -> str | None: """The project's own ` jobs`, verbatim — stdout and stderr as they came, no parsing. None when the row has no `jobs` verb.""" @@ -1725,6 +1943,24 @@ def common(p): "local_path)") r.add_argument("--json", dest="as_json", action="store_true") + b = common(sub.add_parser( + "birth", help="register a scaffolded Science project: its projects.yaml " + "row and ledger on claude/birth- (dry run by default)")) + b.add_argument("key", help="the project key = its folder name") + b.add_argument("--category", required=True, choices=BIRTH_CATEGORIES) + b.add_argument("--assistant", required=True, + help="the domain assistant it was scaffolded from, or none") + b.add_argument("--summary", required=True, help="the ledger title, one line") + b.add_argument("--issue", default="none", help="Repo#N or an issue URL") + b.add_argument("--note", default="", help="a free-text note for the row") + b.add_argument("--partition", default="ral", choices=("ral", "gpu", "both")) + b.add_argument("--sync-verbs", dest="sync_verbs", default="", + help="comma-separated; default: read from the project's hpc/sync") + b.add_argument("--science-root", dest="science_root", default="") + b.add_argument("--apply", action="store_true", help="cut the branch and write") + b.add_argument("--no-push", dest="push", action="store_false", + help="commit on the branch but do not push") + i = common(sub.add_parser( "issue", help="the ledger block that sits at the top of each " "project's issue; --apply writes it there with gh")) @@ -1757,6 +1993,8 @@ def main(argv=None) -> int: return cmd_issue(root, mod, a) if verb == "drift": return cmd_drift(root, mod, a) + if verb == "birth": + return cmd_birth(root, mod, a) c = census(root) except mod.CortexError as e: print(f"cortex: {root}: {e}", file=sys.stderr) diff --git a/skills/cortex/cortex.md b/skills/cortex/cortex.md index 71c85834..bc78a27e 100644 --- a/skills/cortex/cortex.md +++ b/skills/cortex/cortex.md @@ -108,6 +108,29 @@ nothing until they say.** Work on the project itself is delegated the way [`../WORKFLOW.md`](../WORKFLOW.md) "Cortex project work" says; the resume is the entry protocol, not the work. +### 6. Start a new project + +A new Science project is born in two halves, in this order: + +1. **Scaffold** — through the domain assistant the project will work with + (`autolens_assistant` or `autofit_assistant`): run its + `skills/start-new-project.md` (Create phase) with the target folder + `///` (`development/` for a project + whose purpose is developing or validating the software). Take data from the + Science tree: copy a sample from `dataset//`, or run a standard lens + from `simulators//.py --dataset-path dataset` inside the new + folder. The assistant owns the scaffold — never hand-copy an older project. +2. **Register** — `pyauto-brain cortex birth --category + --assistant --summary ""`. The dry run prints the row it + derived from the folder (remote, `ral_root` in the same category, the + `hpc/sync` verbs, the ledger file); show it to the human, then `--apply`: + it cuts `claude/birth-`, writes the row and ledger through + `scripts/cortex.py birth`, and pushes when the Cortex's classifier calls it + a birth (which lands by itself). It creates no GitHub remote and reaches no + cluster — it prints those lines for the human. + +`pyauto-brain cortex drift` should then be clean for the new project. + ## The push rule `--push` is allowed when **the Cortex checkout is clean on `main` with a @@ -166,4 +189,6 @@ checkout. | `dashboard --check` | Are the committed pages current? Exit **1** = stale (the refresh workflow's contract) | | `dashboard --apply` | Regenerate `dashboard.md` + `dashboard.html` — never hand-edit those two | | `issue [--project KEY] [--apply]` | The ledger block at the top of each project's issue (Now, Runs, the last five). `--apply` writes it there through `gh`; never creates an issue; exit **1** without `gh` | +| `drift [--science-root PATH] [--json]` | Does the register match the laptop Science tree? Errors: unregistered `active/`/`development/` folders, vanished rows, category vs status. Reads, never writes; exit **1** on an error | +| `birth --category C --assistant A --summary S [--apply] [--no-push]` | Register a scaffolded project: its row and ledger on `claude/birth-`, written by `cortex.py birth`. Dry run by default; exit **2** = refused (no folder, key or path taken, no sync verbs) | | `retire --why "…"` (`scripts/cortex.py`) | Is this project over? Flips the row to `status: retired`, stamps the note, logs it. Exit **1** = an unknown key, an already-retired row, or a run still listed | diff --git a/tests/test_cortex_conductor.py b/tests/test_cortex_conductor.py index 30d4b463..47bcdfa5 100644 --- a/tests/test_cortex_conductor.py +++ b/tests/test_cortex_conductor.py @@ -1142,3 +1142,175 @@ def test_with_no_repo_backed_row_a_fully_stale_tree_reads_as_off_laptop(tmp_path def test_checkin_dry_run_prints_the_drift_line(tmp_skeleton): r = _run(["checkin", "--dry-run", "--cortex", str(tmp_skeleton)]) assert r.returncode == 0 and "Drift: " in r.stdout + + +# --- the birth (a scaffolded project's row and ledger) ---------------------- + +SYNC = """#!/bin/bash +case "${1:-}" in + help|--help|-h) HELP=1 ;; +esac +cmd="$1" +tail_logs() { case "$1" in gpu) x ;; cpu) y ;; esac; } +case "$cmd" in + push) push ;; + pull) pull ;; + help|--help|-h) print_help ;; + push-data-init) push_data_init ;; + *) + exit 1 + ;; +esac +""" + + +def _scaffold(root, category="active", key="fresh", sync=SYNC, ledger="wiki/project/state.md"): + local = root / category / key + (local / "hpc").mkdir(parents=True) + (local / "hpc" / "sync").write_text(sync) + if ledger: + (local / ledger).parent.mkdir(parents=True, exist_ok=True) + (local / ledger).write_text("# state\n") + return local + + +def _map(root): + return {"a": _row(root / "active/a", ral="/mnt/ral/u/active/a"), + "d": _row(root / "development/d", ral="/mnt/ral/u/development/d")} + + +def test_sync_verbs_are_the_dispatch_case_labels_only(tmp_path): + cli = tmp_path / "sync" + cli.write_text(SYNC) + assert _cortex.sync_verbs_of(cli) == ["push", "pull", "push-data-init"] + assert _cortex.sync_verbs_of(tmp_path / "missing") == [] + + +def test_remote_is_the_github_origin_or_none(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + assert _cortex.remote_of(repo) == "none" + subprocess.run(["git", "init", "-q", str(repo)], check=True) + subprocess.run(["git", "-C", str(repo), "remote", "add", "origin", + "git@github.com:PyAutoLabs/fresh.git"], check=True) + assert _cortex.remote_of(repo) == "PyAutoLabs/fresh" + + +def test_a_birth_row_is_derived_from_the_folder_and_the_other_rows(tmp_path): + root = _science(tmp_path, ["active/a", "development/d"]) + local = _scaffold(root, "development") + row, problems = _cortex.birth_row(_map(root), "fresh", "development", + "autofit_assistant") + assert problems == [] + assert row["local_path"] == str(local) + assert row["ral_root"] == "/mnt/ral/u/development/fresh" + assert row["sync_verbs"] == ["push", "pull", "push-data-init"] + assert row["ledger"] == "wiki/project/state.md" + assert (row["remote"], row["status"], row["assistant"]) == ( + "none", "active", "autofit_assistant") + + +def test_a_birth_refuses_what_it_cannot_register(tmp_path): + root = _science(tmp_path, ["active/a", "development/d"]) + projects = _map(root) + + def why(key, category): + return " | ".join(_cortex.birth_row(projects, key, category, "none")[1]) + + assert "does not exist" in why("ghost", "active") + assert "already a projects.yaml key" in why("a", "active") + assert "born into active or development" in why("old", "archive") + _scaffold(root, "active", "bare", sync="#!/bin/bash\necho no dispatch\n") + assert "no verbs found" in why("bare", "active") + + +def test_birth_dry_run_writes_nothing(tmp_skeleton, tmp_path): + root = _science(tmp_path, ["active/a"]) + _scaffold(root) + text = (tmp_skeleton / "projects.yaml").read_text() + text = text.replace("local_path: /tmp/example\n", f"local_path: {root / 'active/a'}\n") + (tmp_skeleton / "projects.yaml").write_text(text) + text = re.sub(r"ral_root: \S+", "ral_root: /mnt/ral/u/active/a", text, count=1) + (tmp_skeleton / "projects.yaml").write_text(text) + before = _tree_bytes(tmp_skeleton) + r = _run(["birth", "fresh", "--category", "active", "--assistant", "none", + "--summary", "x", "--science-root", str(root), "--cortex", str(tmp_skeleton)]) + assert r.returncode == 0, r.stdout + r.stderr + assert "would cut claude/birth-fresh" in r.stdout + assert _tree_bytes(tmp_skeleton) == before + + +def _git_out(*args, cwd=None): + return subprocess.run(["git", *args], cwd=cwd, check=True, + capture_output=True, text=True).stdout.strip() + + +def _birth_clone(tmp_path): + """A Cortex copy on `main` with a local bare origin, and a Science tree + holding a scaffolded `active/fresh` — `--apply` reaches nothing real.""" + src = cortex_root() + if "def birth_project" not in (src / "scripts" / "cortex.py").read_text(): + pytest.skip("this Cortex checkout has no `cortex.py birth` yet") + work = tmp_path / "cortex" + shutil.copytree(src, work, ignore=shutil.ignore_patterns(".git", "__pycache__")) + root = _science(tmp_path, ["active/a"]) + _scaffold(root) + env = ["-c", "user.name=t", "-c", "user.email=t@t"] + _git_out("init", "-q", "-b", "main", cwd=work) + _git_out(*env, "add", "-A", cwd=work) + _git_out(*env, "commit", "-qm", "base", cwd=work) + _git_out("clone", "-q", "--bare", str(work), str(tmp_path / "origin.git")) + _git_out("remote", "add", "origin", str(tmp_path / "origin.git"), cwd=work) + _git_out("fetch", "-q", "origin", cwd=work) + run_env = {**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", + "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"} + argv = ["birth", "fresh", "--category", "active", "--assistant", "none", + "--summary", "a probe", "--science-root", str(root), + "--cortex", str(work), "--apply"] + return work, root, argv, run_env + + +def test_a_failed_commit_leaves_main_clean_and_exits_nonzero(tmp_path): + work, _root, argv, run_env = _birth_clone(tmp_path) + hook = work / ".git" / "hooks" / "pre-commit" + hook.write_text("#!/bin/sh\nexit 1\n") + hook.chmod(0o755) + r = _run(argv, env=run_env) + assert r.returncode == 1 and "nothing committed" in r.stderr + assert _git_out("branch", "--show-current", cwd=work) == "main" + assert _git_out("status", "--porcelain", cwd=work) == "" + assert "claude/birth-fresh" not in _git_out("branch", cwd=work) + + +def test_a_failed_push_exits_nonzero_with_the_commit_kept(tmp_path): + work, _root, argv, run_env = _birth_clone(tmp_path) + _git_out("remote", "set-url", "--push", "origin", str(tmp_path / "nowhere.git"), cwd=work) + r = _run(argv, env=run_env) + assert r.returncode == 1 and "push: FAILED" in r.stdout + assert _git_out("branch", "--show-current", cwd=work) == "main" + assert "claude/birth-fresh" in _git_out("branch", cwd=work) + + +def test_birth_apply_cuts_a_branch_pushes_it_and_returns_to_main(tmp_path): + work, root, argv, run_env = _birth_clone(tmp_path) + git = _git_out + r = _run(argv, env=run_env) + assert r.returncode == 0, r.stdout + r.stderr + assert "pushed" in r.stdout + assert git("branch", "--show-current", cwd=work) == "main" + assert git("status", "--porcelain", cwd=work) == "" + assert "claude/birth-fresh" in git("branch", cwd=tmp_path / "origin.git") + shown = git("show", "claude/birth-fresh:projects.yaml", cwd=work) + assert "fresh:" in shown and f"local_path: {root / 'active' / 'fresh'}" in shown + again = _run(argv, env=run_env) + assert again.returncode != 0 and "REFUSED" in again.stderr + assert git("branch", "--show-current", cwd=work) == "main" + + +def test_an_over_long_summary_is_refused_in_the_dry_run(tmp_skeleton, tmp_path): + root = _science(tmp_path, ["active/a"]) + _scaffold(root) + r = _run(["birth", "fresh", "--category", "active", "--assistant", "none", + "--summary", " ".join(["word"] * 30), "--science-root", str(root), + "--cortex", str(tmp_skeleton)]) + assert r.returncode == 2 and "--summary is 30 words" in r.stdout From cfc137fa72bb0890a70f0fcd3f2c7dce77ed9508 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 29 Oct 2026 13:57:20 +0000 Subject: [PATCH 2/2] tests: neutral owner/assistant names (tenant firewall) Co-Authored-By: Claude Opus 5.5 --- tests/test_cortex_conductor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_cortex_conductor.py b/tests/test_cortex_conductor.py index 47bcdfa5..4aabef76 100644 --- a/tests/test_cortex_conductor.py +++ b/tests/test_cortex_conductor.py @@ -1192,22 +1192,22 @@ def test_remote_is_the_github_origin_or_none(tmp_path): assert _cortex.remote_of(repo) == "none" subprocess.run(["git", "init", "-q", str(repo)], check=True) subprocess.run(["git", "-C", str(repo), "remote", "add", "origin", - "git@github.com:PyAutoLabs/fresh.git"], check=True) - assert _cortex.remote_of(repo) == "PyAutoLabs/fresh" + "git@github.com:SomeOwner/fresh.git"], check=True) + assert _cortex.remote_of(repo) == "SomeOwner/fresh" def test_a_birth_row_is_derived_from_the_folder_and_the_other_rows(tmp_path): root = _science(tmp_path, ["active/a", "development/d"]) local = _scaffold(root, "development") row, problems = _cortex.birth_row(_map(root), "fresh", "development", - "autofit_assistant") + "some_assistant") assert problems == [] assert row["local_path"] == str(local) assert row["ral_root"] == "/mnt/ral/u/development/fresh" assert row["sync_verbs"] == ["push", "pull", "push-data-init"] assert row["ledger"] == "wiki/project/state.md" assert (row["remote"], row["status"], row["assistant"]) == ( - "none", "active", "autofit_assistant") + "none", "active", "some_assistant") def test_a_birth_refuses_what_it_cannot_register(tmp_path):