Customization¶
The core stays generic. Each repo, profile, and task adds specificity through set files. This file defines where specificity goes and which layer wins.
Layers¶
| Order | Layer | Where | Example content |
|---|---|---|---|
| 1 | Framework core | framework/ in this repo |
Principles, gates, profiles |
| 2 | Adapter and model profile | framework/adapters/<name>.md, its templates, and framework/adapters/<name>/models/ |
File paths for one harness. Idioms and role models for one model family. |
| 3 | Repo config | .cornerstone/config.yaml in the target repo |
Commands, paths, limits, owners |
| 4 | Profile override (D84, D85) | .cornerstone/profiles/<profile>.yaml in the target repo |
Extra checks, a different size limit |
| 5 | Task | The ticket fields | Acceptance criteria, non-goals, a mode, a waiver |
A later layer wins over an earlier layer, with these limits:
- A later layer MAY add a gate, a check, or a protected path.
- A later layer MAY tighten a limit.
- A later layer MUST record a waiver to remove a gate, remove a check, or loosen a limit. A waiver has a reason, an owner, and an end date.
- No layer can remove G3 or G5 (see gates.md).
- Any layer MAY change the mode or single stage drivers. A mode change never changes the gates.
Repo config (.cornerstone/config.yaml)¶
framework_version: 1.2.0 # tag of brawblan/ai-workflow. G3 installs stone from it. See versioning.md.
harness: <harness-adapter-name> # a file name in framework/adapters/
model_profile: <model-family> # a file in framework/adapters/<harness>/models/
spec_tool: plain # plain, or a spec-tool adapter name
profiles: [single-ticket, bug-triage, debugging]
adoption: all # all (default), or opt-in: only marked PRs (see below)
delivery: ci # ci (default), or local: no workflow files (see below)
branches:
integration: dev
main: main
commands: # exactly as a person types them
install: <command>
lint: <command>
typecheck: <command>
test_unit: <command>
test_acceptance: <command> # K5 step in the G3 workflow, if a profile lists K5
test_integration: <command> # K4 step
architecture: <command> # K8 step
property: <command> # K11 step
differential: <command> # K12 step
mutation: <command> # K10 step. It gets BASE_REF and MUTATION_SCORE_MIN.
all_checks: <command>
commits: # optional. These are the defaults.
by: agent # agent: the role commits. human: the role gives a message.
trailers: commit # commit, notes (git notes), or none
message_format: docs/commit-format.md # optional: a file in the repo, or a URL
paths:
source: [src/]
tests: [tests/] # protected for the implementer
checks: [.github/workflows/, .cornerstone/checks/]
plans: docs/plans/ # or docs/plans/{id}/ for one folder per ticket
specs: docs/specs/ # generated by stone spec apply
reports: docs/reports/ # findings reports. {id} also works here.
limits:
pr_changed_lines: 600
function_lines: 60
cyclomatic: 10
mutation_score_changed_code: 0.6
ci: # optional. Settings of the G3 job (#163, #164)
env: {NAME: value} # plain values. A name matches ^[A-Z_][A-Z0-9_]*$.
secrets: [STRIPE_SECRET_KEY] # names of repo secrets. The step gets ${{ secrets.NAME }}.
branches: [dev] # K6, K7, K14, and K20 run only on a PR into these branches (#164)
setup: # steps before `Install` (#162)
node: "22" # adds actions/setup-node with this version and cache: npm
steps: # then these steps, in order. Each `uses` needs a commit SHA.
- {uses: "owner/repo@<40-character sha>", with: {key: value}}
- {run: ./scripts/ci/prepare.sh}
tickets: # optional. Where the ticket lives (#137)
source: github # github (default), or file
path: docs/tickets/{id}.txt # with source: file. {id} is the ticket ID.
specialists: # optional: reviews by a specialist agent or skill (#123)
- id: accessibility
run: <command> # the command, agent, or skill that starts the review
when: # any one match starts it. No `when`: it always runs.
paths: ["**/components/**"]
personas_with: [screen reader] # a word in the limits of a persona of the ticket
labels: [a11y]
stage: verify # after the tester, before the reviewer (the only stage)
blocks: false # true: a finding with severity high stops G4
owner: <github-username> # optional. Default: owners.default
eval: evals/accessibility/ # optional, but P5 asks for an eval plan
owners:
default: <github-username>
solo: false # true if one human fills all human roles (see modes.md)
modes_allowed: [agent-centric, hybrid, human-centric] # the generator renders files for these
modes: # optional: change the default preset of a profile
single-ticket: agent-centric
writing:
standard: ste # see writing.md
dictionary: docs/style/ste.md # optional project dictionary
max_words_procedure: 20
max_words_description: 25
checks:
links:
paths: [README.md, docs/] # optional: more paths for the link check of stone check
waivers: []
Check K7 uses limits.pr_changed_lines only if it is smaller than the profile limit. To loosen a
limit for one PR, add a waiver to the PR body. A waiver is a Markdown heading of any level with
waiver (P10) in its text, in any case, and text under it. Examples: ## Waiver (P10) and
### Size waiver (P10). A list item is not a waiver. An empty section does not count. Name the
profile of the PR on a Profile: <id> line in the PR body. Without it, K7 uses the smallest limit.
Write each branches.* value as a quoted YAML string if it could parse as a number or a boolean
("1.10", "yes"). YAML reads 1.10 as 1.1 and yes as True, and K16 then compares the wrong
name. stone render and stone check fail on a branches.* value that is not a string (D34).
Each value in paths.tests and paths.checks is a folder, a file, or a glob (D62). A folder or
a file protects that path and each path under it. A value with *, ?, or [ is a glob. It
starts at the repo root:
| Glob | Matches |
|---|---|
*, ?, [...] |
Characters in one folder level. [!...] matches a character that is not in the list. |
** as one level |
Any number of levels, also zero |
A glob that ends with / |
Each path under a folder that the glob matches |
For a test file next to its source, use for example tests: ["**/*.test.js", "**/*.spec.ts"].
**/*.test.js matches a.test.js and src/widget/widget.test.js. Quote a glob in YAML, because
* has a meaning in YAML. stone render and stone check fail on a glob that is not valid, for
example a [ with no ].
stone check tests the relative links in the files that Cornerstone owns (D54). These are
AGENTS.md, the file that imports it, and these folders: constitution/, .cornerstone/, plans,
specs, and the framework folder. To
test more files, list them in checks.links.paths, or run stone check --links all. The check
decodes percent-encoding, resolves a link that starts with / from the repo root, and tests links
in the <...> form.
The generator fills the facts that it can find. It asks a human for the rest. It writes
TODO(<owner>) for an unknown value. It does not guess.
Commands for the "by profile" checks¶
The G3 workflow runs a "by profile" check only if a selected profile lists the check and the repo
config has its command (D29). The command runs after all_checks. Its exit code passes or fails the
step. If a profile lists the check and the command has no value, stone render skips the step and
writes a warning.
| Check | Command key | Example |
|---|---|---|
| K4 | test_integration |
pytest tests/integration |
| K5 | test_acceptance |
npx playwright test |
| K8 | architecture |
lint-imports |
| K10 | mutation |
scripts/mutation-score.sh |
| K11 | property |
pytest tests/property |
| K12 | differential |
pytest tests/differential |
The K10 step gives two environment variables to the command. The command MUST read them. Most mutation tools do not read them, so wrap the tool in a repo script:
| Variable | Value |
|---|---|
BASE_REF |
The base branch of the PR, for example origin/main. Test only the mutants in the code changed since it. |
MUTATION_SCORE_MIN |
limits.mutation_score_changed_code. Fail if the score is under it. |
The K10 step needs limits.mutation_score_changed_code. If it has no value, render stops.
A failing command fails the job, and G3 blocks the merge (D52). The steps have no continue-on-error.
Make sure that each command passes before you add it to commands.
With adoption: all, CI does not know the profile of a PR. So each step runs on each PR in the repo. For example, a repo
with single-ticket and refactor-migration runs K10 on each single-ticket PR too.
Opt-in adoption (adoption: opt-in)¶
Use it when only some people or some tickets use Cornerstone (D58).
- A PR is a Cornerstone PR if its body has a line that starts with
Profile: <id>, or it has the labelcornerstone. - The G3 workflow always starts. A first step finds the marker. Each Cornerstone step (K6, K7,
K14, K15, the repo commands, and
commands.all_checks) runs only for a marked PR. The workflow also runs again when a PR is edited, labeled, or unlabeled. The repo's own CI is in other workflows, so it still runs for each PR. - K6 does not count a commit with
Actor: humanand noRole:as an implementer commit. A commit with no trailer still counts as an implementer commit. - The line must name a profile of this repo config, and nothing else, for example
Profile: single-ticket. A line such asProfile: xorProfile: single-ticket-2is not a marker. - The role files of the implementer and the tester tell the role to add the marker to a PR.
- Anyone can remove the marker, and an unmarked PR gets no check. Turn on branch protection with a required human review for the base branch, so that review controls the PRs that G3 skips.
Gate steps by base branch (ci.branches)¶
Use it when a repo promotes code with PRs between long-lived branches, for example dev to
staging to main (D74).
- K6, K7, K14, and K20 are for ticket PRs. With
ci.branches, these steps run only when the base branch of the PR is in the list. In a PR into another branch, they show as skipped. - Each other step runs on each PR: K15, the repo commands, and
commands.all_checks. So a promotion PR still runs the lint, the tests, and the E2E tests. - The workflow still starts on each PR, so the required status check always reports.
- The list must have
branches.integration.stone checkandstone renderfail if it does not. - Without
ci.branches, K6, K7, K14, and K20 run on each PR, as before. - With
adoption: opt-in, a step runs only for a marked PR into a listed branch.
A PR into an unlisted branch has no K6, K7, K14, or K20. This includes a hotfix PR into main. A
person gates it (G5). Turn on branch protection with a required human review for each unlisted
base branch. If ticket PRs also go into a branch, add that branch to the list.
If the repo cannot use branch rules (for example, a private repo on a GitHub plan without branch rules), use this fallback (D81):
- A person reviews and approves each PR into an unlisted branch before the merge.
- The repo writes this rule in its
AGENTS.md. - This control depends on a person. CI does not enforce it, and
stonedoes not check it. A person can skip it by mistake.
A repo-local hook or script that runs K6, K7, or K14 must use the same rule. It must skip these
checks for a base branch that is not in ci.branches. Cornerstone does not write such a hook, so
the hook does not read ci.branches. Without the skip, a push for a promotion branch fails the
check on the local machine, and CI skips the check.
Bot PRs (Dependabot, Renovate)¶
With adoption: all (the default), a bot PR has no K14 sections, so K14 fails. A large lock
file change can also fail K7. There is no config key to skip these checks for a bot
(D79). A person passes them by hand:
- Edit the PR body. Add the four K14 sections: Evidence, Decision log, Review findings, and Confidence.
- If the diff is over the size limit, add a
Waiver (P10)section with the reason. - Start a new run. Push a commit, or close and open the PR again. An edit of the PR body or a re-run of the job does not read the new body.
A bot can write its PR body again, for example when it rebases the PR. Then do these steps again.
With adoption: opt-in, a bot PR has no marker, so K6, K7, and K14 skip it. An edit of the body
starts a new run there.
A bot commit has no Role: trailer, so K6 counts it as an implementer commit. A bot change of a
protected path, for example an action update in the workflow folder, fails K6. To pass it, add a
Waiver (K6) section with the reason and the owner, and start a new run (D83).
The waiver passes only a commit with a bot author on a PR that a bot opened. Each other commit on
a protected path still fails.
Tickets as files (tickets)¶
With tickets.source: file, the ticket is a file, not a GitHub issue (D60).
This fits a tracker such as Jira, and a local-only setup.
tickets.pathhas{id}once, for the ticket ID. It is a path in the repo, with no glob and no...stone checkfails if it overlapspaths.testsorpaths.checks.- The shaper gets the tools
WriteandEdit. The hook lets the shaper change only the files that matchtickets.path. A Bash write is not blocked, as for the other roles. - The implementer, tester, and reviewer files, and
ticket-delivery, read the ticket from that path. They do not use the issue CLI. - The shaper writes the Definition of Ready fields in the section
## Definition of Readyof the file, oneField: valueline for each field. Check K21 (stone check-ready) reads this form (D82).
Commits (commits)¶
The implementer and the tester read this section (#121, D57).
| Key | Value | Effect |
|---|---|---|
by |
agent (default) |
The role commits. |
by |
human |
The role does not commit. It ends with a proposed commit message. |
trailers |
commit (default) |
Role: and Actor: go in the commit message. |
trailers |
notes |
The role writes Role: and Actor: to the git notes ref refs/notes/cornerstone. Check K6 reads the note if the commit has no trailer. Git does not push notes by default, so the history stays clean. To check in CI, push the ref with git push origin refs/notes/cornerstone. |
trailers |
none |
No role record. stone render warns, and does not render K6. stone gate shows K6 as skipped. |
message_format |
a path or a URL | The roles follow it. stone check fails if a path is not a file in the repo. |
Git can lose a note when a person rebases. Run git config notes.rewriteRef refs/notes/cornerstone
to keep notes through a rebase or an amend.
CI setup (ci.setup)¶
The G3 job installs with commands.install on the default tools of the runner image (#162,
D73). ci.setup adds steps before Install. Without ci.setup, the
workflow does not change.
| Key | Value | Effect |
|---|---|---|
node |
a text, for example "22" |
A step actions/setup-node, pinned to a commit SHA, with this node-version and cache: npm. |
steps |
a list | Steps after the setup-node step. Each item has uses (and with) or run, and can have name. |
stone check and stone render fail on an unknown key, on a wrong type, and on a uses value
that has no commit SHA. The steps run only in the G3 job, not in the role jobs.
Specialist reviews (specialists)¶
A repo can connect its own review agents or skills to the roles (D59).
stone renderwrites.cornerstone/specialists.md: each specialist, its trigger, and the order. The reviewer role file tells the reviewer to read the specialist findings as an input.- Each specialist gets a manifest entry with
source: repo, an owner, and a review date (P5).stone renderwarns if a specialist has noevalplan. stone specialists --base BASE --ticket ID [--labels a,b] [--personas x,y]lists the specialists that match the change, in config order. A trigger matches if onewhencondition matches:paths: a folder, a file, or a glob on the files changed since BASE. The rules are those ofpaths.tests(D62, D65).stone specialistsuses.cornerstone/lib/path_match.py, so runstone renderfirst.labels: a label of the ticket.personas_with: a word in thelimitsof a persona that--personasnames.- A specialist writes its findings to
<paths.reports>/<ticket-id>.<id>.md. With{id}inpaths.reports, the folder is<ticket-id>/too. Each finding has a line such asSeverity: high. The command reports a specialist with no file asmissing. It reports ablocks: truespecialist with a high finding asblocking. It exits with 1 if one is notreviewed. - The command cannot start a specialist. The reviewer has no tool to start another agent (P2). A
person, or
ticket-deliverywith the user, starts each one in a new session. - Check K20 runs the same rule in G3 (D66).
stone renderadds the K20 step to the G3 workflow if a selected profile lists K20. The step reads the PR labels and these PR body lines:Ticket: <id>andPersonas: <ids>. K20 fails on these items: - a
blockingspecialist - a
missingreport of ablocks: truespecialist - a matched change with no
Ticket:line
A missing report of another specialist is a warning. Commit each report in the PR, or K20
cannot see it. A heading with waiver (K20) and text under it passes, with a notice.
Local delivery (delivery: local)¶
Use it when one developer runs the roles in a shared repo before the team agrees to the workflow.
stone render then writes no file in .github/workflows/. It prints a warning that G3 is local
only, and the lines to add to .git/info/exclude. It writes nothing outside the rendered set.
stone gate runs K6, K7, and commands.all_checks on the machine, and prints a table for the
Evidence section of the PR. See D56. With specialists, it also runs K20.
K20 needs --body, and --labels gives the PR labels.
{id} in paths.plans or paths.reports gives each ticket its own folder (D67):
paths:
plans: .stone/plans/{id}/ # .stone/plans/MEG-1/MEG-1.decisions.md
{id}is the last folder level, and it is in the value once. A folder in the repo comes before it. The value is one string, with no...stone renderandstone checkfail on another form.- The role files name
<ticket-id>for{id}. The hook lets a role write only in a ticket folder, not in a flat file in the base folder. - K18 and the spec deltas (K19,
stone spec apply) read the files of each ticket folder. A file in the base folder is not read. - The overlap check below uses the part before
{id}. - Without
{id}, nothing changes: the files are in one flat folder.
stone render and stone check fail if paths.plans or paths.reports overlaps the harness
folder, .cornerstone/, paths.tests, or paths.checks. The hook blocks a role that writes there. Use
folders such as docs/plans/ and docs/reports/, and ignore them in git.
Profile override (.cornerstone/profiles/<profile>.yaml)¶
A repo can change one selected profile with a file .cornerstone/profiles/<profile>.yaml
(D84). stone check validates each file. stone render fails on a file that
is not valid, and applies a valid file (D85).
extends: single-ticket # required: the name of the file
mode: hybrid # optional: a mode in modes_allowed
drivers: # optional: change single stages of the preset
build: pair # a stage of the profile
checks_add: [K8] # optional: more checks from checks.md
limits:
pr_changed_lines: 250 # optional: a looser limit needs a limit waiver
waivers:
- gate: G4 # exactly one of gate, check, or limit
reason: <reason>
owner: <github-username>
until: 2027-01-31
scope: patch-versions # optional: free text
stone check fails the file in these cases:
- A key is not in the form above, or
extendsis not the name of the file. - The profile is not in
profilesof the repo config. - The mode is not in
modes_allowed. A stage indriversis not a stage of the profile. - A driver is not
agent,human, orpair. Onlyverifycan usechecks. - A check in
checks_addis not in checks.md. limits.pr_changed_linesis larger than the limit of the profile and the repo config, and no active waiver haslimit: pr_changed_lines.- A waiver has no reason, no owner, or no valid
untildate, or itsuntildate is in the past. - A waiver names G3 or G5. A gate waiver can name only G1, G2, or G4.
- A check waiver names K6, K7, K14, K16, or K17, or a check that the profile does not run.
stone render applies the file to the selected profile:
| Key | Effect on the rendered files |
|---|---|
checks_add |
The profile runs the check. A check with a step, for example K8, gets its step in the G3 workflow. |
A waiver with check: <K> |
The profile does not run the check until the until date. |
limits.pr_changed_lines |
K7 uses this limit for the profile. A larger value needs a waiver with limit: pr_changed_lines. |
A waiver with gate:, mode, drivers |
No effect. They are records, and the human reads them at the gate. Render does not change the branch protection. |
After the until date of a waiver, stone check and stone render fail. Remove the waiver, or set
a new date with the owner. Then run stone render again.
Task layer (ticket fields)¶
A ticket MAY set the mode for one item, for example with a field or a label mode:human-centric. If
the ticket sets no mode, the profile override or the repo config decides. If neither sets one, the
profile default applies.
Hand-written components¶
A repo can add its own rules, skills, roles, or hooks. Put them in .cornerstone/local/. Add each one
to the manifest with source: repo. The lifecycle rules apply to them too.