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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/conventions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ on:
pull_request:
jobs:
conventions:
uses: robertblust/conventions/.github/workflows/check.yml@v1.35.0
uses: robertblust/conventions/.github/workflows/check.yml@v1.37.0
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- conventions · v1.35.0 -->
<!-- conventions · v1.37.0 -->
Shared conventions of the robertblust, guestgraph and companygraph organizations live in `conventions/`, vendored from robertblust/conventions at the release `conventions.json` names. Read them before writing or committing anything here.

- `conventions/WRITING.md` — how we write: one voice, three registers, English and German.
Expand Down
2 changes: 1 addition & 1 deletion conventions.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "repo": "robertblust/conventions", "tag": "v1.35.0" }
{ "repo": "robertblust/conventions", "tag": "v1.37.0" }
2 changes: 1 addition & 1 deletion conventions/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- conventions · v1.35.0 -->
<!-- conventions · v1.37.0 -->
Shared conventions of the robertblust, guestgraph and companygraph organizations live in `conventions/`, vendored from robertblust/conventions at the release `conventions.json` names. Read them before writing or committing anything here.

- `conventions/WRITING.md` — how we write: one voice, three registers, English and German.
Expand Down
2 changes: 1 addition & 1 deletion conventions/PINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ What a member takes from another member of the family is a pin, and `WORKING.md`
| `npm-tag` | `github:owner/repo#tag` in a `package.json` | set the tag, `npm install` |
| `source-commit` | an object with `repo` and `commit` in a JSON file, at the top or under a key | set the commit of the object whose `repo` matches |
| `contract-commit` | a string `owner/repo@commit:path` in a JSON file | set the commit of every string for that repository |
| `core-release` | `core.version` in `.companygraph/manifest.json` | the entry's own `move` command |
| `core-release` | `tooling` in `.companygraph/manifest.json`, the meta-model release an instance took with its core | the entry's own `move` command, with `{version}` that release without its `v` |

## `pins.json`

Expand Down
6 changes: 3 additions & 3 deletions conventions/WORKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The branch is checked out in a worktree, not in the clone. The clone stays on th

A branch is deleted once its pull request is merged. The merge commit is its record; a branch left standing is a question every reader of the branch list has to answer again.

An agent commits when the owner asks, and not on its own initiative. It proposes the message in the git register of `WRITING.md`. The author of the commit is the person. A tool that co-authored the change is named in a `Co-Authored-By` trailer, whichever tool it was, so the history says who and what wrote it. “Commit and open the pull request” is a request to do exactly that; it is not approval to merge. A role invoked as a subagent — the writer of `WRITER.md`, the translator of `TRANSLATOR.md`, the editor of `EDITOR.md` or the back-reader of `BACKREADER.md` — edits files or only reports, as its own file says; it never commits, and the session that invoked it proposes the message.
An agent commits when the owner asks, and not on its own initiative. It proposes the message in the git register of `WRITING.md`. A commit the owner makes is authored by the owner. A commit an agent makes is authored by the seat it held, the role whose work the commit is, at the governing instance's domain — `git commit --author "Implementer <implementer@blust.ch>"` — and the person running the agent stays the committer, so the author says which seat did the work and the committer who is accountable for it being there. The governing instance is the repository's own where it is one, and otherwise its organization's. A specification is the Specifier's, a plan the Planner's, a task, a re-pin, a re-sync and a release bump the Implementer's, English prose the Writer's, its German the Translator's and a narrated clip the Narrator's; the Controller writes nothing and authors no commit. The message opens its trailers with `Process`, `Phase` and `Track`, as `WRITING.md` shows. `conventions/hooks/commit-msg` refuses a seat the phase in the trailers does not list, and the pull request's check refuses it again. `conventions-sync` points `core.hooksPath` at that hook, and `core.hooksPath` is local config that a clone does not carry, so a fresh clone runs `sh conventions/conventions-sync sync` once before its first commit. A tool that co-authored the change is named in a `Co-Authored-By` trailer, whichever tool it was, so the history says who and what wrote it. “Commit and open the pull request” is a request to do exactly that; it is not approval to merge. A role invoked as a subagent — the writer of `WRITER.md`, the translator of `TRANSLATOR.md`, the editor of `EDITOR.md` or the back-reader of `BACKREADER.md` — edits files or only reports, as its own file says; it never commits, and the session that invoked it proposes the message and commits it as the seat whose work it is: the Writer's for the writer's English, the Translator's for the translator's German and the editor's corrections to it.

A commit that exists may still be reworded while its branch is unpushed — a missing `Verified:` line, a fact the body got wrong — and whoever is driving the branch does that. What a commit contains is a different thing. A finding against a task already committed is a finding like any other: it comes back as a new commit through review, never folded into the old one, because a commit rewritten after it was read is no longer the commit that was read. Once a branch is pushed and a pull request is open someone may already have it, so it is rewritten only when the owner says so, with `--force-with-lease` and never `--force`.

Expand All @@ -30,9 +30,9 @@ When a layer merges, GitHub rebases the branches above it onto the merge and rep

## Identity

A commit is authored by the person who made it, under the address they mean to be known by, and the merge commit carries that address to the default branch unchanged. That is the whole rule, and it holds for a contributor from outside exactly as it holds for the owner; nothing here asks a contributor to be anyone but themselves.
A person's commit is authored by the person who made it, under the address they mean to be known by, and the merge commit carries that address to the default branch unchanged. That is the whole rule for a person's commit, and it holds for a contributor from outside exactly as it holds for the owner; nothing here asks a contributor to be anyone but themselves. An agent's commit is authored by its seat, as Branches and commits says.

Nothing on GitHub enforces an address, and nothing should. The ruleset rule that could, `commit_author_email_pattern`, is not available on this plan — a ruleset carrying it is rejected while an otherwise identical one carrying a `deletion` rule is accepted, tested rather than assumed — and it would shut out every outside contributor if it were. So each person's identity is their own `git config` to keep. The owner's is `robert.blust@flatland.ch` in all three organizations, keyed by `includeIf` blocks in `~/.gitconfig` to the directories `~/git/robertblust/`, `~/git/guestgraph/` and `~/git/companygraph/`; a clone made anywhere else takes the global default and gives no warning. Before the first commit in a fresh clone, whoever you are, run `git config user.email` and read the answer.
Nothing on GitHub enforces an address, and nothing should. The ruleset rule that could, `commit_author_email_pattern`, is not available on this plan — a ruleset carrying it is rejected while an otherwise identical one carrying a `deletion` rule is accepted, tested rather than assumed — and it would shut out every outside contributor if it were. The seat check does not change that: it refuses a seat its phase does not list and lets every other author through, so it is a check on seats and not on a person's address. So each person's identity is their own `git config` to keep. The owner's is `robert@blust.ch` in all three organizations, the address the identity carries in `robertblust/mental-model`, set by `includeIf` blocks in `~/.gitconfig` that read `~/.gitconfig-blust` for the directories `~/git/robertblust/`, `~/git/guestgraph/` and `~/git/companygraph/`; a clone made anywhere else takes the global default and gives no warning. Before the first commit in a fresh clone, whoever you are, run `git config user.email` and read the answer.

## Releases and pins

Expand Down
9 changes: 7 additions & 2 deletions conventions/WRITING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ For commit messages and pull request descriptions.

The subject is a sentence in plain words, under seventy characters where it can be, with no type prefix and no trailing period. It says what is now true that was not before.

The body is one to three short paragraphs, cause before mechanism. No headers, no bullets. A table only for counts. It ends with one line beginning `Verified:` that names what ran and passed, then the trailers.
The body is one to three short paragraphs, cause before mechanism. No headers, no bullets. A table only for counts. It ends with one line beginning `Verified:` that names what ran and passed, then a blank line and the trailers. A commit an agent makes opens its trailers with `Process`, `Phase` and `Track`, named as the governing instance names them, and `Track` only where the process has tracks; git reads trailers only from the message's last paragraph, so no blank line falls between them and `Co-Authored-By`.

A pull request description is the commit body reread for a reviewer who has not seen the diff, plus links to the sibling pull requests when there are any.

One example, a commit message in full, then the trailers:
One example, an agent's commit message in full, trailers and all:

```
The list is the scope
Expand All @@ -75,6 +75,11 @@ When a task needs something outside it, the task names it and the one purpose it
serves, and that reference stays with the task.

Verified: the spelling tripwire passes.

Process: Delivery
Phase: Implement
Track: Prose
Co-Authored-By: Claude Code <noreply@anthropic.com>
```

## The reply register
Expand Down
37 changes: 36 additions & 1 deletion conventions/conventions-sync
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ case $cmd in sync|check) ;; *) usage ;; esac

PIN=conventions.json
DIR=conventions
FILES="WRITING.md WORKING.md REPOSITORIES.md PINS.md WRITER.md TRANSLATOR.md GLOSSARY.md GERMAN.md EDITOR.md BACKREADER.md AGENTS.md conventions-sync conventions-check conventions-format markdown-rules.cjs vscode-settings.json vscode-extensions.json"
FILES="WRITING.md WORKING.md REPOSITORIES.md PINS.md WRITER.md TRANSLATOR.md GLOSSARY.md GERMAN.md EDITOR.md BACKREADER.md AGENTS.md conventions-sync conventions-check conventions-format markdown-rules.cjs vscode-settings.json vscode-extensions.json hooks/commit-msg"

# What a member receives at its own root rather than under conventions/, because something other
# than this family reads it there. markdownlint-cli2 and the editor plugins built on it discover
Expand Down Expand Up @@ -97,6 +97,37 @@ write_block() {
mv AGENTS.md.tmp AGENTS.md
}

# hooks_path: point git at the seat hook, which is in use only where git reads hooks from it.
# Only at the top of a clone, because a relative core.hooksPath is read from there. A hooks
# path the member set for itself is its own, and hooks git already runs from its own directory
# would stop running once the path moves, so either is left alone with a sentence instead.
hooks_path() {
[ "$(git rev-parse --is-inside-work-tree 2> /dev/null)" = "true" ] || return 0
[ -z "$(git rev-parse --show-prefix)" ] || return 0
current=$(git config --get core.hooksPath || true)
if [ -n "$current" ]; then
[ "$current" = "$DIR/hooks" ] || echo "! conventions: core.hooksPath is $current here, so the seat check's hook in $DIR/hooks is not in use" >&2
return 0
fi
own=$(git rev-parse --git-path hooks)
# Git for Windows prints an absolute path in drive form, C:/…, in a linked worktree.
case $own in /*|[A-Za-z]:*) ;; *) own=$(git rev-parse --show-toplevel)/$own ;; esac
found=""
if [ -d "$own" ]; then
for h in "$own"/*; do
[ -f "$h" ] || continue
case $h in *.sample) continue ;; esac
found="${found:+$found, }${h##*/}"
done
fi
if [ -n "$found" ]; then
echo "! conventions: $own holds $found, which would stop running under core.hooksPath, so it is left unset and the seat check's hook in $DIR/hooks is not in use" >&2
return 0
fi
git config core.hooksPath "$DIR/hooks"
echo "✓ conventions: core.hooksPath is now $DIR/hooks; it is local config and not cloned, so a fresh clone runs sync once, or git config core.hooksPath $DIR/hooks"
}

sync() {
mkdir -p "$DIR"
# A member running an older conventions-sync has an older FILES list, missing whatever file
Expand All @@ -115,6 +146,8 @@ sync() {
fi
fi
for f in $FILES; do
d=${f%/*}
[ "$d" = "$f" ] || mkdir -p "$DIR/$d"
case $f in
AGENTS.md) fetch "AGENTS.md" "$DIR/$f.new" ;;
*) fetch "$DIR/$f" "$DIR/$f.new" ;;
Expand All @@ -139,6 +172,8 @@ sync() {
chmod +x "$DIR/conventions-sync"
chmod +x "$DIR/conventions-check"
chmod +x "$DIR/conventions-format"
chmod +x "$DIR/hooks/commit-msg"
hooks_path
write_block
{
printf '{\n "repo": "%s",\n "tag": "%s",\n "files": {\n' "$REPO" "$TAG"
Expand Down
49 changes: 49 additions & 0 deletions conventions/hooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/bin/sh
# Refuses a commit whose author is a seat of the governing instance that the phase named in its
# trailers does not list, as WORKING.md says under Branches and commits. The governing instance
# is this repository where it is one, and otherwise its organization's mental-model at the
# local path REPOSITORIES.md gives. The check is companygraph commits, at the release that
# instance's manifest names, so this family pins no meta-model release of its own. Only the
# checker's refusal (exit 3) refuses the commit; where the check cannot run — no clone, offline,
# no npx, a release without commits — the commit goes through with a sentence, because the pull
# request's check runs it again.
top=$(git rev-parse --show-toplevel) || exit 0
list=${CONVENTIONS_REPOSITORIES:-conventions/REPOSITORIES.md}
if [ -f "$top/.companygraph/manifest.json" ]; then
instance=$top
else
org=$(git -C "$top" remote get-url origin 2> /dev/null | sed -n 's#.*github\.com[:/]\([^/]*\)/.*#\1#p')
path=$(awk -F'|' -v repo="$org/mental-model" '{ gsub(/^ +| +$/, "", $2) } $2 == repo { p = $(NF - 1); gsub(/^ +| +$/, "", p); print p; exit }' "$top/$list" 2> /dev/null)
case $path in \~/*) path=$HOME/${path#\~/} ;; esac
if [ -z "$org" ] || [ -z "$path" ] || [ ! -f "$path/.companygraph/manifest.json" ]; then
echo "commit-msg: no clone of ${org:-this organization}/mental-model at ${path:-a path REPOSITORIES.md gives}; the seat check did not run, and the pull request's check will" >&2
exit 0
fi
instance=$path
fi
# Git hands the hook its own repository variables: GIT_INDEX_FILE always, and GIT_DIR in a
# linked worktree. npx clones meta-model, and a clone that inherits them writes meta-model's
# index over this repository's. Git runs the hook at the top of the working tree, so git var,
# interpret-trailers and the checker find this repository from the working directory alone.
unset GIT_INDEX_FILE GIT_DIR GIT_WORK_TREE GIT_PREFIX GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
tooling=$(sed -n 's/.*"tooling" *: *"\([^"]*\)".*/\1/p' "$instance/.companygraph/manifest.json" | head -1)
# The checker's refusals go to stderr, and a passing check has nothing to say, so its stdout is
# kept and shown only when the check did not pass.
out=$(
if [ -n "${COMPANYGRAPH_CLI:-}" ]; then
node "$COMPANYGRAPH_CLI" commits "$instance" --message "$1"
elif command -v npx > /dev/null 2>&1 && [ -n "$tooling" ]; then
npx --yes --prefer-offline --package "github:companygraph/meta-model#v$tooling" companygraph commits "$instance" --message "$1"
else
false
fi
)
status=$?
[ "$status" -eq 0 ] && exit 0
[ -n "$out" ] && printf '%s\n' "$out"
if [ "$status" -eq 3 ]; then
echo "commit-msg: correct the trailers, or the author with git commit --author \"<Seat> <seat@domain>\"; the rule is in conventions/WORKING.md, Branches and commits" >&2
exit 1
fi
echo "commit-msg: the seat check did not run here (exit $status); the pull request's check will run it" >&2
exit 0
13 changes: 7 additions & 6 deletions conventions/manifest.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
{
"repo": "robertblust/conventions",
"tag": "v1.35.0",
"tag": "v1.37.0",
"files": {
"conventions/WRITING.md": "sha256:c789a79512ba7599fde6eeb08ae7832bf2812c9c193872742219daad308eb6dc",
"conventions/WORKING.md": "sha256:4e1ad8b8c22355303868b795f6c100c5b5d28aa9a3de334fedfa3e4632a01de1",
"conventions/WRITING.md": "sha256:51d9a902ec3038392160df07f86aba98d34db1d4dbfaa5f4a647590eaccfb390",
"conventions/WORKING.md": "sha256:8fa553323ec1d53c8a2b4b5f260790731db69856a1053896f771db3caf94ee1c",
"conventions/REPOSITORIES.md": "sha256:c37d322b74344a7677ab5248841b230612c42107e6721123971458abde03dddd",
"conventions/PINS.md": "sha256:87acf40bafc38b522fc790f41eaebf90eef482cda1354515d142577a39248940",
"conventions/PINS.md": "sha256:ccfa0b7876eb3b71127cfe1db573e03a0ed42b84cd0c4b80270a2ba0bef80bd9",
"conventions/WRITER.md": "sha256:bc3b92683130b75993f824f0e34073d4344846c91be2009fd90c39099fd4fd48",
"conventions/TRANSLATOR.md": "sha256:026c4767347ea907e6ef285f7ccf8192e0b3dec12b40207acd32ad3a2f9b5140",
"conventions/GLOSSARY.md": "sha256:ffad6b58a3c3b5c92109ec15b1aa0669a2915e9641b7d240c3126a65a16a3cbf",
"conventions/GERMAN.md": "sha256:2f4651dd74e2970f0751621bf9264441b43ee619b8a6e1373d5f3c7809581f77",
"conventions/EDITOR.md": "sha256:e65d0b2893ac841faefede9b873f7e33c1fd7b02bedf97d37d5362c0e83c08ea",
"conventions/BACKREADER.md": "sha256:36b12d4bee8e4fe190469e2f550d50d8a7f6dcab6bec573da17eb1c165c417cd",
"conventions/AGENTS.md": "sha256:7e3d5d6940ef1888f28e4e8e4116ddbf44c2415dd06680c3e9f1ee07594e8b99",
"conventions/conventions-sync": "sha256:bc68607ea24c5cea4d449c1bc2f3a1bf681f55254aa581bee7f8750667a824f7",
"conventions/AGENTS.md": "sha256:44f02db310f5ae86489c11e5cc4fc54f56a5e67a17dae43933a28dd044e54433",
"conventions/conventions-sync": "sha256:a190ff79c2d8d3f6f6d9a597e9f261654f2419d30e2ac43abf6c64ebe2c47ed9",
"conventions/conventions-check": "sha256:30b2528e84381f04fd6b06b26ef103cd2360d877639706eb0b53f362a10fc433",
"conventions/conventions-format": "sha256:2911a0d30dda1bc0f621522a9538b3e39704eee03458e65ba320eccbf39b5c7f",
"conventions/markdown-rules.cjs": "sha256:5cbf6120bf16e358b3a4e362015d054f0cd6e47ddfb0e5fe5ac17143b2bdef26",
"conventions/vscode-settings.json": "sha256:86d3c951f7c06d86ee3bc828ad6e49fac3531be98117a7a590cc75da7ba3600a",
"conventions/vscode-extensions.json": "sha256:bc61cd9278b6bd01f0b091efea96cc24c8a8c8f3ed884c7de7579abee384fce6",
"conventions/hooks/commit-msg": "sha256:4f46f55d8b918b65c7e1f48f67d63053bca658c9a0855c8ccc2b7410fdf0a95d",
".markdownlint-cli2.jsonc": "sha256:81cbb5431f8eaeeea01a4e0ec0e7ced6304fa5036c3915aca46d6960d96b4eb1",
"CLAUDE.md": "sha256:ed4d8a69be583a788abab0585cea9c7d5d65a51dc3085fd4c38a727038fec9e8"
}
Expand Down
Loading