Skip to content

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:

  1. A later layer MAY add a gate, a check, or a protected path.
  2. A later layer MAY tighten a limit.
  3. 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.
  4. No layer can remove G3 or G5 (see gates.md).
  5. 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 label cornerstone.
  • 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: human and no Role: 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 as Profile: x or Profile: single-ticket-2 is 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 check and stone render fail 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 stone does 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:

  1. Edit the PR body. Add the four K14 sections: Evidence, Decision log, Review findings, and Confidence.
  2. If the diff is over the size limit, add a Waiver (P10) section with the reason.
  3. 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.path has {id} once, for the ticket ID. It is a path in the repo, with no glob and no ... stone check fails if it overlaps paths.tests or paths.checks.
  • The shaper gets the tools Write and Edit. The hook lets the shaper change only the files that match tickets.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 Ready of the file, one Field: value line 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 render writes .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 render warns if a specialist has no eval plan.
  • 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 one when condition matches:
  • paths: a folder, a file, or a glob on the files changed since BASE. The rules are those of paths.tests (D62, D65). stone specialists uses .cornerstone/lib/path_match.py, so run stone render first.
  • labels: a label of the ticket.
  • personas_with: a word in the limits of a persona that --personas names.
  • A specialist writes its findings to <paths.reports>/<ticket-id>.<id>.md. With {id} in paths.reports, the folder is <ticket-id>/ too. Each finding has a line such as Severity: high. The command reports a specialist with no file as missing. It reports a blocks: true specialist with a high finding as blocking. It exits with 1 if one is not reviewed.
  • The command cannot start a specialist. The reviewer has no tool to start another agent (P2). A person, or ticket-delivery with the user, starts each one in a new session.
  • Check K20 runs the same rule in G3 (D66). stone render adds 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> and Personas: <ids>. K20 fails on these items:
  • a blocking specialist
  • a missing report of a blocks: true specialist
  • 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 render and stone check fail 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 extends is not the name of the file.
  • The profile is not in profiles of the repo config.
  • The mode is not in modes_allowed. A stage in drivers is not a stage of the profile.
  • A driver is not agent, human, or pair. Only verify can use checks.
  • A check in checks_add is not in checks.md.
  • limits.pr_changed_lines is larger than the limit of the profile and the repo config, and no active waiver has limit: pr_changed_lines.
  • A waiver has no reason, no owner, or no valid until date, or its until date 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.