From 719c1264f3beb92d2882156130c1a197d57d1e46 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Fri, 30 Oct 2026 15:34:34 +0900 Subject: [PATCH 1/6] docs(roadmap): amend windows_install (A2): release lifecycle and live-session split 0.3.0 ships tested on Windows in CI only and is the first release cut by CI under writing-release v2.0.0. A2 adds release_lifecycle beside install, rewrites release around the CI cut, moves the live Windows check to a new live_session leaf, and corrects pre-conditions that still named the blocked if_probe and shell_gaps nodes. Gap note: __reports__/windows_install/A2_lifecycle_and_live_split.md Co-Authored-By: Claude Opus 5.5 --- .../A2_lifecycle_and_live_split.md | 74 +++++++++++++++++ __roadmap__/windows_install/README.md | 7 +- __roadmap__/windows_install/next/README.md | 2 +- .../windows_install/next/next/README.md | 2 +- .../windows_install/next/next/next/README.md | 11 ++- .../windows_install/next/next/next/install.md | 61 +++++++++++--- .../next/next/next/next/README.md | 9 +- .../next/next/next/next/next/README.md | 38 +++++++++ .../next/next/next/next/next/live_session.md | 21 +++++ .../next/next/next/next/release.md | 39 +++++---- .../next/next/next/release_lifecycle.md | 83 +++++++++++++++++++ 11 files changed, 309 insertions(+), 38 deletions(-) create mode 100644 __reports__/windows_install/A2_lifecycle_and_live_split.md create mode 100644 __roadmap__/windows_install/next/next/next/next/next/README.md create mode 100644 __roadmap__/windows_install/next/next/next/next/next/live_session.md create mode 100644 __roadmap__/windows_install/next/next/next/release_lifecycle.md diff --git a/__reports__/windows_install/A2_lifecycle_and_live_split.md b/__reports__/windows_install/A2_lifecycle_and_live_split.md new file mode 100644 index 0000000..8faa70d --- /dev/null +++ b/__reports__/windows_install/A2_lifecycle_and_live_split.md @@ -0,0 +1,74 @@ +# Gap analysis: windows_install — adopt the release lifecycle, split the live Windows session (A2) + +## Problem statement +The roadmap's last two leaves no longer describe the work. +- **`install.md` gates on a live Windows session.** Eliott has no Windows machine this week, so the leaf cannot close, and `release.md` (one level deeper) cannot start. +- **`release.md` describes a release that no longer exists.** It says 0.2.0, gates on "a01 denied" (the retired guard, A1), and cuts the release with a hand `chore(release)` bump. +- **The org release default changed.** `writing-release` v2.0.0 says releases are cut only by CI after a merge to `main`, through lanes, with no hand bumps or tags. Eliott asked, on 2026-10-30, to fold the adoption into this cycle. + +## Evidence +- `next/next/next/install.md`: + - its last gate is `[behavioral] On a real Windows machine …`; + - Step 1 still says "implement the `if` decision", which A1 superseded. +- `next/next/next/next/release.md`: + - its Goal says "0.2.0"; + - a gate reads "a01 denied, `ls` allowed"; + - its Commit is `chore(release): 0.2.0`; + - Step 1 says "bump to 0.3.0". The leaf contradicts itself. +- Pre-conditions that BFS can never satisfy: + - `next/README.md` waits on `if_probe`, which is blocked; + - `next/next/README.md` waits on `shell_gaps`, which is blocked; + - `next/next/next/README.md` waits on `if_decision` and `path_gaps`, both blocked. +- State of the repository on origin/main 3afb2da: + - no tags, no CONTRIBUTING, no release tool, no commit checker; + - `main` has no protection (`rules` [], protection 404); + - squash and rebase merges are enabled, `merge_commit_title = MERGE_MESSAGE`, and branches are not deleted on merge; + - 0.1.1, 0.1.2 and 0.2.0 were all hand `chore(release)` commits. +- The Windows launch: the 0.2.0 binary rows exec `${CLAUDE_PLUGIN_DATA}/bin/murmuration`. Claude Code's hooks reference says exec form on Windows needs "a real executable such as a `.exe`", and it documents no extension lookup. + +## Root cause +- When the roadmap was written, a Windows machine was assumed, and the release process was the repository's own. +- A1 then removed the guard and left the gate prose that named it. + +## Impact assessment +- **Scope.** + - In: a new leaf `release_lifecycle` (PR-A), a sibling of `install` that runs in parallel. + - In: a new leaf `live_session`, two levels below `install`. It takes over the live gate. + - In: corrected prose in `install`, `release` and the four READMEs. + - Out: any change to the guard leaves, which stay blocked. +- **Dependencies.** + - `release` needs both `install` and `release_lifecycle`. The 0.3.0 release is cut by the release job PR-A adds, and the binary it releases comes from PR-B. + - `live_session` needs `release`, because it installs 0.3.0 from Nest. +- **Risk.** + - Without `release_lifecycle`, 0.3.0 would be the last hand-cut release, and the next repository adopting v2 would have nothing to copy. + - Without the split, `install` cannot close this week. + - With the split, 0.3.0 ships tested on Windows in CI only, and its banner says so. +- **Campaign close.** The knowledge-transfer note moves to `release`, so the campaign closes with 0.3.0. `live_session` stays open on reminder `murmuration-win-live`, and a `done` leaf is never reopened for it. + +## Proposed solution +- **`release_lifecycle`** (PR-A, a Reviewed PR): + - commitizen with a custom `bump_map`: feat is minor; fix and perf are patch; `BREAKING CHANGE` is minor while on 0.x; + - `.cz.toml` at the root; + - a `changes` job, a `commits` job, a PR preview step, and a `release` job in `guard-binaries.yml`, which keeps its name; + - `pr-title.yml`; + - CONTRIBUTING.md; + - the `v0.2.0` baseline tag at 1f164ae; + - the repository settings. + + The release job fails closed: it does not run on a red matrix, needs a `v*` tag, checks the manifest hashes, and pushes atomically. +- **`install`** (PR-B, stacked on PR-A): + - the binary rows exec `…/bin/murmuration.exe` on every OS; + - the stamp is keyed to that name; + - `TARGETS` includes Windows; + - on Windows, the doubt branch removes the binary instead of writing a POSIX launcher; + - the smoke test runs on Windows; + - the docs say "Windows tested in CI only". + + The Python rows are unchanged. A real `python3` on PATH is a documented Windows requirement: a launch shim measured about 70 ms more per start on macOS and was rejected. +- **`release`**: PR-B's merge makes CI commit `build(bin)` and then cut `v0.3.0`. The leaf then checks a macOS install from Nest and writes the knowledge-transfer note. +- **`live_session`**: the Windows install from Nest and a real session. + +## Recommendations +1. Decide the commit vocabulary (`__reports__/release_lifecycle/commit_vocabulary_v0.md`) before `.cz.toml` is written, so the checker encodes a decision rather than a guess. +2. Push the baseline tag and change the settings before PR-A merges. Without a `v*` tag, cz counts the whole history as unreleased. +3. Keep the workflow file's name, and split its hunks between the two leaves: PR-A edits `on`, `needs`, `if` and `concurrency` and adds jobs; `install` edits the bodies of `build`, `assemble` and `install_smoke`. diff --git a/__roadmap__/windows_install/README.md b/__roadmap__/windows_install/README.md index 84dc87a..e3a0ac1 100644 --- a/__roadmap__/windows_install/README.md +++ b/__roadmap__/windows_install/README.md @@ -4,7 +4,7 @@ The Windows cycle of Murmuration, after 0.1.1 shipped the 4 Unix binaries. CI already builds a Windows row, but installs stay deferred: hooks exec `python3` and the data path is `bin/murmuration`. One leaf per `murmuration-win-*` reminder (macOS Reminders, list Murmuration); depth follows each reminder's `after:`. ## Goal -`murmuration@cracking-shells` installs on Windows, released as 0.3.0. +`murmuration@cracking-shells` installs on Windows as far as CI can show, released as 0.3.0 by CI; the live Windows session follows (`live_session`, A2). ## Pre-conditions - [x] 0.1.1 on `origin/main` (72a61d0) @@ -16,7 +16,9 @@ The Windows cycle of Murmuration, after 0.1.1 shipped the 4 Unix binaries. CI al - **Hashed inputs.** `budget.py`, `handback.py`, `run_hook.py` and the Rust sources are hashed into `bin/manifest.json` (`build_manifest.SCRIPTS`, `RUST_FILES`). A change to any of them needs CI's `build(bin)` commit on `main` before a version bump. - **Version numbers.** 0.1.1 is taken; the next guard change (reminder `murmuration-posix-gaps`) shipped as 0.1.2. Eliott, 2026-10-29: the guard's retirement ships as 0.2.0, since it removes a feature, and the Windows release moves to 0.3.0. - **The guard is retired (A1).** Eliott, 2026-10-29: first "deprecate Python" once Windows is judged sound, then, after the `shell_gaps` round-3 verdict and the threat-model review, "Yes, retire it": the whole guard goes, not only its Python half (`retire_guard.md`). `if_probe`, `next/if_decision`, `next/shell_gaps` and `next/next/path_gaps` change or probe the guard, so they close as blocked when `retire_guard` is done. Eliott, the same day: every Python script that has a working Rust equivalent on macOS, Linux and Windows retires too, so `budget.py` and `handback.py` go once the Windows binary passes `hook_selftest` on Windows; until then they stay as the launcher's fallback. -- **One data folder for every installed version.** All projects share `~/.claude/plugins/data/murmuration-cracking-shells/bin/`, and each SessionStart installs its own version's binary there. A 0.1.x project's guard rows (`onFailure: "block"`) exec that binary with `guard`; the 0.2.0 binary answers with usage and exit 2, so a 0.1.x session that runs after a 0.2.0 session started blocks every Bash and settings-file write until it restarts. Update every project to 0.2.0 (`claude plugin update murmuration@cracking-shells --scope project` in its folder). Found 2026-10-30 at the 0.2.0 release check. +- **One data folder for every installed version.** All projects share `~/.claude/plugins/data/murmuration-cracking-shells/bin/`, and each SessionStart installs its own version's binary there. A 0.1.x project's guard rows (`onFailure: "block"`) exec that binary with `guard`; the 0.2.0 binary answers with usage and exit 2, so a 0.1.x session that runs after a 0.2.0 session started blocks every Bash and settings-file write until it restarts. Update every project to 0.2.0 (`claude plugin update murmuration@cracking-shells --scope project` in its folder). Found 2026-10-30 at the 0.2.0 release check. From 0.3.0 the binary is `bin/murmuration.exe` with its own stamp `bin/murmuration.exe.verified` on every OS, so 0.3.0 and 0.2.0 no longer overwrite each other's files. +- **Windows needs a real `python3` on PATH (A2).** The three Python rows stay `python3 -S -I …` in exec form; a shim that found `python`/`py` cost about 70 ms per start on macOS and was rejected (Eliott, 2026-10-30). Microsoft Store Python provides `python3`; a python.org-only install does not. +- **Releases are cut by CI (A2).** From PR-A on, `plugin.json`'s version is written only by the `release` job of `guard-binaries.yml` after a merge to `main` (writing-release v2.0.0); no leaf bumps it by hand. ## Status ```mermaid @@ -44,6 +46,7 @@ graph TD | ID | Date | Source | Nodes Added | Rationale | |:---|:-----|:-------|:------------|:----------| | A1 | 2026-10-29 | `__reports__/windows_install/A1_retire_guard.md` | ["retire_guard.md"] | The guard's Windows reading cannot converge; Claude Code's own settings cover the risk | +| A2 | 2026-10-30 | `__reports__/windows_install/A2_lifecycle_and_live_split.md` | ["next/next/next/release_lifecycle.md", "next/next/next/next/next/", "next/next/next/next/next/live_session.md"] | Adopt writing-release v2.0.0; split the live Windows session out of `install` (no Windows machine this week) | ## Progress | Node | Branch | Commits | Notes | diff --git a/__roadmap__/windows_install/next/README.md b/__roadmap__/windows_install/next/README.md index d428780..7c39207 100644 --- a/__roadmap__/windows_install/next/README.md +++ b/__roadmap__/windows_install/next/README.md @@ -7,7 +7,7 @@ Runs after `ci_green` and `if_probe`: the decision needs the probe's report, the The `if` decision is recorded and the Windows shell bypasses are closed. ## Pre-conditions -- [ ] `ci_green` and `if_probe` done +- [x] `ci_green` done; `if_probe` closed as blocked (A1) ## Success Gates - ⬜ `if_decision.md` and `shell_gaps.md` gates pass diff --git a/__roadmap__/windows_install/next/next/README.md b/__roadmap__/windows_install/next/next/README.md index 3988e1f..920b989 100644 --- a/__roadmap__/windows_install/next/next/README.md +++ b/__roadmap__/windows_install/next/next/README.md @@ -7,7 +7,7 @@ After `shell_gaps`, because both edit `permission_guard.py` and `rust/`. The remaining Windows path forms are decided. ## Pre-conditions -- [ ] `shell_gaps` done +- [x] `shell_gaps` closed as blocked (A1) ## Success Gates - ⬜ `path_gaps.md` gates pass diff --git a/__roadmap__/windows_install/next/next/next/README.md b/__roadmap__/windows_install/next/next/next/README.md index fdf725f..0cb5f09 100644 --- a/__roadmap__/windows_install/next/next/next/README.md +++ b/__roadmap__/windows_install/next/next/next/README.md @@ -1,22 +1,23 @@ # Install ## Context -After `retire_guard` (A1), which replaced `if_decision` and `path_gaps`: the install ships the `budget` and `handback` hooks on Windows. +After `retire_guard` (A1), which replaced `if_decision` and `path_gaps`. Two parallel leaves (A2): `install` makes the install path work on Windows as far as CI can show (PR-B), and `release_lifecycle` adopts writing-release v2.0.0 (PR-A). Both edit `guard-binaries.yml`, split by hunk: `release_lifecycle` owns `on`, `concurrency`, `needs`/`if` and new jobs; `install` owns the bodies of `build`, `assemble` and `install_smoke`. ## Goal -A Windows session observed end to end. +The install proven in CI on macOS, Linux and Windows, and releases cut by CI. ## Pre-conditions -- [ ] `if_decision` and `path_gaps` done +- [x] `retire_guard` done (A1) ## Success Gates -- ⬜ `install.md` gates pass +- ⬜ `install.md` and `release_lifecycle.md` gates pass ## Status ```mermaid graph TD install[Install path and first Windows session]:::planned next[Release]:::planned + release_lifecycle[Adopt the writing-release lifecycle]:::amendment classDef done fill:#166534,color:#bbf7d0 classDef inprogress fill:#854d0e,color:#fef08a classDef planned fill:#374151,color:#e5e7eb @@ -29,10 +30,12 @@ graph TD |:-----|:-----|:-------| | `install.md` | 📄 Leaf Task | ⬜ Planned | | `next/` | 📁 Directory | ⬜ Planned | +| `release_lifecycle.md` | 📄 Leaf Task | 🔵 Amendment | ## Amendment Log | ID | Date | Source | Nodes Added | Rationale | |:---|:-----|:-------|:------------|:----------| +| A2 | 2026-10-30 | `__reports__/windows_install/A2_lifecycle_and_live_split.md` | ["release_lifecycle.md"] | Releases cut by CI after a merge (writing-release v2.0.0), adopted before 0.3.0 | ## Progress | Node | Branch | Commits | Notes | diff --git a/__roadmap__/windows_install/next/next/next/install.md b/__roadmap__/windows_install/next/next/next/install.md index 5fc26a8..38e97eb 100644 --- a/__roadmap__/windows_install/next/next/next/install.md +++ b/__roadmap__/windows_install/next/next/next/install.md @@ -1,19 +1,54 @@ -# Install path and first Windows session +# Install path, proven in CI on Windows -**Goal**: The plugin installs and runs on Windows, observed end to end in a real session. +**Goal**: The plugin's install path works on Windows as far as CI can show: the binary installs, its self-test passes, and the hook rows start as Claude Code spawns them. Docs say "Windows tested in CI only". **Pre-conditions**: -- [ ] `retire_guard` done (A1: it replaces `if_decision` and `path_gaps`) +- [x] `retire_guard` done (A1; it replaces `if_decision` and `path_gaps`) **Success Gates**: -- ⬜ Hook rows start on Windows (`python3` is usually absent; the binary is `murmuration.exe`), with `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_DATA}` substitution checked -- ⬜ `("Windows", "AMD64")` in `install_binary.TARGETS` (its self-test updated) and a windows row in `install_smoke` -- ⬜ `scripts/check_text_io.py` clean: every text open and stdin/stdout explicit UTF-8 -- ⬜ [behavioral] On a real Windows machine, installed from a branch (git-subdir): a session starts, `/lab` works, `.verified` and `murmuration.exe` in the data dir, `budget` and `handback` run from the binary in a lab wave -**References**: reminder `murmuration-win-install`; `plugins/murmuration/scripts/install_binary.py` (comment on `TARGETS`) +- ⬜ The 9 binary rows in `hooks.json` exec `${CLAUDE_PLUGIN_DATA}/bin/murmuration.exe`; the 3 Python rows are byte-identical to origin/main 3afb2da +- ⬜ `("Windows", "AMD64")` in `install_binary.TARGETS`; every `--selftest` in `plugins/murmuration/scripts/` passes on macOS +- ⬜ `scripts/check_text_io.py` clean +- ⬜ `claude plugin validate plugins/murmuration` passes +- ⬜ [run] PR-B's `pull_request` run (stacked on PR-A): 5 build rows and `install_smoke` green on macos-15, ubuntu-22.04 and windows-latest; `assemble` fails if the Windows binary is missing +- ⬜ Plugin description and both README banners read "macOS and Linux; Windows tested in CI only, no live session yet"; README Requirements state the Windows `python3` requirement +**References**: plan `~/.claude/plans/system-reminder-linked-sessions-now-groovy-waffle.md` § PR-B; `__reports__/windows_install/A2_lifecycle_and_live_split.md`; Claude Code hooks reference, exec form (code.claude.com/docs/en/hooks); reminder `murmuration-win-ci-release` -## Step 1: Launcher, targets, smoke, then the real session -**Goal**: Unblock the Windows install and watch it work. +## Step 1: The binary as `murmuration.exe` on every OS +**Goal**: Hooks exec a documented Windows form and never touch 0.2.0's files in the shared data folder. **Implementation Logic**: -Choose how hook rows start on Windows; implement the `if` decision; extend `TARGETS` and `install_smoke`; run `check_text_io.py`; install on Windows and run the behavioural checks. -**Deliverables**: `plugins/murmuration/hooks/hooks.json`, `plugins/murmuration/scripts/install_binary.py`, `.github/workflows/guard-binaries.yml`; results in this leaf -**Consistency Checks**: `python3 -I plugins/murmuration/scripts/install_binary.py --selftest` (expected: PASS) +- `hooks/hooks.json`: append `.exe` to the `command` of the 9 binary rows. The Python rows (`install_binary.py`, `resume_brief.py`, `user_words.py`) stay exactly as they are. Shell-form or shim launches were rejected because they add about 70 ms per start. +- `scripts/install_binary.py`, the minimum: + - add `("Windows", "AMD64"): "x86_64-pc-windows-msvc"` to `TARGETS` and fix its comment and the module docstring, which names `bin/murmuration`; + - `install()`: `dest` becomes `bin/murmuration.exe` and the stamp becomes `bin/murmuration.exe.verified`. 0.2.0 owns `bin/murmuration` and `bin/.verified`; sharing the stamp would make the two versions re-copy and re-self-test at each other's SessionStart; + - on Windows (`os.name == "nt"`), the doubt branch removes `dest` instead of writing the POSIX launcher, and the existing note says why. The note text may say the hooks are off on Windows instead of "run as the Python scripts"; + - `selftest()`: flip the two asserts that pin "no Windows". Cover the `.exe` destination and stamp name in the existing temp-lab cases. +- Leave the rest alone. A failed `os.replace` (an exe held open by another session) already reaches `main()`'s handler, which keeps the old binary and retries next SessionStart. The `tmp` copy name changes only if the Windows CI row shows it must. +- `scripts/budget.py` self-tests 7 and 8: expect the `/bin/murmuration.exe` suffix. The file is hashed into `bin/manifest.json`, so CI rebuilds; do not touch `bin/`. +**Deliverables**: `plugins/murmuration/hooks/hooks.json`; `plugins/murmuration/scripts/install_binary.py` (`TARGETS`, `install`, `selftest`); `plugins/murmuration/scripts/budget.py` (self-tests 7–8) +**Consistency Checks**: `for s in install_binary budget handback build_manifest; do python3 -I plugins/murmuration/scripts/$s.py --selftest >/dev/null || exit 1; done && python3 -I plugins/murmuration/scripts/check_text_io.py && claude plugin validate plugins/murmuration` (expected: PASS) **Commit**: `feat(install): install the hook binary on Windows` + +## Step 2: Smoke the install on Windows in CI +**Goal**: CI shows the install and one budget hook working on all three OSes, spawned the way Claude Code spawns them. +**Implementation Logic**: +Edit only the bodies of the `build`, `assemble` and `install_smoke` jobs in `.github/workflows/guard-binaries.yml`. `release_lifecycle` owns `on`, `needs`, `if`, `concurrency` and any new jobs. +- `install_smoke`: + - add `windows-latest` to the matrix, with a `python3.exe` copy of the runner's Python on PATH (the documented requirement); + - every row reads `hooks.json` and runs the SessionStart install row and one PreToolUse `Agent` budget row the way Claude Code spawns them: exec form, no shell, placeholders substituted (backslashes on Windows), env `CLAUDE_PLUGIN_ROOT` and `CLAUDE_PLUGIN_DATA` set; + - assert empty stdout from the install, `murmuration.exe.verified` present, `murmuration.exe` byte-equal to the CI artifact for that target, and the budget row exits 0. +- `assemble`: Windows is required; drop the warning-only path in `reject()`. +- The Windows `build` row also runs `check_text_io.py` and the `install_binary.py --selftest`. +- Keep the replay logic in a small script under `plugins/murmuration/scripts/` only if inline YAML gets unreadable. Any new script must pass `check_text_io.py`, and is not hashed unless added to `SCRIPTS`. +**Deliverables**: `.github/workflows/guard-binaries.yml` (bodies of `build`, `assemble`, `install_smoke`) +**Consistency Checks**: `uv run --no-project --with pyyaml==6.0.2 python -c "import yaml,sys; d=yaml.safe_load(open('.github/workflows/guard-binaries.yml',encoding='utf-8')); m=d['jobs']['install_smoke']['strategy']['matrix']; sys.exit(0 if 'windows-latest' in str(m) else 1)"` (expected: PASS) +**Commit**: `ci(install): smoke-test the install on Windows` + +## Step 3: Say what was tested +**Goal**: Nobody reads 0.3.0 as live-tested on Windows. +**Implementation Logic**: +- `plugins/murmuration/.claude-plugin/plugin.json` description: replace "Claude Code only; macOS and Linux." with wording that says Windows is tested in CI only, with no live session yet. Never touch `version`; the CI release job owns it. +- Root `README.md` banner (lines 5–10) and `plugins/murmuration/README.md` banner (lines 4–5): the same statement. +- Requirements (root ~93, plugin ~111): on Windows, a real `python3` on PATH. Microsoft Store Python provides one. A python.org-only install has only `python` and `py`, so the user adds a `python3` alias; without one, the install hook fails without blocking and the budget and hand-back hooks error. +- `release_lifecycle` owns README:151 and the versioning text. `live_session` owns flipping the banner after a live run. +**Deliverables**: `plugins/murmuration/.claude-plugin/plugin.json` (`description`), `README.md`, `plugins/murmuration/README.md` +**Consistency Checks**: `python3 -I -c "import json,sys; d=json.load(open('plugins/murmuration/.claude-plugin/plugin.json',encoding='utf-8')); sys.exit(0 if 'CI' in d['description'] and d['version']=='0.2.0' else 1)"` (expected: PASS) +**Commit**: `docs(readme): Windows tested in CI only` diff --git a/__roadmap__/windows_install/next/next/next/next/README.md b/__roadmap__/windows_install/next/next/next/next/README.md index c4f78ca..1276a46 100644 --- a/__roadmap__/windows_install/next/next/next/next/README.md +++ b/__roadmap__/windows_install/next/next/next/next/README.md @@ -1,13 +1,13 @@ # Release ## Context -Last step, behind Eliott's go-aheads: merge, CI binaries for 5 targets, 0.2.0, Nest. +Behind Eliott's go-aheads: PR-B merges, CI commits the binaries for 5 targets and then cuts 0.3.0; macOS install from Nest. The live Windows session (A2) runs one level deeper. ## Goal -0.2.0 with Windows support installs from Nest. +0.3.0, cut by CI and tested on Windows in CI only, installs from Nest. ## Pre-conditions -- [ ] `install` done +- [ ] `install` and `release_lifecycle` done ## Success Gates - ⬜ `release.md` gates pass @@ -16,6 +16,7 @@ Last step, behind Eliott's go-aheads: merge, CI binaries for 5 targets, 0.2.0, N ```mermaid graph TD release[0.2.0 release with Windows support]:::planned + next[Live Windows session]:::amendment classDef done fill:#166534,color:#bbf7d0 classDef inprogress fill:#854d0e,color:#fef08a classDef planned fill:#374151,color:#e5e7eb @@ -27,10 +28,12 @@ graph TD | Node | Type | Status | |:-----|:-----|:-------| | `release.md` | 📄 Leaf Task | ⬜ Planned | +| `next/` | 📁 Directory | 🔵 Amendment | ## Amendment Log | ID | Date | Source | Nodes Added | Rationale | |:---|:-----|:-------|:------------|:----------| +| A2 | 2026-10-30 | `__reports__/windows_install/A2_lifecycle_and_live_split.md` | ["next/"] | The live Windows session moves after the release | ## Progress | Node | Branch | Commits | Notes | diff --git a/__roadmap__/windows_install/next/next/next/next/next/README.md b/__roadmap__/windows_install/next/next/next/next/next/README.md new file mode 100644 index 0000000..c4eabee --- /dev/null +++ b/__roadmap__/windows_install/next/next/next/next/next/README.md @@ -0,0 +1,38 @@ +# Live Windows session + +## Context +Added by A2: the live Windows check left `install` so 0.3.0 could ship tested on Windows in CI only. Runs after `release`, from the version on Nest, when a Windows machine is at hand. + +## Goal +Murmuration observed working in a real Windows session. + +## Pre-conditions +- [ ] `release` done + +## Success Gates +- ⬜ `live_session.md` gates pass + +## Status +```mermaid +graph TD + live_session[Windows install and session from Nest]:::amendment + classDef done fill:#166534,color:#bbf7d0 + classDef inprogress fill:#854d0e,color:#fef08a + classDef planned fill:#374151,color:#e5e7eb + classDef amendment fill:#1e3a5f,color:#bfdbfe + classDef blocked fill:#7f1d1d,color:#fecaca +``` + +## Nodes +| Node | Type | Status | +|:-----|:-----|:-------| +| `live_session.md` | 📄 Leaf Task | 🔵 Amendment | + +## Amendment Log +| ID | Date | Source | Nodes Added | Rationale | +|:---|:-----|:-------|:------------|:----------| +| A2 | 2026-10-30 | `__reports__/windows_install/A2_lifecycle_and_live_split.md` | ["live_session.md"] | The live Windows gate leaves `install`; no Windows machine this week | + +## Progress +| Node | Branch | Commits | Notes | +|:-----|:-------|:--------|:------| diff --git a/__roadmap__/windows_install/next/next/next/next/next/live_session.md b/__roadmap__/windows_install/next/next/next/next/next/live_session.md new file mode 100644 index 0000000..9fe1c66 --- /dev/null +++ b/__roadmap__/windows_install/next/next/next/next/next/live_session.md @@ -0,0 +1,21 @@ +# Windows install and session from Nest + +**Goal**: 0.3.0 installs from Nest on a real Windows machine and a lab session runs its budget and hand-back hooks from `murmuration.exe`; the docs drop "CI only". +**Pre-conditions**: +- [ ] `release` done (0.3.0 on Nest) +- [ ] A Windows machine (reminder `murmuration-win-live`) +**Success Gates**: +- ⬜ [behavioral] `claude plugin install murmuration@cracking-shells` from Nest on Windows: SessionStart installs `murmuration.exe` and `murmuration.exe.verified` in the data dir, no launcher note +- ⬜ [behavioral] `/lab` works; in one lab wave, `budget` and `handback` run from the binary (rows in `harness.jsonl`, hand-back saved under `__statera__/handbacks/`) +- ⬜ [behavioral] What a user sees without a real `python3` (python.org-only install, Store stub) matches the README's Requirements text +- ⬜ The cached plugin checkout's line endings and `.gitattributes` handling checked (`core.autocrlf` true on that machine) +- ⬜ Banner and description flipped to "Windows supported" (ships with the next release-type commit) +**References**: reminder `murmuration-win-live`; `release.md` results; `install.md` Step 3 Requirements text + +## Step 1: Install, session, observe +**Goal**: See on a real machine what CI could not show. +**Implementation Logic**: +Install from Nest at project scope in a scratch folder; start a session; run `/lab` and one wave; read the data dir, `harness.jsonl`, and the hook output. Repeat once with `python3` removed from PATH to check the README's failure text. Record everything in this leaf. +**Deliverables**: results in this leaf's Progress row; `README.md`, `plugins/murmuration/README.md`, `plugins/murmuration/.claude-plugin/plugin.json` (`description`) +**Consistency Checks**: `python3 -I -c "import json,sys; d=json.load(open('plugins/murmuration/.claude-plugin/plugin.json',encoding='utf-8')); sys.exit(0 if 'CI only' not in d['description'] else 1)"` (expected: PASS) +**Commit**: `docs(readme): Windows observed in a live session` diff --git a/__roadmap__/windows_install/next/next/next/next/release.md b/__roadmap__/windows_install/next/next/next/next/release.md index 772c0f2..354d858 100644 --- a/__roadmap__/windows_install/next/next/next/next/release.md +++ b/__roadmap__/windows_install/next/next/next/next/release.md @@ -1,19 +1,30 @@ -# 0.2.0 release with Windows support +# 0.3.0, cut by CI, tested on Windows in CI only -**Goal**: Ship what the Windows cycle built, as 0.2.0, from Nest. +**Goal**: PR-B's merge makes CI build the binaries and cut 0.3.0; it installs from Nest on macOS with no regression, and the campaign closes with a knowledge-transfer note. **Pre-conditions**: -- [ ] `install` done +- [ ] `install` done (PR-B open and green on 3 OS) +- [ ] `release_lifecycle` done (PR-A merged, first push run released nothing) **Success Gates**: -- ⬜ [run] Merged to `main` and pushed (Eliott's yes); the `build(bin)` commit by `github-actions[bot]` carries all 5 targets; `install_smoke` green on every row -- ⬜ `plugin.json` at 0.2.0 only after that commit is on `main` (a bump ahead of the binaries ships the Python launcher); README banner no longer says Windows is deferred -- ⬜ [behavioral] Nest entry updated and pushed (Eliott's yes); `murmuration@cracking-shells` installed from Nest on Windows and on macOS; a01 denied, `ls` allowed -- ⬜ This roadmap closed with a knowledge-transfer note -**References**: reminder `murmuration-win-release`; Nest at `../Nest` +- ⬜ [run] PR-B merged as a merge commit (Eliott's yes, given when PR-B is green ×3 OS and its `preview` printed 0.3.0) +- ⬜ [run] The push run: `build(bin)` by `github-actions[bot]` carries all 5 targets; `install_smoke` green ×3 OS; `release` committed `chore(release): 0.3.0 [skip ci]`, pushed tag `v0.3.0` and created the GitHub Release +- ⬜ Manifest hashes on `v0.3.0` equal its sources (`build_manifest.py sources-hash`); `plugin.json` version 0.3.0 only on that commit +- ⬜ Nest unchanged (its entry names no version) +- ⬜ [behavioral] Installed from Nest in a scratch project on macOS: cache holds 0.3.0; data dir `murmuration.exe` equals CI's aarch64-apple-darwin binary and `murmuration.exe.verified` exists; no launcher note; one Agent call in a scratch lab appends `harness.jsonl` lines; 0.2.0's `bin/murmuration` untouched +- ⬜ Knowledge-transfer note `__reports__/windows_install/kt_windows_install.md`; this campaign's Progress tables filled +**References**: plan `~/.claude/plans/system-reminder-linked-sessions-now-groovy-waffle.md` § Landing; writing-release `references/recovery.md`; reminder `murmuration-win-ci-release`; Nest at `../Nest` -## Step 1: Merge, binaries, bump, Nest -**Goal**: Each public step behind its own yes. +## Step 1: Merge PR-B and let CI release +**Goal**: The release comes from CI, after the binaries. **Implementation Logic**: -Push `main`, wait for `guard-binaries` with a background `gh run watch`, check the `build(bin)` commit, bump to 0.3.0, update the README banner, update and push Nest, install on both OSes and repeat the hook check. -**Deliverables**: `plugins/murmuration/.claude-plugin/plugin.json`, `README.md`, Nest entry; knowledge-transfer note -**Consistency Checks**: `git ls-tree origin/main plugins/murmuration/bin` lists 5 targets (expected: PASS) -**Commit**: `chore(release): 0.2.0` +Coordinator, behind Eliott's conditional yes: after PR-A merges, PR-B retargets to `main`; merge it as a merge commit; `env -u GH_TOKEN -u GITHUB_TOKEN gh run watch` in the background, no polling; check the bot commit, the tag, the Release; pull `main`. If `release` reports nothing to release or fails, follow `recovery.md` and ask Eliott before any re-run that would push. +**Deliverables**: results in this leaf's Progress row +**Consistency Checks**: `git fetch -q --tags origin && test "$(git ls-tree --name-only origin/main plugins/murmuration/bin/ | wc -l | tr -d ' ')" -ge 6 && git rev-parse -q --verify refs/tags/v0.3.0` (expected: PASS) +**Commit**: `merge(install): install the hook binary on Windows` + +## Step 2: macOS install from Nest, then close +**Goal**: No regression on the platform we can run live, and the campaign's lessons kept. +**Implementation Logic**: +In a scratchpad project, at project scope: `claude plugin marketplace update`, `claude plugin install murmuration@cracking-shells --scope project`, one headless session that runs `/lab` and one Agent call; read the data dir and the cache. Write the KT note (writing-reports, knowledge-transfer). `live_session` stays open on reminder `murmuration-win-live`. +**Deliverables**: `__reports__/windows_install/kt_windows_install.md`; Progress rows in this campaign's READMEs +**Consistency Checks**: `test -f __reports__/windows_install/kt_windows_install.md` (expected: PASS) +**Commit**: `docs(roadmap): record the 0.3.0 release and the install from Nest` diff --git a/__roadmap__/windows_install/next/next/next/release_lifecycle.md b/__roadmap__/windows_install/next/next/next/release_lifecycle.md new file mode 100644 index 0000000..cd5667c --- /dev/null +++ b/__roadmap__/windows_install/next/next/next/release_lifecycle.md @@ -0,0 +1,83 @@ +# Adopt the writing-release lifecycle + +**Goal**: Releases of `plugins/murmuration` are cut only by CI after a merge to `main`. The commit checker, the release job, CONTRIBUTING and the repository settings follow writing-release v2.0.0, so 0.3.0 is the first release nobody cuts by hand. +**Pre-conditions**: +- [x] `retire_guard` done (A1) +- [ ] Commit vocabulary decided by Eliott (`__reports__/release_lifecycle/commit_vocabulary_v0.md`, sitting 1) +- [ ] Tag `v0.2.0` at 1f164ae pushed, and repository settings applied and read back (Eliott's yes, sitting 1) +**Success Gates**: +- ⬜ `cz check --rev-range` accepts every origin/main subject of the last 60 days that the decided vocabulary keeps, and rejects the forms it retires +- ⬜ The `bump_map` releases only on feat (minor), fix and perf (patch), and `BREAKING CHANGE` (minor while 0.x). `refactor`, `docs`, `merge`, `rdm`, `reports` and the research types never bump, checked by `cz bump --dry-run` in a scratch clone +- ⬜ The `release` job fails closed. It does not run after a red matrix (it tolerates `skipped` only when `changes` found no gated path). It exits 1 when no `v*` tag exists. It checks the manifest hashes against the new tip of origin/main, uses `git push --atomic`, and grants `contents: write` to itself alone +- ⬜ [run] PR-A's `pull_request` run green, including `commits`, `pr-title` and `preview` +- ⬜ [run] PR-A merged as a merge commit (Eliott's yes). Its push run's `release` job reports nothing to release, and no tag beyond `v0.2.0` exists +- ⬜ CONTRIBUTING.md has the sections Commit messages, Integration lanes, Versioning and Branches, and the root README names it +**References**: plan `~/.claude/plans/system-reminder-linked-sessions-now-groovy-waffle.md` § PR-A; writing-release v2.0.0 `references/setting-up.md`, `ci-release-job.md`, `lifecycle.md`, `recovery.md` (first run); `__reports__/windows_install/A2_lifecycle_and_live_split.md` + +## Step 1: Commitizen config from the decided vocabulary +**Goal**: One config holds the vocabulary, the version file and what releases. +**Implementation Logic**: +- `.cz.toml` at the repository root, with `cz_customize`: + - `schema_pattern` from the decided vocabulary, which also accepts Git's `Merge …` subjects; + - `bump_pattern` and `bump_map` per the gates. Confirm on the pinned cz that `refactor` no longer bumps; + - `tag_format = "v$version"`, `major_version_zero = true`, `version_files` pointing at the `"version"` line of `plugins/murmuration/.claude-plugin/plugin.json`, `bump_message = "chore(release): $new_version [skip ci]"`, `update_changelog_on_bump = false`. +- Pin commitizen to 4.19.2 (released 2026-10-07) in the workflow and in every check. +- Never create or push a tag; `v0.2.0` comes from sitting 1 (`git fetch --tags`). +**Deliverables**: `.cz.toml` +**Consistency Checks**: `uvx --from commitizen==4.19.2 cz check --rev-range v0.2.0..HEAD` (expected: PASS) +**Commit**: `build(release): commitizen config for the commit check and the bump` + +## Step 2: CI jobs that check commits and cut the release +**Goal**: The workflow gates PRs and releases after merges, without a hand step. +**Implementation Logic**: +Edit `.github/workflows/guard-binaries.yml` and keep its name. +- Edit `on`, `concurrency`, and the `needs`/`if` lines, and add jobs. Leave the bodies of `build`, `assemble` and `install_smoke`, which belong to `install`. +- `on`: `pull_request`, `push: branches: [main]`, `workflow_dispatch` (keep the `commit_back` input). No path filters. +- `concurrency`: `group: ${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: ${{ github.event_name == 'pull_request' }}`. +- Jobs: + - `changes` outputs `gated`: true when `plugins/murmuration/{rust,scripts,hooks}/**` or the workflow changed. `build` needs it and runs only when it is true. + - `commits` runs `cz check --rev-range` over the PR's base..head, or the push's before..after. + - `preview` (PR only) runs `cz bump --dry-run --yes` and prints the version a merge would cut, or "nothing to release". + - `commit_back` keeps its body. Its `if` stays main-only. + - `release` needs `[changes, commits, install_smoke, commit_back]` and runs on push to main only. Its order: + 1. Fail closed: no `v*` tag means exit 1. + 2. Fetch origin/main. + 3. Check that the manifest `scripts_sha256` and `rust_sources_sha256` equal that tip's sources (`build_manifest.py sources-hash`). + 4. Run `cz --no-raise bump --yes`. + 5. Compare HEAD to the bump commit, per setting-up.md. + 6. Push with `git push --atomic origin HEAD:main "$TAG"`. + 7. Run `gh release create "$TAG" --notes` from `cz changelog --dry-run "$VERSION"`. +- `release` uses `GITHUB_TOKEN` and its own `permissions: contents: write`. Every other job keeps `contents: read`, except `commit_back`, which already writes. +- `.github/workflows/pr-title.yml` runs `cz check --message "$PR_TITLE"`, with the title passed through `env`, never interpolated into the script. +- Pin every action by SHA, with the tag in a comment. +**Deliverables**: `.github/workflows/guard-binaries.yml` (`on`, `concurrency`, jobs `changes`, `commits`, `preview`, `release`, `needs`/`if` of existing jobs); `.github/workflows/pr-title.yml` +**Consistency Checks**: `uv run --no-project --with pyyaml==6.0.2 python -c "import yaml,sys; j=yaml.safe_load(open('.github/workflows/guard-binaries.yml',encoding='utf-8'))['jobs']; sys.exit(0 if {'changes','commits','preview','release'} <= set(j) and j['release'].get('permissions',{}).get('contents')=='write' else 1)"` (expected: PASS) +**Commit**: `ci(release): cut releases from CI after a merge to main` + +## Step 3: CONTRIBUTING and the README +**Goal**: Humans and agents find the lanes and the commit rules in one place. +**Implementation Logic**: +- `CONTRIBUTING.md` at the root (not shipped), with these sections: + - **Commit messages**: the decided vocabulary and what releases. + - **Integration lanes**: + - Reviewed PR: CI and release machinery, campaign landings, persisted contracts (`hooks.json` rows, the data-dir program name, `bin/manifest.json`, the lab layout `lab_init.py` writes), and anything in doubt. + - Self-merge PR: other changes under `plugins/murmuration/**`. + - Direct: the `__*__/` stores, the root README, probes. + - **Versioning**: CI cuts the release; `plugin.json`'s `version` is never edited by hand; `build(bin)` precedes the bump. + - **Branches**. +- Root `README.md`: the repository-layout table names `pr-title.yml` and `CONTRIBUTING.md`, and the line about the workflow (≈151) says it also checks commits and cuts releases. +**Deliverables**: `CONTRIBUTING.md`, `README.md` (repository layout) +**Consistency Checks**: `python3 -I -c "import sys; t=open('CONTRIBUTING.md',encoding='utf-8').read(); sys.exit(0 if all('## '+s in t for s in ('Commit messages','Integration lanes','Versioning','Branches')) else 1)"` (expected: PASS) +**Commit**: `docs(contributing): lanes, commit messages, versioning and branches` + +## Step 4: Land PR-A +**Goal**: The machinery is on `main`, and its first run on `main` released nothing. +**Implementation Logic**: +The coordinator does this step, behind Eliott's yeses. +1. Push `ci/release-lifecycle` and open PR-A, titled `merge(ci): adopt the writing-release lifecycle`, with the PR body as the merge body. +2. Once it is green and Eliott says yes, merge it as a merge commit. +3. Watch the push run in the background. Record that `release` reported nothing to release; a harmless `build(bin)` from the workflow change may appear. If the run is red, re-run it or dispatch. If anything was released, stop and ask Eliott. +4. Pull `main`. +**Deliverables**: results in this leaf's Progress row +**Consistency Checks**: `test "$(git ls-remote --tags origin 'v*' | wc -l | tr -d ' ')" = 1` (expected: PASS) +**Commit**: `merge(ci): adopt the writing-release lifecycle` From db9330fd4fdf11b2ce4dccfeae3350e6f03bcfd6 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Fri, 30 Oct 2026 15:41:37 +0900 Subject: [PATCH 2/6] docs(reports): commit vocabulary findings for the release lifecycle (v0) Measures the 850 subjects on origin/main 3afb2da and proposes the types, id grammar and schema_pattern that release_lifecycle's .cz.toml needs. Decision support for sitting 1; nothing is decided here. Co-Authored-By: Claude Opus 5.5 --- .../release_lifecycle/commit_vocabulary_v0.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 __reports__/release_lifecycle/commit_vocabulary_v0.md diff --git a/__reports__/release_lifecycle/commit_vocabulary_v0.md b/__reports__/release_lifecycle/commit_vocabulary_v0.md new file mode 100644 index 0000000..6a30782 --- /dev/null +++ b/__reports__/release_lifecycle/commit_vocabulary_v0.md @@ -0,0 +1,178 @@ +# Murmuration commit vocabulary — Findings (v0) + +Date: 2026-10-30 + +--- +type: findings +topic: release_lifecycle +date: 2026-10-30 +version: v0 +prior-version: none +key-metric: subjects accepted by the proposed schema_pattern: 302 of 850 (prior: N/A, delta: N/A) +decision-required: confirm +--- + +Scope: `origin/main` at 3afb2da (local ref, not fetched), 850 commits, 2026-10-18 to 2026-10-30. All counts are mine, from `git log --name-only` parsed in Python. + +## Headline Result + +metric: subjects accepted by the proposed `schema_pattern` (every origin/main subject) +value: 302 of 850 (35.5%); 103 of 105 since 2026-10-27 +unit: commits +prior: N/A (first run) +direction: new + +The 548 rejects are 546 old trace-form subjects and 2 conventional ones. They are never rewritten, so only new ranges matter. + +## Results Tables + +### 1. Subject forms in use (850) + +| Form | n | Examples | +|---|---|---| +| `type(scope): ` with a real type | 229 | docs 118, fix 45, feat 16, chore 14, test 13, build 5, merge 5, perf 5, ci 3, revert 3, refactor 2 | +| git `Merge …` | 53 | `Merge task/windows-paths: …` | +| id in the type slot | 362 | `w08-c1: …`, `wave-06: …`, `w09-j: …` | +| kind word in the type slot | 38 | `verdict(w06-c4)` 9, `user_words:` 6, `synthesis:` 6, `report(…)` 4, `lab_status:` 3 | +| no `x: ` form at all | 168 | `wave-09 author-w09-4: puzzles`, `w09 reproducer-w09-4: …` | + +Matching no lexical `word(scope)?: ` form: 221 (168 + 53 Merge). Matching no known type: 621. 37 distinct scopes in the conventional commits. The only variants: `lab_status`/`lab-status`, `user_words`/`user-words`, and one comma scope. + +### 2. Stores: do commits follow the folder? + +| Store | commits touching | only this store | with another store | also touching plugin | +|---|---|---|---|---| +| `__pondus__` | 200 | 94 (47%) | 106 | 13 | +| `__scrupuli__` | 184 | 33 (18%) | 151 | 12 | +| `__statera__` | 172 | 122 (71%) | 50 | 8 | +| `__instrumenta__` | 191 | 28 (15%) | 163 | 33 | +| `__reports__` | 231 | 61 (26%) | 170 | 21 | +| `__roadmap__` | 51 | 43 (84%) | 8 | 3 | +| `__design__` | 9 | 1 | 8 | 0 | +| plugin tree | 157 | 98 | 59 | n/a | + +620 commits touch a trace store; 247 (40%) touch two or more. `__statera__` holds three files: `wave-NN.md` (146 commits), `synthesis.md` (24), `user_words.md` (18). `handbacks/` has no commit yet. Verdicts are sections inside `__pondus__/.md`, so a verdict is an act, not a folder. Of the 200 pondus commits, 62 file a claim, 61 add a verdict, 58 edit a head, 3 record a PI decision alone, and 16 mix acts (12 filing plus verdict, 4 filing plus decision). + +### 3. Research commits that touched shipped code + +| Measure | n | +|---|---| +| old-form subjects touching plugin source | 61 (waves 4 to 8, all before 2026-10-27) | +| same, after the move into `plugins/murmuration/` | 0 | +| feat/fix/perf commits also carrying trace files | 14 | +| non-release types on the plugin tree since the move (43 commits) | test 8, build 5, chore 4, docs 4, refactor 2 | +| revert on the plugin tree | 3 | + +### 4. Era comparison (the 60-day window is the whole history) + +| Period | commits | conventional | git Merge | old trace forms | accepted | +|---|---|---|---|---|---| +| A 10-18 to 10-19 | 95 | 88 | 7 | 0 | 94 | +| B 10-20 to 10-26 (waves 4 to 9) | 650 | 47 | 40 | 563 | 105 | +| C 10-27 to 10-30 (plugin work) | 105 | 94 | 6 | 5 | 103 | + +### 5. Proposed vocabulary + +Evidence is the number of the 562 trace-only commits that the path rule below assigns to each type, except where marked. + +| Type | Scope rule | Paths | Releases | Evidence | +|---|---|---|---|---| +| `feat` | component, kebab or snake | `plugins/murmuration/**` | minor | 16 | +| `fix` | same | same | patch | 45 | +| `perf` | same | same | patch | 5 | +| `docs` `test` `build` `ci` `chore` `refactor` `revert` | same | anywhere; never trace stores | no | 118, 13, 5, 3, 14, 2, 3 | +| `claim` | `wNN-cN` | `__pondus__` head, plus its scripts and puzzles | no | 209 (2 literal) | +| `verdict` | `wNN-cN` | verdict or PI-decision section | no | 82 (9 literal) | +| `wave` | `wNN` | `__statera__/wave-NN.md` | no | 134 | +| `synthesis`, `user_words` | optional `wNN` | those two files | no | 13 (6), 11 (6) | +| `reports` | topic dir or agent id (`w09-i`) | `__reports__/**`, explorer hand-offs | no | 113 | +| `rdm` | campaign dir | `__roadmap__/**` | no | 45 literal | +| `merge` | scope | PR merge subjects | no | 5 literal | +| `Merge …` | none | git's own | no | 53 literal | + +Priority when a commit spans stores: `__pondus__` over `__statera__` over the rest. Only 18 of 562 (3%) need it: 5 pondus with statera, 13 with several statera files. 218 of the 562 need the agent role (author gives claim, reproducer gives verdict, explorer gives reports) read from the subject or file name rather than the path alone. + +## Observations + +| Signal | Baseline / Expected | Observed [source] | Interpretation | +|---|---|---|---| +| Lead form converging | one form by the last wave | `wNN-cN:` leads 78% of wave 4 (43 of 55) and 22% of wave 9 (20 of 89); `wave-NN:` 0% to 34%; wave+agent forms 0% to 29% [source: git log] | Diverging within waves; the period C "convergence" is the trace stores going quiet (6 trace commits: 3 `docs(…)`, 3 `user_words:`). | +| Claim id equals the claim file touched | scope derivable from paths | 138 of 258 subjects led by a claim id touch that file (53%) [source: name-only] | The id names the claim served, not the file; the scope must be written. | +| Wave id available to write | scope writable by agents | 489 of 502 old trace subjects carry a wave id; 44 name two or more claims | One id per commit; a multi-claim commit takes the wave id. | +| Types fitting the existing forms | existing forms reusable | `verdict(wNN-cN)`, `synthesis:`, `user_words:`, `claim(…)` already pass: 22 old subjects | The proposal ratifies what agents already wrote. | +| Hidden shipped change | none | 61 trace-form commits changed plugin source | A trace type must never cover plugin paths. | + +## Charts & Visualizations + +Share of old-form subjects led by `wNN-cN:`, by wave (n in brackets): + +``` +w4 [ 55] ################################ 78% +w5 [ 51] ############################## 73% +w6 [137] #################### 50% +w7 [ 81] ############### 37% +w8 [127] ################### 46% +w9 [ 89] ######### 22% +``` + +## Contradictions & Surprises + +- The brief's "last 60 days against all history" is the same set: the first commit is 2026-10-18, 12 active days. I compared three periods instead. +- `plugins/murmuration/**` exists only since d0c605d (2026-10-27, 46 renames). Before it the plugin lived in 7 root folders, so the research-versus-plugin check returns 0 by construction on the new path. I counted the old root paths (61). +- The repo has no `AGENTS.md`, `CLAUDE.md`, commitizen config, `CONTRIBUTING` or tag (the local clone lists none, although 0.1.1, 0.1.2 and 0.2.0 were released). The three releases are hand-written `chore(release): murmuration X.Y.Z`. +- 846 of 850 commits are by one author; the 4 others are the CI bot's `build(bin)`. + +## Steering Questions + +- [now] D1 to D5 below. +- [now] Approve the regex, or say which types to drop. +- [next run] Add the path check to CI and the rules to the briefs. +- [later] Re-derive after the next research wave, since waves 4 to 9 supplied all the trace evidence. + +## Decisions for Eliott + +| # | Decision | Options | Recommendation and evidence | +|---|---|---|---| +| D1 | What the type follows | folder; act on the primary artifact; one `trace` type | The act. Folders fail: only 15 to 26% of scrupuli, instrumenta and reports commits stand alone. Pondus commits are single-act in 184 of 200. | +| D2 | Own types for `puzzle` and `probe` | add both; add neither | Neither. 82% of scrupuli commits and 85% of instrumenta commits touch another store; the 33 puzzle-only commits are 25 author, 5 PI, 2 reproducer, 1 explorer, so puzzles take the owning act's type. `probe` collides with the top-level `probes/` folder (3 `test(probes)`). The 28 instrumenta-only commits are claim scripts and become `claim(wNN-cN)`. | +| D3 | Id grammar | `wave(09)` against `wave(w09)` | `w09` everywhere (`wave(w09)`, `claim(w09-c8)`, `reports(w09-i)`); the brief's examples mix both. | +| D4 | Plugin guard | rule only; CI path check next to `cz check` | CI check: trace types, `rdm` and `reports` may not touch `plugins/murmuration/**`. cz sees no paths. Also require `fix` to revert a released change (3 reverts today), and note that `build(bin)` ships binaries yet never releases (5). | +| D5 | Release and bot subjects | keep hand-written; configure `bump_message` | Set `bump_message` to `chore(release): murmuration $new_version`; cz's default `bump: …` fails the regex. `build(bin): …` already passes. | +| D6 | Scope required on code types | required; optional | Required. One reject (`chore:` on 10-18) and 227 accepted. | +| D7 | Subject length | none; 120 | None for now: 458 of 850 exceed 72 chars; a 120 cap rejects 42 of 302 accepted. | + +## Writers + +| Writer | Today | What changes | +|---|---|---| +| author, reproducer, explorer, proof-adversary | `briefs.md` rule 5 and `working_defaults.md:30`: "a message that says why", no format | Add the role-to-type table and the id grammar to both lines. | +| research PI | commits wave notes by hand | `wave(wNN)`, `synthesis`, `user_words`; the hook `user_words.py` only writes the file and calls no git. | +| `managing-roadmaps` | generic `()` (`dirtree-execution.md:138`) | name `rdm()`. | +| CI bot | `build(bin): … for ` (workflow line 284) | none. | +| umbod | none: 0 of 850 commits show `close cycle`, `pi-ack` or `__threads__` | None today. Its `commits.rs` emits `style` and `pi-ack()`, which this regex rejects. `SKILL.md` names a `committing-changes` skill I did not find in this session. | + +## schema_pattern (draft) + +``` +(?s)(?:(?:feat|fix|perf|docs|test|build|ci|chore|refactor|revert)\([a-z0-9][a-z0-9_-]*\)!?|(?:claim|verdict)\(w\d{2}-c\d+\)|wave\(w\d{2}\)|(?:synthesis|user_words)(?:\(w\d{2}\))?|(?:reports|rdm|merge)\([a-z0-9][a-z0-9_-]*\)):( [^\n\r]+)((\n\n.*)|(\s*))?$|Merge .* +``` + +Tested with Python `re.match` (cz is not installed here, so `cz check` itself did not run). Run on the 850 subjects: 302 accepted, 548 rejected. Rejected: 546 old trace forms and 2 conventional. + +| Reject sample | Why | +|---|---| +| `w08-c11: fenced blocks are text; …` | id in type slot | +| `wave-07 reproducer-w07-2: live runs, …` | no type, no colon after it | +| `w04-c10 verdict (reproducer-w04-10), …` | no type | +| `wave-08 report v0 (two placeholders …)` | no type | +| `fix(wave-05, research-loop): reassessment times …` | comma scope | +| `chore: add the AGPL-3.0 license and gitignore` | scope missing | + +Unit checks pass: `feat(guard)!: x`, a body with a trailer, `verdict(w08-c1): SUPPORTED`, `merge(guard): x`, `Merge pull request #1 from a/b`. They fail as intended: `wave(09): x`, `claim(w08): x`, `bump: version …`, an empty subject. The regex needs `allowed_prefixes` only if a `Revert "…"` subject is wanted. + +## Pointers + +- Method: `writing-history` 1.2.0 `references/convention-setup.md`. +- Brief rules: `plugins/murmuration/skills/research-loop/references/briefs.md` (lines 26 to 28), `working_defaults.md` (line 30). +- CI: `.github/workflows/guard-binaries.yml`. +- Move commit: d0c605d. Release commits: b511983, 72a61d0, 1f164ae. From d3bdd6ad67c48a9b801056eb0e8b00cd0b9c351d Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Fri, 30 Oct 2026 17:27:40 +0900 Subject: [PATCH 3/6] rdm(windows_install): record sitting 1: A2 approved, vocabulary decided Eliott approved A2, adopted the commit vocabulary report as recommended (D4 as a CI path check, bump_message keeping [skip ci]), and said yes to the v0.2.0 tag and the PR merge settings. The tag is on origin at 1f164ae; the settings read back as applied. A2's nodes move to planned. Co-Authored-By: Claude Opus 5.5 --- .../windows_install/next/next/next/README.md | 4 ++-- .../windows_install/next/next/next/next/README.md | 4 ++-- .../next/next/next/next/next/README.md | 4 ++-- .../windows_install/next/next/next/next/release.md | 4 ++-- .../next/next/next/release_lifecycle.md | 13 +++++++------ 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/__roadmap__/windows_install/next/next/next/README.md b/__roadmap__/windows_install/next/next/next/README.md index 0cb5f09..80d84db 100644 --- a/__roadmap__/windows_install/next/next/next/README.md +++ b/__roadmap__/windows_install/next/next/next/README.md @@ -17,7 +17,7 @@ The install proven in CI on macOS, Linux and Windows, and releases cut by CI. graph TD install[Install path and first Windows session]:::planned next[Release]:::planned - release_lifecycle[Adopt the writing-release lifecycle]:::amendment + release_lifecycle[Adopt the writing-release lifecycle]:::planned classDef done fill:#166534,color:#bbf7d0 classDef inprogress fill:#854d0e,color:#fef08a classDef planned fill:#374151,color:#e5e7eb @@ -30,7 +30,7 @@ graph TD |:-----|:-----|:-------| | `install.md` | 📄 Leaf Task | ⬜ Planned | | `next/` | 📁 Directory | ⬜ Planned | -| `release_lifecycle.md` | 📄 Leaf Task | 🔵 Amendment | +| `release_lifecycle.md` | 📄 Leaf Task | ⬜ Planned | ## Amendment Log | ID | Date | Source | Nodes Added | Rationale | diff --git a/__roadmap__/windows_install/next/next/next/next/README.md b/__roadmap__/windows_install/next/next/next/next/README.md index 1276a46..985c6e3 100644 --- a/__roadmap__/windows_install/next/next/next/next/README.md +++ b/__roadmap__/windows_install/next/next/next/next/README.md @@ -16,7 +16,7 @@ Behind Eliott's go-aheads: PR-B merges, CI commits the binaries for 5 targets an ```mermaid graph TD release[0.2.0 release with Windows support]:::planned - next[Live Windows session]:::amendment + next[Live Windows session]:::planned classDef done fill:#166534,color:#bbf7d0 classDef inprogress fill:#854d0e,color:#fef08a classDef planned fill:#374151,color:#e5e7eb @@ -28,7 +28,7 @@ graph TD | Node | Type | Status | |:-----|:-----|:-------| | `release.md` | 📄 Leaf Task | ⬜ Planned | -| `next/` | 📁 Directory | 🔵 Amendment | +| `next/` | 📁 Directory | ⬜ Planned | ## Amendment Log | ID | Date | Source | Nodes Added | Rationale | diff --git a/__roadmap__/windows_install/next/next/next/next/next/README.md b/__roadmap__/windows_install/next/next/next/next/next/README.md index c4eabee..e74a8d2 100644 --- a/__roadmap__/windows_install/next/next/next/next/next/README.md +++ b/__roadmap__/windows_install/next/next/next/next/next/README.md @@ -15,7 +15,7 @@ Murmuration observed working in a real Windows session. ## Status ```mermaid graph TD - live_session[Windows install and session from Nest]:::amendment + live_session[Windows install and session from Nest]:::planned classDef done fill:#166534,color:#bbf7d0 classDef inprogress fill:#854d0e,color:#fef08a classDef planned fill:#374151,color:#e5e7eb @@ -26,7 +26,7 @@ graph TD ## Nodes | Node | Type | Status | |:-----|:-----|:-------| -| `live_session.md` | 📄 Leaf Task | 🔵 Amendment | +| `live_session.md` | 📄 Leaf Task | ⬜ Planned | ## Amendment Log | ID | Date | Source | Nodes Added | Rationale | diff --git a/__roadmap__/windows_install/next/next/next/next/release.md b/__roadmap__/windows_install/next/next/next/next/release.md index 354d858..cef99c4 100644 --- a/__roadmap__/windows_install/next/next/next/next/release.md +++ b/__roadmap__/windows_install/next/next/next/next/release.md @@ -6,7 +6,7 @@ - [ ] `release_lifecycle` done (PR-A merged, first push run released nothing) **Success Gates**: - ⬜ [run] PR-B merged as a merge commit (Eliott's yes, given when PR-B is green ×3 OS and its `preview` printed 0.3.0) -- ⬜ [run] The push run: `build(bin)` by `github-actions[bot]` carries all 5 targets; `install_smoke` green ×3 OS; `release` committed `chore(release): 0.3.0 [skip ci]`, pushed tag `v0.3.0` and created the GitHub Release +- ⬜ [run] The push run: `build(bin)` by `github-actions[bot]` carries all 5 targets; `install_smoke` green ×3 OS; `release` committed `chore(release): murmuration 0.3.0 [skip ci]`, pushed tag `v0.3.0` and created the GitHub Release - ⬜ Manifest hashes on `v0.3.0` equal its sources (`build_manifest.py sources-hash`); `plugin.json` version 0.3.0 only on that commit - ⬜ Nest unchanged (its entry names no version) - ⬜ [behavioral] Installed from Nest in a scratch project on macOS: cache holds 0.3.0; data dir `murmuration.exe` equals CI's aarch64-apple-darwin binary and `murmuration.exe.verified` exists; no launcher note; one Agent call in a scratch lab appends `harness.jsonl` lines; 0.2.0's `bin/murmuration` untouched @@ -27,4 +27,4 @@ Coordinator, behind Eliott's conditional yes: after PR-A merges, PR-B retargets In a scratchpad project, at project scope: `claude plugin marketplace update`, `claude plugin install murmuration@cracking-shells --scope project`, one headless session that runs `/lab` and one Agent call; read the data dir and the cache. Write the KT note (writing-reports, knowledge-transfer). `live_session` stays open on reminder `murmuration-win-live`. **Deliverables**: `__reports__/windows_install/kt_windows_install.md`; Progress rows in this campaign's READMEs **Consistency Checks**: `test -f __reports__/windows_install/kt_windows_install.md` (expected: PASS) -**Commit**: `docs(roadmap): record the 0.3.0 release and the install from Nest` +**Commit**: `rdm(windows_install): record the 0.3.0 release and the install from Nest` diff --git a/__roadmap__/windows_install/next/next/next/release_lifecycle.md b/__roadmap__/windows_install/next/next/next/release_lifecycle.md index cd5667c..8308ad0 100644 --- a/__roadmap__/windows_install/next/next/next/release_lifecycle.md +++ b/__roadmap__/windows_install/next/next/next/release_lifecycle.md @@ -3,10 +3,11 @@ **Goal**: Releases of `plugins/murmuration` are cut only by CI after a merge to `main`. The commit checker, the release job, CONTRIBUTING and the repository settings follow writing-release v2.0.0, so 0.3.0 is the first release nobody cuts by hand. **Pre-conditions**: - [x] `retire_guard` done (A1) -- [ ] Commit vocabulary decided by Eliott (`__reports__/release_lifecycle/commit_vocabulary_v0.md`, sitting 1) -- [ ] Tag `v0.2.0` at 1f164ae pushed, and repository settings applied and read back (Eliott's yes, sitting 1) +- [x] Commit vocabulary decided by Eliott (sitting 1, 2026-10-30): `__reports__/release_lifecycle/commit_vocabulary_v0.md` D1–D7 as recommended, D4 as a CI check, `bump_message` keeps `[skip ci]` +- [x] Tag `v0.2.0` at 1f164ae pushed, and repository settings applied and read back (Eliott's yes, sitting 1): PR merges are merge commits only, titled by the PR title with the PR body, branch deleted on merge; no branch protection, so the Direct lane still pushes **Success Gates**: -- ⬜ `cz check --rev-range` accepts every origin/main subject of the last 60 days that the decided vocabulary keeps, and rejects the forms it retires +- ⬜ `cz check` accepts the subjects the report's draft regex accepts (302 of 850 on 3afb2da, 103 of the 105 since 2026-10-27) and rejects its reject samples; the report's unit checks pass under the pinned cz +- ⬜ The `commits` job fails a range in which a trace type (`claim`, `verdict`, `wave`, `synthesis`, `user_words`, `reports`, `rdm`) touches `plugins/murmuration/**` (D4) - ⬜ The `bump_map` releases only on feat (minor), fix and perf (patch), and `BREAKING CHANGE` (minor while 0.x). `refactor`, `docs`, `merge`, `rdm`, `reports` and the research types never bump, checked by `cz bump --dry-run` in a scratch clone - ⬜ The `release` job fails closed. It does not run after a red matrix (it tolerates `skipped` only when `changes` found no gated path). It exits 1 when no `v*` tag exists. It checks the manifest hashes against the new tip of origin/main, uses `git push --atomic`, and grants `contents: write` to itself alone - ⬜ [run] PR-A's `pull_request` run green, including `commits`, `pr-title` and `preview` @@ -18,9 +19,9 @@ **Goal**: One config holds the vocabulary, the version file and what releases. **Implementation Logic**: - `.cz.toml` at the repository root, with `cz_customize`: - - `schema_pattern` from the decided vocabulary, which also accepts Git's `Merge …` subjects; + - `schema_pattern`: the report's draft regex (§ schema_pattern), which also accepts Git's `Merge …` subjects; scope required on code types (D6), no length cap (D7); - `bump_pattern` and `bump_map` per the gates. Confirm on the pinned cz that `refactor` no longer bumps; - - `tag_format = "v$version"`, `major_version_zero = true`, `version_files` pointing at the `"version"` line of `plugins/murmuration/.claude-plugin/plugin.json`, `bump_message = "chore(release): $new_version [skip ci]"`, `update_changelog_on_bump = false`. + - `tag_format = "v$version"`, `major_version_zero = true`, `version_files` pointing at the `"version"` line of `plugins/murmuration/.claude-plugin/plugin.json`, `bump_message = "chore(release): murmuration $new_version [skip ci]"` (D5), `update_changelog_on_bump = false`. - Pin commitizen to 4.19.2 (released 2026-10-07) in the workflow and in every check. - Never create or push a tag; `v0.2.0` comes from sitting 1 (`git fetch --tags`). **Deliverables**: `.cz.toml` @@ -36,7 +37,7 @@ Edit `.github/workflows/guard-binaries.yml` and keep its name. - `concurrency`: `group: ${{ github.workflow }}-${{ github.ref }}`, `cancel-in-progress: ${{ github.event_name == 'pull_request' }}`. - Jobs: - `changes` outputs `gated`: true when `plugins/murmuration/{rust,scripts,hooks}/**` or the workflow changed. `build` needs it and runs only when it is true. - - `commits` runs `cz check --rev-range` over the PR's base..head, or the push's before..after. + - `commits` runs `cz check --rev-range` over the PR's base..head, or the push's before..after. A second step fails when a commit in that range whose type is a trace type touches `plugins/murmuration/**` (D4; cz sees no paths). Keep it a short inline script or one under `.github/`, not in the plugin tree. - `preview` (PR only) runs `cz bump --dry-run --yes` and prints the version a merge would cut, or "nothing to release". - `commit_back` keeps its body. Its `if` stays main-only. - `release` needs `[changes, commits, install_smoke, commit_back]` and runs on push to main only. Its order: From 5f9a8a9debb40470f0c47ba3832de2b985ef7709 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Fri, 30 Oct 2026 17:33:28 +0900 Subject: [PATCH 4/6] build(release): commitizen config for the commit check and the bump One file holds the decided vocabulary (report D1-D7), the version file and what releases, so CI's `cz check` and `cz bump` read the same rules. Only feat, fix, perf and a breaking change bump; the research types, docs, refactor, merge and rdm never do. The scm provider reads the last v* tag, and the bump rewrites the version line of plugin.json. Co-Authored-By: Claude Opus 5.5 --- .cz.toml | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .cz.toml diff --git a/.cz.toml b/.cz.toml new file mode 100644 index 0000000..d4ede39 --- /dev/null +++ b/.cz.toml @@ -0,0 +1,57 @@ +# Commitizen config for the commit check (cz check) and the CI bump (cz bump). +# Pinned in CI and in every local check: uvx --from commitizen==4.19.2 cz ... +# The vocabulary is __reports__/release_lifecycle/commit_vocabulary_v0.md (D1-D7); how to write a message is in CONTRIBUTING.md. +# Nobody runs `cz bump` by hand: the `release` job of .github/workflows/guard-binaries.yml does, after a merge to main. +[tool.commitizen] +name = "cz_customize" + +# the version is the latest `v*` tag; the only file the bump rewrites is the plugin manifest below +version_provider = "scm" +version_scheme = "semver" +tag_format = "v$version" +annotated_tag = true +major_version_zero = true +update_changelog_on_bump = false +version_files = ['plugins/murmuration/.claude-plugin/plugin.json:"version"'] +bump_message = "chore(release): murmuration $new_version [skip ci]" + +# the schema_pattern below is the only authority: no `Merge`, `Revert`, `fixup!` or `squash!` shortcut (git's own `Merge ...` subjects are in the pattern) +allowed_prefixes = [] + +[tool.commitizen.customize] +example = "fix(hooks): stop a verdict hook from reading a closed pipe" +schema = "(): , or (): ; see CONTRIBUTING.md" +# feat fix perf docs test build ci chore refactor revert: scope required (D6), `!` allowed +# claim verdict: (wNN-cN) wave: (wNN) synthesis user_words: optional (wNN) reports rdm merge: scope required git's own `Merge ...` +# no length cap (D7) +schema_pattern = '(?s)(?:(?:feat|fix|perf|docs|test|build|ci|chore|refactor|revert)\([a-z0-9][a-z0-9_-]*\)!?|(?:claim|verdict)\(w\d{2}-c\d+\)|wave\(w\d{2}\)|(?:synthesis|user_words)(?:\(w\d{2}\))?|(?:reports|rdm|merge)\([a-z0-9][a-z0-9_-]*\)):( [^\n\r]+)((\n\n.*)|(\s*))?$|Merge .*' + +# only these release: feat (minor), fix and perf (patch), a breaking `!` on those, and a BREAKING CHANGE footer (minor while major_version_zero). +# Every other type fails the pattern, so refactor, docs, test, build, ci, chore, revert, merge, rdm, reports and the research types never bump. +# cz searches each line of a message, so a footer line `BREAKING CHANGE: ...` counts. +bump_pattern = '^((BREAKING[\-\ ]CHANGE|feat|fix|perf)(\(.+\))?!?):' + +# release notes (cz changelog --dry-run): the three release types and breaking changes only +changelog_pattern = '^((BREAKING[\-\ ]CHANGE|feat|fix|perf)(\(.+\))?!?):' +commit_parser = '^((?Pfeat|fix|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?):\s(?P.*)?' +change_type_order = ["BREAKING CHANGE", "Feat", "Fix", "Perf"] + +[tool.commitizen.customize.change_type_map] +feat = "Feat" +fix = "Fix" +perf = "Perf" + +[tool.commitizen.customize.bump_map] +'^BREAKING[\-\ ]CHANGE' = "MAJOR" +'^(feat|fix|perf)(\(.+\))?!$' = "MAJOR" +'^feat' = "MINOR" +'^fix' = "PATCH" +'^perf' = "PATCH" + +# major_version_zero: a MAJOR is cut as a MINOR +[tool.commitizen.customize.bump_map_major_version_zero] +'^BREAKING[\-\ ]CHANGE' = "MINOR" +'^(feat|fix|perf)(\(.+\))?!$' = "MINOR" +'^feat' = "MINOR" +'^fix' = "PATCH" +'^perf' = "PATCH" From c5d3737f117db60f3de3eaabc6e18dc08cae548d Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Fri, 30 Oct 2026 17:40:00 +0900 Subject: [PATCH 5/6] ci(release): cut releases from CI after a merge to main The workflow now runs on pull requests and on every push to main, with a `changes` job gating the binary build, a `commits` job (cz check plus the D4 path check), a `preview` of the version a merge would cut, and a `release` job that fails closed, checks the manifest against the tip, bumps, pushes branch and tag atomically and creates the GitHub Release. pr-title.yml checks the title that becomes the merge subject. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/check_trace_paths.py | 43 +++++ .github/scripts/manifest_matches.py | 43 +++++ .github/workflows/guard-binaries.yml | 262 ++++++++++++++++++++++++++- .github/workflows/pr-title.yml | 28 +++ 4 files changed, 370 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/check_trace_paths.py create mode 100644 .github/scripts/manifest_matches.py create mode 100644 .github/workflows/pr-title.yml diff --git a/.github/scripts/check_trace_paths.py b/.github/scripts/check_trace_paths.py new file mode 100644 index 0000000..86089d4 --- /dev/null +++ b/.github/scripts/check_trace_paths.py @@ -0,0 +1,43 @@ +"""Fail when a commit typed as research trace touches the shipped plugin (D4 of __reports__/release_lifecycle/commit_vocabulary_v0.md). + + python3 -I .github/scripts/check_trace_paths.py + +`cz check` reads messages and sees no paths, so this is the other half of the vocabulary: a commit whose type is one of TRACE and that changes a file under plugins/murmuration/ +would ship code under a type that never releases. Merge commits are skipped (their files belong to the commits they merge). Standard library only; lives outside plugins/murmuration so that it is not shipped. +""" +import re +import subprocess +import sys + +TRACE = {"claim", "verdict", "wave", "synthesis", "user_words", "reports", "rdm", "merge"} +SHIPPED = "plugins/murmuration/" + + +def git(*args): + return subprocess.run(["git", *args], check=True, capture_output=True, text=True, encoding="utf-8").stdout + + +def main(argv): + if len(argv) != 2: + print(__doc__) + return 2 + bad = 0 + for sha in git("rev-list", "--no-merges", "--reverse", argv[1]).split(): + subject = git("log", "-1", "--format=%s", sha).rstrip("\n") + m = re.match(r"([a-z_]+)(?:\(|:|!)", subject) + if not m or m.group(1) not in TRACE: + continue + files = [f for f in git("diff-tree", "--no-commit-id", "--name-only", "-r", "--root", "-z", sha).split("\0") if f] + hits = [f for f in files if f.startswith(SHIPPED)] + if hits: + bad += 1 + more = " (and %d more)" % (len(hits) - 5) if len(hits) > 5 else "" + print("::error title=trace type on the shipped plugin::%s %s: type `%s` releases nothing, yet it changes %s%s. Use feat, fix, perf, refactor, test, build, ci, chore or docs for plugin changes, or split the commit." % (sha[:7], subject, m.group(1), ", ".join(hits[:5]), more)) + if bad: + return 1 + print("check_trace_paths: no trace-typed commit in %s touches %s" % (argv[1], SHIPPED)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.github/scripts/manifest_matches.py b/.github/scripts/manifest_matches.py new file mode 100644 index 0000000..0547259 --- /dev/null +++ b/.github/scripts/manifest_matches.py @@ -0,0 +1,43 @@ +"""Exit 0 when plugins/murmuration/bin/manifest.json was written from the sources in this checkout, else 1. + + python3 -I .github/scripts/manifest_matches.py + +Compares scripts_sha256 and rust_sources_sha256, the two fields install_binary.py trusts, against what build_manifest.py computes from the tree now, and requires a binary for every target in ALL_TARGETS. +Used by the `changes` job (a stale manifest makes a push gated) and by the `release` job (never release a tree whose binaries are not its own). +Standard library only; lives outside plugins/murmuration so that it is not shipped. +""" +import json +import os +import sys + +ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")) +PLUGIN = os.path.join(ROOT, "plugins", "murmuration") +KEYS = ("scripts_sha256", "rust_sources_sha256") + + +def main(): + sys.dont_write_bytecode = True # no __pycache__ in the plugin tree: the release job bumps a clean checkout + sys.path.insert(0, os.path.join(PLUGIN, "scripts")) + import build_manifest + + try: + with open(os.path.join(PLUGIN, "bin", "manifest.json"), encoding="utf-8") as fh: + have = json.load(fh) + except (OSError, ValueError) as err: + print("manifest_matches: cannot read bin/manifest.json: %s" % err) + return 1 + now = build_manifest.build(PLUGIN) + differ = [k for k in KEYS if have.get(k) != now[k]] + missing = [t for t in build_manifest.ALL_TARGETS if t not in (have.get("binaries") or {})] + if missing: + print("manifest_matches: bin/manifest.json has no binary for %s" % ", ".join(missing)) + return 1 + if differ: + print("manifest_matches: bin/manifest.json differs from the sources in %s: %s" % (", ".join(differ), PLUGIN)) + return 1 + print("manifest_matches: bin/manifest.json matches the sources (rust %s)" % now["rust_sources_sha256"][:12]) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/guard-binaries.yml b/.github/workflows/guard-binaries.yml index 2b2eae8..156262a 100644 --- a/.github/workflows/guard-binaries.yml +++ b/.github/workflows/guard-binaries.yml @@ -1,15 +1,16 @@ # Builds the `murmuration` hook binary on five runners, checks each against the Python hooks, and commits bin/ back to main when everything agrees. Cases: # - main moves during the run: commit_back fetches origin/main, builds on that tip, commits only if the hashed sources (rust_sources_sha256, scripts_sha256) are unchanged, else exits 0 (the newer push runs its own build); it retries a rejected push 3 times. -# - commit-back loop: the commit touches only plugins/murmuration/bin/**, outside the `push` paths filter, and a GITHUB_TOKEN push starts no workflow. -# - token scope: contents: read everywhere; only commit_back has contents: write, and only on refs/heads/main; its concurrency group bin-commit serialises commits without cancelling one. +# - commit-back loop: a GITHUB_TOKEN push starts no workflow, and the bump commit carries [skip ci]. +# - token scope: contents: read everywhere; only commit_back and release have contents: write, and only on refs/heads/main; commit_back's concurrency group bin-commit serialises commits without cancelling one. # - an artefact under the wrong target name: assemble checks that each murmuration- holds exactly , sources.sha256 and a file the `file` command reports as 's architecture. # - Windows absent or faulty: assemble detects it from its artefact (missing, wrong name or type, other sources) and leaves it out with a warning; no continue-on-error. Any other target missing fails the job. # - a wrong-architecture binary: each build row checks `file` before upload (and assemble again); a runner label that moved to another architecture fails there. name: guard-binaries +# No path filter on a trigger: the commit check must run on every push to main, and a workflow skipped by `paths` leaves its checks pending. `changes` filters per job instead. on: + pull_request: push: - # every file the manifest hashes lives under these two folders; plugins/murmuration/bin/** is left out so that a commit of binaries cannot start another run - paths: ["plugins/murmuration/rust/**", "plugins/murmuration/scripts/**"] + branches: [main] workflow_dispatch: inputs: commit_back: @@ -20,13 +21,57 @@ on: permissions: contents: read +# A superseded PR run is cancelled; a run on main never is (a cancelled release can leave a tag without its Release). +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: PLUGIN: plugins/murmuration # the contract's ALL_TARGETS, in the order the matrix lists them ALL_TARGETS: "aarch64-apple-darwin x86_64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-pc-windows-msvc" jobs: + # Does this event touch what the binaries are built from? A job skipped by `if` reports success, so the build matrix does not block a docs-only PR. + changes: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + outputs: + gated: ${{ steps.diff.outputs.gated }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - id: diff + env: + BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + gated=true + why="no base to compare with (a manual run, a new branch or a rewritten history): everything is gated" + if [ -n "${BASE//0/}" ] && git cat-file -e "$BASE^{commit}" 2>/dev/null && files=$(git diff --name-only "$BASE...$HEAD" 2>/dev/null); then + if grep -Eq '^(plugins/murmuration/(rust|scripts|hooks)/|\.github/workflows/guard-binaries\.yml$)' <<< "$files"; then + why="plugins/murmuration/{rust,scripts,hooks}/** or this workflow changed" + elif [ "$GITHUB_EVENT_NAME" != pull_request ] && ! python3 -I .github/scripts/manifest_matches.py; then + # the build run of an earlier push was replaced or lost, so bin/ is behind its sources: build now, or every later release would fail closed + why="bin/manifest.json does not match the sources at this commit" + else + gated=false + why="nothing under plugins/murmuration/{rust,scripts,hooks}/** or this workflow changed" + fi + fi + echo "gated=$gated ($why)" + echo "gated=$gated" >> "$GITHUB_OUTPUT" + build: + needs: changes + if: ${{ needs.changes.outputs.gated == 'true' }} # cargo test, an LTO release build and the hook self-test take well under 15 min even on the slow runners; 30 leaves room without letting a hung row hold commit_back for GitHub's 6 h default timeout-minutes: 30 permissions: @@ -86,8 +131,9 @@ jobs: assemble: # `!cancelled()` and not `success()`: a red Windows row must not skip this job. It decides itself which rows it can do without (Windows only). - needs: build - if: ${{ !cancelled() }} + # `changes` is named so that the job is skipped, not failed, when `build` was skipped + needs: [changes, build] + if: ${{ !cancelled() && needs.changes.outputs.gated == 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: @@ -288,3 +334,207 @@ jobs: done echo "::error::main kept moving; bin/ not committed" exit 1 + + # Every commit in the range is a valid message of the vocabulary (.cz.toml), and no trace type touches the shipped plugin (D4). Runs on every event, whatever changed. + commits: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + - run: python3 -m pip install --disable-pip-version-check commitizen==4.19.2 + # cz check needs an identity as soon as it creates anything under .git + - run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - id: range + env: + PR_BASE: ${{ github.event.pull_request.base.sha }} + PR_HEAD: ${{ github.event.pull_request.head.sha }} + BEFORE: ${{ github.event.before }} + run: | + set -euo pipefail + # a PR: its commits; a push: the commits it brought; a manual run, or a push whose predecessor is gone (new branch, rewritten history): everything since the last release + if [ "$GITHUB_EVENT_NAME" = pull_request ]; then + range="$PR_BASE..$PR_HEAD" + elif [ "$GITHUB_EVENT_NAME" = push ] && [ -n "${BEFORE//0/}" ] && git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then + range="$BEFORE..$GITHUB_SHA" + else + last=$(git -c versionsort.suffix=- tag --list 'v*' --sort=-version:refname | head -1) + range="${last:+$last..}$GITHUB_SHA" + fi + echo "range=$range" + echo "range=$range" >> "$GITHUB_OUTPUT" + echo "count=$(git rev-list --count "$range")" >> "$GITHUB_OUTPUT" + - name: cz check over the range + if: steps.range.outputs.count != '0' + env: + RANGE: ${{ steps.range.outputs.range }} + run: cz check --rev-range "$RANGE" + - name: No trace type touches plugins/murmuration/** (D4) + if: steps.range.outputs.count != '0' + env: + RANGE: ${{ steps.range.outputs.range }} + run: python3 -I .github/scripts/check_trace_paths.py "$RANGE" + + # What would merging this PR cut? Informs the reviewer; changes nothing. + preview: + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + - run: python3 -m pip install --disable-pip-version-check commitizen==4.19.2 + - name: The version a merge would cut + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + set -euo pipefail + # the checkout is the merge of the PR into main; GitHub will give the merge commit this title and body, and cz reads every line of both + git -c user.name=preview -c user.email=preview@invalid commit --allow-empty -q -m "$PR_TITLE" -m "$PR_BODY" + rc=0 + out=$(cz bump --dry-run --yes 2>&1) || rc=$? + echo "$out" + # cz's own codes: 21 no commit warrants a bump, 3 no commit since the tag + case "$rc" in + 0) msg="a merge would release $(sed -n 's/^tag to create: //p' <<< "$out")" ;; + 3|21) msg="nothing to release" ;; + *) echo "::error::cz bump --dry-run exited $rc"; exit "$rc" ;; + esac + echo "::notice title=release preview::$msg" + echo "release preview: $msg" >> "$GITHUB_STEP_SUMMARY" + + # Cuts the release after a merge: bump commit, tag, GitHub Release. The only job besides commit_back that may write, and it runs on main alone. + release: + # build is named so that a red matrix (Windows included) stops the release; install_smoke and commit_back are named so that none of them was skipped, unless `changes` found nothing to build + needs: [changes, commits, build, install_smoke, commit_back] + # `!cancelled()` so that this job runs when a gate was skipped, and every requirement re-stated because a status function also runs the job after a failure. A manual run releases only when it also commits bin/. + if: >- + ${{ !cancelled() + && github.ref == 'refs/heads/main' + && github.event_name != 'pull_request' + && needs.changes.result == 'success' + && needs.commits.result == 'success' + && ((needs.changes.outputs.gated == 'true' + && needs.build.result == 'success' + && needs.install_smoke.result == 'success' + && needs.commit_back.result == 'success') + || (needs.changes.outputs.gated != 'true' + && needs.build.result == 'skipped' + && needs.install_smoke.result == 'skipped' + && needs.commit_back.result == 'skipped')) }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + - run: python3 -m pip install --disable-pip-version-check commitizen==4.19.2 + - run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: 1. No v* tag means no baseline; fail closed + run: | + set -euo pipefail + git fetch --tags origin + last=$(git -c versionsort.suffix=- tag --list 'v*' --sort=-version:refname | head -1) + if [ -z "$last" ]; then + echo "::error::no v* tag in this repository: cz would count every commit as unreleased. Seed the baseline tag by hand (writing-release recovery.md, first release); nothing was released." + exit 1 + fi + echo "last release: $last" + - name: 2. Check out the current tip of origin/main + run: | + set -euo pipefail + git fetch --no-tags origin main + # the commit_back job may have pushed bin/ after this run started, and so may anyone else + git checkout -q -f -B main origin/main + last=$(git -c versionsort.suffix=- tag --list 'v*' --sort=-version:refname | head -1) + if ! git merge-base --is-ancestor "$last" HEAD; then + echo "::error::the last tag $last is not an ancestor of origin/main ($(git rev-parse --short HEAD)); cz would bump from the wrong baseline. Nothing was released." + exit 1 + fi + git log --oneline "$last..HEAD" | head -20 + - name: 3. The binaries in bin/ were built from this tip's sources + run: | + if ! python3 -I .github/scripts/manifest_matches.py; then + echo "::error::bin/manifest.json is not from the sources at origin/main $(git rev-parse --short HEAD). A push landed during the run, or a build was lost; the run for that push releases, or dispatch this workflow on main with commit_back. Nothing was released." + exit 1 + fi + - name: 4. Bump + id: bump + run: | + set -euo pipefail + before=$(git rev-parse HEAD) + # exit codes of cz 4.19.2: 21 NO_INCREMENT (no commit warrants a bump), 3 NO_COMMITS_FOUND (HEAD is the last tag) + cz --no-raise 3,21 bump --yes + # 5. HEAD has not moved: nothing to release + if [ "$(git rev-parse HEAD)" = "$before" ]; then + echo "::notice title=release::nothing to release since $(git describe --tags --abbrev=0)" + exit 0 + fi + tag=$(git describe --tags --exact-match HEAD) + files=$(git diff --name-only HEAD~1 HEAD) + if [ "$files" != "plugins/murmuration/.claude-plugin/plugin.json" ]; then + echo "::error::the bump commit changed more than the plugin manifest: $files" + exit 1 + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + git log -1 --format='bump commit %h: %s' + # one atomic push: the branch and the tag land together, or neither does (main moved since step 2) + - name: 6. Push the bump commit and the tag + if: steps.bump.outputs.tag != '' + env: + TAG: ${{ steps.bump.outputs.tag }} + run: git push --atomic origin HEAD:main "refs/tags/$TAG" + # runs after a bump and also when there is nothing to bump, so that a tag whose Release failed to be created (step 7 after the push of step 6) is repaired by the next run or dispatch + - name: 7. GitHub Release + env: + TAG: ${{ steps.bump.outputs.tag }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + create=1 + if [ -z "$TAG" ]; then + TAG=$(git -c versionsort.suffix=- tag --list 'v*' --sort=-version:refname | head -1) + if gh release view "$TAG" > /dev/null 2>&1; then + echo "::notice title=release::$TAG already has its GitHub Release" + create=0 + else + echo "::warning title=release::$TAG has no GitHub Release (an earlier run failed after its push); creating it" + fi + fi + if [ "$create" = 1 ]; then + cz changelog --dry-run "${TAG#v}" > "$RUNNER_TEMP/notes.md" + if [ ! -s "$RUNNER_TEMP/notes.md" ]; then printf 'Release %s\n' "$TAG" > "$RUNNER_TEMP/notes.md"; fi + cat "$RUNNER_TEMP/notes.md" + gh release create "$TAG" --verify-tag --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md" + fi diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..3c3ff40 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,28 @@ +# The PR title becomes the merge commit's subject, so it must be a message of the vocabulary in .cz.toml; CONTRIBUTING.md asks for `merge()`, a type that releases nothing of its own. +# A separate workflow: `edited` also fires on body edits, and in guard-binaries.yml it would cancel the gates of the same commit. +name: pr-title +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + title: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + - run: python3 -m pip install --disable-pip-version-check commitizen==4.19.2 + - name: The title is a message of the vocabulary + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: cz check --message "$PR_TITLE" From 192dc57e32a0c9b93640c8c3ab65137fa2080652 Mon Sep 17 00:00:00 2001 From: Eliott Jacopin Date: Fri, 30 Oct 2026 17:41:03 +0900 Subject: [PATCH 6/6] docs(contributing): lanes, commit messages, versioning and branches A fresh session, or an agent, should derive the lifecycle from one file: the types and what they release, which lane a change takes, that CI cuts every release and plugin.json's version is never hand-edited, and how branches and PR titles become merge commits. The README layout table now names the new workflow and the guide. Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 +++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a07fe91 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing + +How a change reaches `main` and becomes a release. The plugin is `plugins/murmuration/`; everything else in the repository is the research record that designed it. CI cuts every release; nobody tags or bumps by hand. + +## Commit messages + +`(): `, then a body that says why. The convention is checked by commitizen (`.cz.toml`, pinned to 4.19.2) in the `commits` job of `.github/workflows/guard-binaries.yml`, over the commits of each pull request and of each push to `main`, and by `.github/workflows/pr-title.yml` over the PR title. Check a range or a message yourself: + +```bash +uvx --from commitizen==4.19.2 cz check --rev-range origin/main..HEAD +uvx --from commitizen==4.19.2 cz check --message "fix(hooks): stop reading a closed pipe" +``` + +The scope is required. There is no length cap. A trailing `!` after the scope (`feat(hooks)!:`) or a `BREAKING CHANGE:` footer marks a breaking change. + +| Type | Scope | Used for | Releases | +|:--|:--|:--|:--| +| `feat` | the component, kebab or snake case (`hooks`, `lab_init`) | a new capability of the plugin, under `plugins/murmuration/**` | minor | +| `fix`, `perf` | the component | a repair or a speed-up of the plugin | patch | +| `docs`, `test`, `build`, `ci`, `chore`, `refactor`, `revert` | the component | anything, but never the research stores below | nothing | +| `claim`, `verdict` | `wNN-cN` (`claim(w09-c8)`) | a claim filed in `__pondus__/`, with its scripts and puzzles; a verdict or a decision recorded on it | nothing | +| `wave` | `wNN` | `__statera__/wave-NN.md` | nothing | +| `synthesis`, `user_words` | optional `wNN` | those two files in `__statera__/` | nothing | +| `reports` | a topic folder or an agent id (`windows_install`, `w09-i`) | `__reports__/**` and explorer hand-offs | nothing | +| `rdm` | the campaign folder | `__roadmap__/**` | nothing | +| `merge` | the scope of the work | the subject of a pull request's merge commit | nothing | + +- A commit takes the type of the act it performs on its main file, not of the folder. When one commit spans stores, `__pondus__` outranks `__statera__`, which outranks the rest; better, split it. Ids are written `wNN` everywhere (`wave(w09)`, never `wave(09)`). +- A research type never touches `plugins/murmuration/**`: CI fails the range if one does. Split the commit and give the plugin part a code type. +- `build(bin)` commits binaries that ship, and releases nothing: CI makes them (`build(bin): hook binaries for `), and so does the bump (`chore(release): murmuration [skip ci]`). Do not write either by hand. +- Git's own `Merge …` subjects pass. `fixup!`, `squash!` and `Revert "…"` do not: write `revert(): …` and autosquash before pushing. GitHub's "Revert" button titles its PR `Revert "…"`, which `pr-title.yml` rejects: retitle it, as `merge(): revert …`, or `fix(): revert …` when the reverted change was released and the plugin should get a patch. +- Commitizen reads every line of a message, so a line of a body that starts with `feat(x):`, `fix(x):`, `perf(x):` or `BREAKING CHANGE:` counts toward the release. A PR body becomes the merge commit's body: do not start its lines that way unless you mean it, and never write `[skip ci]` in a commit or a PR body. +- The older subjects of the research waves (`w08-c1: …`, `wave-06: …`) are history; they are not rewritten, and only new ranges are checked. + +## Integration lanes + +Released paths: `plugins/murmuration/**`. The release tool does not scope by path, so a `feat`, `fix`, `perf` or `BREAKING CHANGE` releases wherever it lands; give changes outside the plugin a type that releases nothing. +Release-type commits: `feat` (minor), `fix` and `perf` (patch), a `BREAKING CHANGE` (minor while the version is 0.x). + +| Lane | In this repository | +|:--|:--| +| Reviewed PR | Landing a roadmap campaign or a multi-leaf branch; a `BREAKING CHANGE`; CI and release machinery (`.github/**`, `.cz.toml`, repository settings); persisted contracts: the rows of `plugins/murmuration/hooks/hooks.json`, the program name in the data directory (`${CLAUDE_PLUGIN_DATA}/bin/murmuration`), `plugins/murmuration/bin/manifest.json` and the lab layout `scripts/lab_init.py` writes; and anything in doubt | +| Self-merge PR | Any other change under `plugins/murmuration/**` that is self-contained: one component, no machinery, no persisted contract, undone by one revert. `main` has no protection and no required review, and `guard-binaries.yml` gates the PR, so the author merges after CI is green and reports the link; if anything refuses the merge, hand the green PR to the maintainer | +| Direct | Releases nothing and touches no released path: the `__*__/` stores, the root `README.md`, `probes/`. `git pull --ff-only`, integrate (a branch comes in with `git merge --no-ff`), push to `main`. If `main` refuses the push, open a PR | + +Doubt takes the heavier lane. A wrong lane is caught afterwards: the `commits` job runs on every push to `main`, and a Direct push that turns out to be release-type still releases. + +## Versioning + +CI cuts the release after a merge to `main`; never run `cz bump`, tag, or `gh release create` locally; a missing GitHub Release is repaired by re-running the workflow (see the last bullet). + +- Tool: commitizen 4.19.2, configured in `.cz.toml`. One version for the whole plugin. Tag format `v` (annotated). The last `v*` tag is the baseline, so a tag is never created, moved or deleted by hand. +- The `release` job of `guard-binaries.yml` runs on every push to `main`, after the checks: it fails closed (no tag, a red build matrix, or binaries built from other sources than the tip's all stop it), bumps the `"version"` line of `plugins/murmuration/.claude-plugin/plugin.json`, commits `chore(release): murmuration [skip ci]`, pushes the commit and the tag together, and creates the GitHub Release with notes from the commits. When no commit warrants a release it bumps nothing, but it still checks that the latest `v*` tag has its GitHub Release and creates it when it is missing. +- `plugin.json`'s `version` is never edited by hand. +- `build(bin)` precedes the bump: when the build ran, the `commit_back` job commits the binaries first and the bump commit lands on top, so a release always carries binaries built from its sources. +- Published to: GitHub Releases and the repository itself; users install the plugin from the repository, for example through Nest. No registry, no changelog file. Channel: `main` only. +- A release missing after a merge: read the `release` job's log, then re-run the job or run the workflow on `main` with `commit_back` ticked ("Run workflow"). If the bump was pushed but the GitHub Release failed, the re-run or dispatch creates the missing Release for the latest tag; it does not bump twice. Do not tag by hand. + +## Branches + +- Topic branches are named `/` (`fix/hook-timeout`, `ci/release-lifecycle`) and start from an up-to-date `main`. +- Before the merge, rebase onto `origin/main` and push with `--force-with-lease`, so that CI's verdict describes what `main` will hold. +- Merge commits only: squash and rebase merges are off in the repository settings. The PR title is the merge subject, so title it `merge(): ` (checked by `pr-title.yml`); the PR body is the merge body, so write why there. The branch is deleted on merge. +- Pull `main` after every merge (`git pull --ff-only`): CI may have added `build(bin)` and `chore(release)` commits. diff --git a/README.md b/README.md index 5559e69..4ce654c 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,9 @@ CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 claude -p --agent murmuration:research-pi | Path | Holds | |:--|:--| | `plugins/murmuration/` | The plugin: agents, skills, hooks, the hook scripts, the Rust source of the hook binary, and the binaries CI built (`bin/`) | -| `.github/workflows/guard-binaries.yml` | Builds, checks and commits the hook binaries | +| `.github/workflows/guard-binaries.yml` | Builds, checks and commits the hook binaries, checks every commit, and cuts the release after a merge to `main` (its helpers are in `.github/scripts/`) | +| `.github/workflows/pr-title.yml` | Checks that a pull request's title is a valid commit subject, since it becomes the merge commit's | +| `CONTRIBUTING.md` | The commit vocabulary, the integration lanes, how versions are cut, and branch rules | | everything else | The research record (below) | ## Research record