From f7db10b6c936895d2b5739cd56239ff214289f3d Mon Sep 17 00:00:00 2001 From: David Hamilton Date: Thu, 15 Oct 2026 15:43:32 -0400 Subject: [PATCH 1/3] Add versioned releases: CHANGELOG.md and a draft-release workflow Pushing a tag vX.Y.Z builds one file with all normative drafts, links pinned to the tag, and release notes from the CHANGELOG section. The workflow creates a draft; a maintainer publishes it. --- .github/workflows/release.yml | 29 +++++++++++++++ .gitignore | 1 + CHANGELOG.md | 32 +++++++++++++++++ README.md | 2 +- scripts/build-release.py | 68 +++++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 scripts/build-release.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..745c941 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: release + +# Push a tag vX.Y.Z to build a draft release. A maintainer reviews the +# draft and publishes it by hand. +on: + push: + tags: ["v*.*.*"] + +permissions: + contents: write + +jobs: + draft: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Build release files + run: python3 scripts/build-release.py "$GITHUB_REF_NAME" + - name: Create draft release + env: + GH_TOKEN: ${{ github.token }} + run: | + version="${GITHUB_REF_NAME#v}" + prerelease="" + case "$version" in 0.*) prerelease="--prerelease" ;; esac + gh release create "$GITHUB_REF_NAME" --draft $prerelease \ + --title "me.md protocol $version" \ + --notes-file dist/release-notes.md \ + "dist/me.md-protocol-$version.md" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e20ac00 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +Each release is a tag of the form `vX.Y.Z`. A host that claims conformance names the release it follows, for example "conforms to me.md v0.1.0". Until version 1.0, any release can change the normative drafts. + +Each release lists changes for the person and for hosts. The release page includes one file that contains all normative drafts at that tag. + +## 0.1.0 — Unreleased + +The first tagged draft. It records the protocol as it is now, so that hosts and experiments have a version to name. + +### For the person + +- One setup instruction ([SETUP.md](./SETUP.md)). Give it to any agent that can write a file. After setup, the agent reads `~/.me/me.md` at the start of each session with no instruction from you. +- Your store is plain files in `~/.me/`: `me.md`, `topics/`, `projects/`, `proposals/`, and `policy.json`. It starts empty. +- Agents can only propose. You accept, edit, or refuse. A refusal stays in effect. Silence is not acceptance. +- The commitments to you are in [SOVEREIGNTY.md](./SOVEREIGNTY.md). + +### For hosts + +- [PROTOCOL.md](./PROTOCOL.md): the store layout, the loop (mount, recall, propose, decide), and a paste-in block for a harness that does not conform natively. +- [HOST.md](./HOST.md): the conformance rules for mounting, recall, writing, provenance, revocation, and security, with eight black-box tests. +- [BRIEFING.md](./BRIEFING.md): what a conforming briefing contains. + +### Not decided in this release + +These are under test, not standardized. See [SOVEREIGNTY.md](./SOVEREIGNTY.md#what-remains-under-test). + +- the exact `policy.json` schema +- whether `~/.me/` is the right default location, and whether Markdown is the right primary format +- provenance, history, and undo storage +- protection of context at rest and in transit, and synchronization between devices +- the minimum useful conformance surface diff --git a/README.md b/README.md index 37629d8..b1b6c3f 100644 --- a/README.md +++ b/README.md @@ -63,4 +63,4 @@ It is not a transcript, not a hidden profile, not a productivity score, not empl ## Status -`me.md` is an experimental protocol draft, not a stable standard yet. The most important question for any contribution is whether the system increases the person's agency. Governance is in [GOVERNANCE.md](./GOVERNANCE.md). The license is [Apache 2.0](./LICENSE). +`me.md` is an experimental protocol draft, not a stable standard yet. Releases are tagged `vX.Y.Z`, and a host that claims conformance names the release it follows. Changes are in [CHANGELOG.md](./CHANGELOG.md). The most important question for any contribution is whether the system increases the person's agency. Governance is in [GOVERNANCE.md](./GOVERNANCE.md). The license is [Apache 2.0](./LICENSE). diff --git a/scripts/build-release.py b/scripts/build-release.py new file mode 100644 index 0000000..cb286f1 --- /dev/null +++ b/scripts/build-release.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Build the release files for one tag. + +Writes two files to dist/: + me.md-protocol-X.Y.Z.md all normative drafts in one file, with links + pinned to the tag + release-notes.md the CHANGELOG.md section for X.Y.Z, plus links + +Usage: python3 scripts/build-release.py vX.Y.Z +""" + +import pathlib +import re +import sys + +REPO = "https://github.com/block/me.md" +ROOT = pathlib.Path(__file__).resolve().parent.parent +NORMATIVE = ["PROTOCOL.md", "SOVEREIGNTY.md", "HOST.md", "BRIEFING.md"] +MARKER = re.compile(r"^\n", re.M) +LINK = re.compile(r"\]\(\./([^)\s]+)\)") + + +def pin_links(text, tag): + return LINK.sub(lambda m: f"]({REPO}/blob/{tag}/{m.group(1)})", text) + + +def changelog_section(version): + text = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + match = re.search( + rf"^## {re.escape(version)}\b.*?\n(.*?)(?=^## |\Z)", text, re.M | re.S + ) + if not match: + sys.exit(f"error: CHANGELOG.md has no section for {version}") + return match.group(1).strip() + + +def main(): + if len(sys.argv) != 2 or not re.fullmatch(r"v\d+\.\d+\.\d+", sys.argv[1]): + sys.exit("usage: build-release.py vX.Y.Z") + tag = sys.argv[1] + version = tag[1:] + dist = ROOT / "dist" + dist.mkdir(exist_ok=True) + + combined = dist / f"me.md-protocol-{version}.md" + parts = [ + f"# me.md protocol {version}\n\n" + f"All normative drafts at tag `{tag}`, in one file. " + f"Source: {REPO}/tree/{tag}\n" + ] + for name in NORMATIVE: + body = MARKER.sub("", (ROOT / name).read_text(encoding="utf-8")) + parts.append(f"\n\n{body.strip()}\n") + combined.write_text(pin_links("\n---\n\n".join(parts), tag), encoding="utf-8") + + notes = pin_links(changelog_section(version), tag) + notes += ( + f"\n\n---\n\n" + f"- Read the protocol at this release: {REPO}/blob/{tag}/PROTOCOL.md\n" + f"- All normative drafts in one file: `{combined.name}` (attached below)\n" + f"- Full changelog: {REPO}/blob/{tag}/CHANGELOG.md\n" + ) + (dist / "release-notes.md").write_text(notes, encoding="utf-8") + print(f"wrote {combined.relative_to(ROOT)} and dist/release-notes.md") + + +if __name__ == "__main__": + main() From b9ef26cf428cfadfd6c716bc5a44051b24bed037 Mon Sep 17 00:00:00 2001 From: David Hamilton Date: Thu, 15 Oct 2026 16:23:33 -0400 Subject: [PATCH 2/3] Put the release workflow behind a trusted release boundary Only repository admins can create, move, or delete v* tags (tag ruleset). The job runs in the release environment, which accepts only v* tags and requires maintainer approval before the write token is issued. The job refuses a tag whose commit is not on main. The write permission is scoped to the job, and checkout does not keep credentials. --- .github/workflows/release.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 745c941..1182fc4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,18 +2,38 @@ name: release # Push a tag vX.Y.Z to build a draft release. A maintainer reviews the # draft and publishes it by hand. +# +# Trust boundary, enforced by repository settings, not by this file: +# - The "Release tags: admins only" tag ruleset lets only repository +# admins create, move, or delete v* tags. +# - The "release" environment accepts only v* tags and requires a +# maintainer to approve each run before it gets the write token. +# The ancestry check below also refuses a tag that is not on main. on: push: tags: ["v*.*.*"] permissions: - contents: write + contents: read jobs: draft: runs-on: ubuntu-latest + environment: release + permissions: + contents: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Refuse a tag that is not on main + run: | + git fetch --no-tags origin main + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + echo "error: $GITHUB_REF_NAME points to $GITHUB_SHA, which is not on main" >&2 + exit 1 + fi - name: Build release files run: python3 scripts/build-release.py "$GITHUB_REF_NAME" - name: Create draft release From bee353da8bcfb845b3433d5c70471f22a5402afb Mon Sep 17 00:00:00 2001 From: David Hamilton Date: Thu, 15 Oct 2026 16:46:46 -0400 Subject: [PATCH 3/3] Use manual releases; remove release build and automation --- .github/workflows/release.yml | 49 ------------------------- .gitignore | 1 - CHANGELOG.md | 6 ++-- scripts/build-release.py | 68 ----------------------------------- 4 files changed, 4 insertions(+), 120 deletions(-) delete mode 100644 .github/workflows/release.yml delete mode 100644 .gitignore delete mode 100644 scripts/build-release.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1182fc4..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: release - -# Push a tag vX.Y.Z to build a draft release. A maintainer reviews the -# draft and publishes it by hand. -# -# Trust boundary, enforced by repository settings, not by this file: -# - The "Release tags: admins only" tag ruleset lets only repository -# admins create, move, or delete v* tags. -# - The "release" environment accepts only v* tags and requires a -# maintainer to approve each run before it gets the write token. -# The ancestry check below also refuses a tag that is not on main. -on: - push: - tags: ["v*.*.*"] - -permissions: - contents: read - -jobs: - draft: - runs-on: ubuntu-latest - environment: release - permissions: - contents: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - name: Refuse a tag that is not on main - run: | - git fetch --no-tags origin main - if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then - echo "error: $GITHUB_REF_NAME points to $GITHUB_SHA, which is not on main" >&2 - exit 1 - fi - - name: Build release files - run: python3 scripts/build-release.py "$GITHUB_REF_NAME" - - name: Create draft release - env: - GH_TOKEN: ${{ github.token }} - run: | - version="${GITHUB_REF_NAME#v}" - prerelease="" - case "$version" in 0.*) prerelease="--prerelease" ;; esac - gh release create "$GITHUB_REF_NAME" --draft $prerelease \ - --title "me.md protocol $version" \ - --notes-file dist/release-notes.md \ - "dist/me.md-protocol-$version.md" diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 849ddff..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e20ac00..66b9c88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ Each release is a tag of the form `vX.Y.Z`. A host that claims conformance names the release it follows, for example "conforms to me.md v0.1.0". Until version 1.0, any release can change the normative drafts. -Each release lists changes for the person and for hosts. The release page includes one file that contains all normative drafts at that tag. +Each release lists changes for the person and for hosts. Read a release against the documents at its tag, not the changing main branch. + +To publish: merge the reviewed changes and dated changelog entry into main, then use GitHub’s Releases page to create a tag at that commit and write the release notes. Mark experimental releases as pre-releases. Share the specific release URL or the Releases list; pre-releases do not appear at `releases/latest`. ## 0.1.0 — Unreleased @@ -12,7 +14,7 @@ The first tagged draft. It records the protocol as it is now, so that hosts and - One setup instruction ([SETUP.md](./SETUP.md)). Give it to any agent that can write a file. After setup, the agent reads `~/.me/me.md` at the start of each session with no instruction from you. - Your store is plain files in `~/.me/`: `me.md`, `topics/`, `projects/`, `proposals/`, and `policy.json`. It starts empty. -- Agents can only propose. You accept, edit, or refuse. A refusal stays in effect. Silence is not acceptance. +- Agent-inferred changes require a proposal. You may authorize an edit directly or approve a proposal. A refusal stays in effect. Silence is not acceptance. - The commitments to you are in [SOVEREIGNTY.md](./SOVEREIGNTY.md). ### For hosts diff --git a/scripts/build-release.py b/scripts/build-release.py deleted file mode 100644 index cb286f1..0000000 --- a/scripts/build-release.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -"""Build the release files for one tag. - -Writes two files to dist/: - me.md-protocol-X.Y.Z.md all normative drafts in one file, with links - pinned to the tag - release-notes.md the CHANGELOG.md section for X.Y.Z, plus links - -Usage: python3 scripts/build-release.py vX.Y.Z -""" - -import pathlib -import re -import sys - -REPO = "https://github.com/block/me.md" -ROOT = pathlib.Path(__file__).resolve().parent.parent -NORMATIVE = ["PROTOCOL.md", "SOVEREIGNTY.md", "HOST.md", "BRIEFING.md"] -MARKER = re.compile(r"^\n", re.M) -LINK = re.compile(r"\]\(\./([^)\s]+)\)") - - -def pin_links(text, tag): - return LINK.sub(lambda m: f"]({REPO}/blob/{tag}/{m.group(1)})", text) - - -def changelog_section(version): - text = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") - match = re.search( - rf"^## {re.escape(version)}\b.*?\n(.*?)(?=^## |\Z)", text, re.M | re.S - ) - if not match: - sys.exit(f"error: CHANGELOG.md has no section for {version}") - return match.group(1).strip() - - -def main(): - if len(sys.argv) != 2 or not re.fullmatch(r"v\d+\.\d+\.\d+", sys.argv[1]): - sys.exit("usage: build-release.py vX.Y.Z") - tag = sys.argv[1] - version = tag[1:] - dist = ROOT / "dist" - dist.mkdir(exist_ok=True) - - combined = dist / f"me.md-protocol-{version}.md" - parts = [ - f"# me.md protocol {version}\n\n" - f"All normative drafts at tag `{tag}`, in one file. " - f"Source: {REPO}/tree/{tag}\n" - ] - for name in NORMATIVE: - body = MARKER.sub("", (ROOT / name).read_text(encoding="utf-8")) - parts.append(f"\n\n{body.strip()}\n") - combined.write_text(pin_links("\n---\n\n".join(parts), tag), encoding="utf-8") - - notes = pin_links(changelog_section(version), tag) - notes += ( - f"\n\n---\n\n" - f"- Read the protocol at this release: {REPO}/blob/{tag}/PROTOCOL.md\n" - f"- All normative drafts in one file: `{combined.name}` (attached below)\n" - f"- Full changelog: {REPO}/blob/{tag}/CHANGELOG.md\n" - ) - (dist / "release-notes.md").write_text(notes, encoding="utf-8") - print(f"wrote {combined.relative_to(ROOT)} and dist/release-notes.md") - - -if __name__ == "__main__": - main()