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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
id: 01a13c3b-e14f-7bb9-851d-f1819fa21865
source: Local
kind: detective
mode: automated
enforces:
- A state is read again before it is relied on
---

# A deployed server is asked which commit it serves

> After every deploy of mcp.blust.ch the run asks the live server which commit of the model it answers at, and fails unless it is the commit the repository pins.

## How it is carried out

The deploy job that mcp.blust.ch's workflow calls from the MCP server, on every push to main, once Terraform has applied: it calls the live server's own `list_types` tool, reads the commit every answer names under `model`, and fails unless it is the commit `source.json` pins. A deploy that applied cleanly but left an earlier revision serving, or one built from another commit, then turns the run red instead of reading as done.

## Applies to

| Type | Entity | Owner |
| --- | --- | --- |
| phase | Integrate | Delivery |

## References

| What | URL |
| --- | --- |
| The deploy job and its check | https://github.com/companygraph/mcp-server/blob/main/.github/workflows/deploy-google.yml |
| The workflow that calls it | https://github.com/robertblust/mcp-blust-ch/blob/main/.github/workflows/deploy.yml |
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
id: 01a13c3b-e14f-7482-8a59-87fcdfb5f595
source: Local
kind: preventive
mode: automated
enforces:
- The Owner's word merges
---

# A registry entry waits for a release tag and the Owner

> A server's entry in the MCP registry is published only from a release tag and only once the Owner has approved that run, because the key that signs it is held where no other run can reach it.

## How it is carried out

GitHub's `registry` environment on mcp.blust.ch's repository, on every run of the publish workflow: it lets a job in only from a tag matching `v*`, and holds that job until the Owner approves it. The key that signs the entry is a secret of the environment and of no other, so a run from a branch, or one nobody approved, never holds it and cannot publish. A repository admin may bypass the review, which is the Owner's account, so an agent working under it is held by An agent's permissions refuse a merge it was not given.

## Applies to

| Type | Entity | Owner |
| --- | --- | --- |
| phase | Integrate | Delivery |

## References

| What | URL |
| --- | --- |
| GitHub's deployment environments | https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments |
| The publish workflow and its registry job | https://github.com/robertblust/mcp-blust-ch/blob/main/.github/workflows/publish.yml |
28 changes: 28 additions & 0 deletions model/controls/a-release-tag-stays-where-the-owner-put-it.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
id: 01a13c3b-e14f-752b-b9db-56b6563a532c
source: Local
kind: preventive
mode: automated
enforces:
- The Owner's word merges
---

# A release tag stays where the Owner put it

> Each of my repositories refuses a tag beginning with `v` that any account but the Owner's creates, moves or deletes, so a release exists only where the Owner tagged it and stays on the commit it was made on.

## How it is carried out

GitHub's ruleset `protect-release-tags` on each repository, on every push of a tag matching `v*`: it refuses its creation, its update and its deletion, and lets only the repository's admin role past, which is the Owner's account. A release is its tag and a consumer takes it by naming the tag, so a tag moved after it was taken would change what every consumer already holds; a release tagged wrongly is followed by a patch release and never moved. An agent working under the Owner's account passes it, so what holds an agent's tag to the Owner's word is An agent's permissions refuse a merge it was not given.

## Applies to

| Type | Entity | Owner |
| --- | --- | --- |
| phase | Integrate | Delivery |

## References

| What | URL |
| --- | --- |
| GitHub's rulesets for branches and tags | https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets#branch-and-tag-rulesets |
29 changes: 29 additions & 0 deletions model/controls/a-secret-is-refused-at-the-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
id: 01a13c3b-e14f-74a9-add2-d57f91715839
source: Local
kind: preventive
mode: automated
enforces:
- A secret is never written into a repository
---

# A secret is refused at the push

> GitHub refuses a push to any of my repositories that carries a secret it recognizes, before the secret is published, and scans what is already there for any that got in.

## How it is carried out

GitHub's secret scanning with push protection, turned on in each of my repositories one by one, since a personal account has no default that turns it on for a new one: on every push it looks for the shapes of keys and tokens its providers register, refuses the push that carries one and names the file and line, and it keeps scanning the history for any that reached a repository before. Whoever pushes may push past a refusal by giving a reason, that the secret is used in tests, is a false positive or will be fixed later, and GitHub then records an alert, writes the bypass to the audit log and emails the owners. It knows only those shapes, so a password someone made up, or a key in a form no provider registered, passes it; for those the rule rests on whoever writes the change.

## Applies to

| Type | Entity | Owner |
| --- | --- | --- |
| phase | Implement | Delivery |
| phase | Integrate | Delivery |

## References

| What | URL |
| --- | --- |
| GitHub's push protection | https://docs.github.com/en/code-security/concepts/secret-security/push-protection |
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
id: 01a13c3b-e14f-7b14-9bd5-db46ef6e4c7d
source: Local
kind: preventive
mode: automated
enforces:
- A worktree is removed only once its plan is done
---

# An agent's hook refuses removing a worktree that holds a plan

> Before an agent on my machine runs a shell command, a hook refuses it where it would remove a worktree that still holds a plan's ledger.

## How it is carried out

A hook Claude Code runs on my machine before every shell command an agent issues: it reads each `git worktree remove` in the command, resolves the worktree's path through any `cd` or `-C` before it, and refuses the command where that worktree holds a plan's ledger under `.superpowers/sdd/`, saying why, so the agent stops before anything is deleted. It holds an agent and not my own terminal, and a worktree deleted some other way, such as by `rm -rf`, passes it; the hook lives in my own settings and in no repository.

## Applies to

| Type | Entity | Owner |
| --- | --- | --- |
| phase | Implement | Delivery |
| phase | Integrate | Delivery |

## References

| What | URL |
| --- | --- |
| Claude Code hooks | https://code.claude.com/docs/en/hooks |
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
id: 01a13c3b-e14f-78de-8e41-201b8987c4d4
source: Local
kind: preventive
mode: automated
enforces:
- What is cited from outside the model is not rewritten
---

# An id, a decision and a label are held as they were

> A pull request to the model fails where it changes an id already on the default branch, rewrites a decision beyond closing it, or moves, swaps or reuses a label.

## How it is carried out

The instance check, required on the default branch, on every pull request: its step `companygraph ids --range` compares each page of the branch with its base after both are put in the one form, and refuses a changed id, a decision whose text changed beyond its status and one dated closing sentence, and a label of a scenario, a step or an invariant that moved to another item, swapped with one or came back after it was removed. A reference name may follow the rename of what it names, and a URL under `## References` may change on its own. Where a release of core changed the schema that governs a type, the decision and label checks stand aside for that type and say so in a note.

## Applies to

| Type | Entity | Owner |
| --- | --- | --- |
| phase | Integrate | Delivery |

## References

| What | URL |
| --- | --- |
| The instance check | https://github.com/companygraph/meta-model/blob/main/.github/workflows/instance-check.yml |
| The change checks | https://github.com/companygraph/meta-model/blob/main/lib/checks.mjs |
28 changes: 28 additions & 0 deletions model/controls/only-the-owner-s-account-changes-main.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
id: 01a13c3b-e14f-7b2a-ac60-5ef22adf037f
source: Local
kind: preventive
mode: automated
enforces:
- The Owner's word merges
---

# Only the Owner's account changes main

> The default branch of each of my repositories takes a push or a merge from my account alone, because no one else holds write access to them.

## How it is carried out

GitHub's permissions on a repository under a personal account, on every push and every merge into it: such a repository takes a push only from its owner and the collaborators the owner invites, and mine invite none, so anyone else can fork a repository and open a pull request but cannot merge it. A personal account cannot restrict who may push beyond that, as an organization's branch protection can, so the day a collaborator is invited this control no longer holds. An agent working under my account passes it, so what holds an agent to the Owner's word is An agent's permissions refuse a merge it was not given.

## Applies to

| Type | Entity | Owner |
| --- | --- | --- |
| phase | Integrate | Delivery |

## References

| What | URL |
| --- | --- |
| GitHub's permission levels for a personal account's repository | https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/repository-access-and-collaboration/permission-levels-for-a-personal-account-repository |
19 changes: 19 additions & 0 deletions model/rules/a-secret-is-never-written-into-a-repository.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
id: 01a13c3b-e14f-7f29-9337-6620a10a3531
source: Local
modality: must not
---

# A secret is never written into a repository

> No key, token or password is committed to any of my repositories, in a file, a commit message or a test fixture; a secret lives in the store of the workflow or the service that reads it.

## Why

Every repository I work in is public, so whatever is pushed is published the moment it reaches GitHub, and it stays in the history after a later commit removes it. A secret that reached a repository can only be rotated, never taken back, and everything it opened stays open until it is. For the same reason a secret is never printed, not even to check it: a value that reaches a transcript has to be rotated as well.

## References

| What | URL |
| --- | --- |
| Working rules | https://github.com/robertblust/conventions/blob/main/conventions/WORKING.md |
13 changes: 13 additions & 0 deletions model/rules/a-worktree-is-removed-only-once-its-plan-is-done.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: 01a13c3b-e14f-722f-a2e8-240dc563ab97
source: Local
modality: must not
---

# A worktree is removed only once its plan is done

> A worktree that holds a plan's ledger is not removed before every task of that plan is done, even when its pull request has merged.

## Why

A plan's ledger, its briefs and its task reports live in the worktree under `.superpowers/`, which git ignores, and `git worktree remove` deletes ignored files without asking, with or without `--force`, so nothing brings them back. A merged pull request does not finish a plan: a plan can carry tasks in other repositories that are still owed. Three ledgers were lost this way in September 2026, twice in one day.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: 01a13c3b-e14f-7492-9f36-ffa64f8bf645
source: Local
modality: must not
---

# What is cited from outside the model is not rewritten

> Once on the default branch, an entity's id, a decision's text and a label on a page are not changed, because a link, a commit or a test outside the model may already cite them.

## Why

An outside system holds on to what it cites: a graph node to an entity's id, a commit or a later decision to a decision as it was decided, a test or a line of code to the label of a scenario, a step or an invariant. Each is the only handle that citation has, and a rewrite leaves it pointing at something that now says another thing, with nothing to tell whoever cited it. So an id stays with its entity, a decision that no longer stands is closed by its status and one dated sentence instead of being edited, and a label stays with the item it was given to and is never given to another.
Loading