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
6 changes: 4 additions & 2 deletions .agents/skills/bot-instructions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ repo-effects:
.agents/skills/bot-instructions/scripts/bot-instructions retire # revoke kendex automatic rendering on removal
```

Flags: `--repo`, `--spec`, `--staged`, `--dry-run`; `bot-instructions --help`. Python 3.11+.
Flags: `--repo`, `--spec`, `--staged`, `--output full|local`, `--dry-run`; `bot-instructions --help`. Python 3.11+.

`--output local` limits `render`, `check` and `adopt` to the `AGENTS.md` review region. The doctrine loader selects `SKILL.md` or `SKILL.md.disabled`. The pointer uses that filename and the `#doctrine` anchor inside the selected checkout installation. Checkout parent paths resolve through their symlinks. The installation's own symlink keeps its checkout path. Source reads use its canonical target. An external `--spec` needs a matching selected installation inside the checkout. Otherwise local mode refuses: use `--output full` or install the package inside the checkout. Remote files are excluded from validation, adoption, writes, orphan removal and ownership reports. Local review reads the package doctrine directly; repository doctrine overrides remain in the full render. The default `--output full` keeps every enabled bot output. `adopt --dry-run` reports the content to take over and its divergence, writes no marker, and refuses symlinked targets.

Exit codes: 0 clean, 1 findings, 2 could not complete. An unconfigured repository follows [schemas/validators.md](schemas/validators.md) § `orphan`, including its empty JSON ownership report. The `commit-guards` chain runs `check --staged` itself where this package is installed. A pre-commit lane blocks on nonzero codes, except an older check's `unconfigured` refusal carrying its `renders=none` attestation.

Expand Down Expand Up @@ -76,7 +78,7 @@ The generator owns only the `AGENTS.md` § Code Review Rules region and never cr

## The doctrine lives in one file per repo

`[bot-instructions.bots] codex` writes the complete doctrine to `[bot-instructions.repo] code_review_path`, which defaults to `.github/instructions/code-review.md`, and writes the `AGENTS.md` owned region as one directive line naming it. A longer region is a finding: `adopt` reports it under `agents-region` and still writes the marker, `check` reports it under `drift`, and `render` replaces it. A repo migrates by rendering. Body and bounds: [schemas/renders.md](schemas/renders.md) § `code-review.md`.
In full mode, `[bot-instructions.bots] codex` writes the complete doctrine to `[bot-instructions.repo] code_review_path`, which defaults to `.github/instructions/code-review.md`, and writes the `AGENTS.md` owned region as one directive line naming it. A longer region is a finding: `adopt` reports it under `agents-region` and still writes the marker unless `--dry-run` is set, `check` reports it under `drift`, and `render` replaces it. A repo migrates by rendering. Body and bounds: [schemas/renders.md](schemas/renders.md) § `code-review.md`.

**No vendor page documents a bot following an in-file reference.** CodeRabbit's `code_guidelines.filePatterns` is a real load, so its doctrine is not at issue; Codex and Copilot reach the pointed file only by opening what the directive names, and a bot that does not reviews with no repo rules at all. A repo enabling `codex` renders a canary to find out — one harmless rule that is exclusive among the files the bot under test reads, so its appearance in a comment proves the comment was written against the pointed file. A `[bot-instructions.doctrine.append]` on a block the `copilot-instructions.md` routing column does not carry gives that exclusivity for Codex and Copilot, which read no other file carrying that block; [schemas/renders.md](schemas/renders.md) § Doctrine routing note (b) defines that column. An append to a block the column does carry, today `severity`, is not a Copilot canary: Copilot loads `copilot-instructions.md` itself, so the append can appear in its comments without Copilot following the pointer. No append is exclusive in general, since an append reaches every destination its block routes to, Qodo's and Macroscope's included. [references/limits.md](references/limits.md) § GitHub Copilot code review carries the evidence and the failure mode.

Expand Down
6 changes: 4 additions & 2 deletions .agents/skills/bot-instructions/schemas/renders.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

One section per output. Each names the exact path, what the generator owns inside it, which doctrine blocks it carries and in what order, and how repo text from `[bot-instructions]` is escaped into that file's syntax.

Whole-file outputs in this reference apply to full mode. SKILL.md documents local mode and its installed-doctrine pointer.

Doctrine block ids are the `###` headings of the doctrine source, whose parse rule is SKILL.md § Doctrine. The routing table below is the only other place they are written down, and `doctrine-routing` holds the two to the same set.

## Common rules
Expand Down Expand Up @@ -119,9 +121,9 @@ If you are a review agent reviewing code, read <code_review_path> before you com

No doctrine block reaches this region, so the routing table has no column for it. Every harness loads this file at the start of every session, in this repo and in every repo the package renders, and a working session never uses the review rules; what the region owes a review agent is the name of the file that holds them.

**Any longer region is a finding**, so a repo cannot keep the doctrine here by hand. `adopt` reports one under `agents-region` and still writes the marker, because the marker is what makes the repair a single `render`. `check` reports one under `drift`, which holds the region against a fresh render and is that comparison's only owner. `render` replaces it, which is the migration.
**Any longer region is a finding**, so a repo cannot keep the doctrine here by hand. `adopt` reports one under `agents-region` and writes the marker unless `--dry-run` is set, because the marker is what makes the repair a single `render`. `check` reports one under `drift`, which holds the region against a fresh render and is that comparison's only owner. `render` replaces it, which is the migration.

**Escaping.** Nothing is interpolated but the configured path, whose class `repo-toml.md` § `[bot-instructions.repo]` fixes. No doctrine or repo string reaches this region at all, so the heading predicate has nothing to do here; the surfaces it protects are the whole-file markdown outputs those strings do reach, which `repo-toml.md` § Render-side second checks names.
**Escaping.** Full mode interpolates only the configured path, whose class `repo-toml.md` § `[bot-instructions.repo]` fixes. Local mode uses the selected installation's checkout path and Doctrine anchor, under SKILL.md's `--output local` contract. No doctrine or repo string reaches this region at all, so the heading predicate has nothing to do here; the surfaces it protects are the whole-file markdown outputs those strings do reach, which `repo-toml.md` § Render-side second checks names.

## `code-review.md`

Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/bot-instructions/schemas/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Every bot in this set fails silently. A rejected config, a mistyped enum, an exc

So each validator below names the silent failure it exists to catch, then what it rejects. A failure fails the run; none of them warns. Which tree each one reads, and which verb runs it, is § Where these run, and it is not the same answer for all of them.

Remote-file validation below applies to full mode. Local mode judges the `AGENTS.md` region only, as SKILL.md defines.

**No validator parses a render back.** One whose input is bytes this run emitted checks the emitter against itself and needs a reader for every format this package writes. The structure is in hand before it is serialised, so a validator that judges a document reads that structure. What is left to judge on bytes is size, which has no pre-emission form. A validator whose input is the REPO's own files is a different thing: `orphan`, `drift` and `agents-section` are it.

## Controls
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/bot-instructions/scripts/bot-instructions
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# check with what it already has.
set -euo pipefail

here="$(cd -- "$(dirname -- "$0")" && pwd -P)"
here="$(cd -- "$(dirname -- "$0")" && pwd -L)"

if ! command -v python3 >/dev/null 2>&1; then
echo "bot-instructions: python3=missing" >&2
Expand Down
34 changes: 27 additions & 7 deletions .agents/skills/bot-instructions/scripts/lib/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ def parser():
help="read the index rather than the working tree, for every render input as "
"well as the outputs, so a pre-commit lane judges one coherent state",
)
p.add_argument("--dry-run", action="store_true", help="render: validate and write nothing")
p.add_argument("--output", choices=("full", "local"), default="full",
help="render, check, adopt: full bot files (default), or only the local AGENTS.md region")
p.add_argument("--dry-run", action="store_true", help="render, adopt: preview and write nothing")
p.add_argument("--json", action="store_true", help="check: report verified whole-file paths as JSON")
p.add_argument("--input", default=None, help=argparse.SUPPRESS)
return p
Expand All @@ -94,6 +96,22 @@ def running_copy():
return os.path.dirname(os.path.dirname(os.path.dirname(here)))


def _installed_spec(repo, named, spec_root):
"""Keep the selected checkout installation, including a package-store symlink.

The launcher can select that installation when an explicit canonical
--spec names the same package. Source reads still use spec_root.
"""
invoked = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
for candidate in (os.path.abspath(named) if named else invoked, invoked):
# Resolve checkout aliases while retaining the installation's own link.
candidate = os.path.join(os.path.realpath(os.path.dirname(candidate)), os.path.basename(candidate))
relative = os.path.relpath(candidate, repo)
if relative.split(os.sep)[0] != os.pardir and os.path.realpath(candidate) == spec_root:
return candidate
return None


def _region_bounds(path):
"""Report the package-owned body span for one host-supplied snapshot."""
try:
Expand Down Expand Up @@ -147,11 +165,9 @@ def main(argv=None):
print("--staged is a check mode; render and adopt write the working tree",
file=sys.stderr)
return 2
if args.dry_run and args.verb != "render":
# `adopt` is the one-time verb that writes, so a flag it accepted and
# ignored would take the files over on a run meant to preview.
if args.dry_run and args.verb not in ("render", "adopt"):
print("bot-instructions: usage=--dry-run", file=sys.stderr)
print(f"--dry-run is a render mode; {args.verb} does not write a set to preview",
print(f"--dry-run belongs to render and adopt; {args.verb} does not write a set to preview",
file=sys.stderr)
return 2
if args.verb == "retire":
Expand All @@ -169,10 +185,14 @@ def main(argv=None):
try:
work = tree.open_tree(repo, args.staged)
spec_tree, spec_paths = _spec_source(repo, spec_root, work, args.staged)
installed_spec = _installed_spec(repo, args.spec, spec_root)
launcher = os.path.join(package_root, "scripts", "bot-instructions")
if args.output == "local" and installed_spec is not None:
launcher = os.path.join(installed_spec, "scripts", "bot-instructions")
ctx = run.Context(repo, work, spec_tree, spec_paths,
"render" if args.verb == "render" else "check",
spec_names=SPEC_FILES, launcher=launcher)
spec_names=SPEC_FILES, launcher=launcher, output_mode=args.output,
installed_spec=installed_spec)
if isinstance(ctx.config, Unconfigured):
if args.verb == "adopt":
raise ctx.config
Expand All @@ -183,7 +203,7 @@ def main(argv=None):
elif args.verb == "check":
lines = verbs.check_verb(ctx)
else:
lines = verbs.adopt_verb(ctx, repo)
lines = verbs.adopt_verb(ctx, repo, dry_run=args.dry_run)
except ValidationFailed as exc:
for line in exc.report:
print(line)
Expand Down
31 changes: 22 additions & 9 deletions .agents/skills/bot-instructions/scripts/lib/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


class Build:
def __init__(self, model, files, data, region_body):
def __init__(self, model, files, data, region_body, output_mode="full"):
self.model = model
self.files = files # repo-relative path -> text
# The structure each of those paths was serialised FROM, for the
Expand All @@ -23,18 +23,31 @@ def __init__(self, model, files, data, region_body):
# itself, and would need a reader for every format this writes.
self.data = data # repo-relative path -> structure
self.region_body = region_body # None when [bot-instructions.bots] codex is false
self.remote = output_mode == "full"
self.schema = None
self.review_path = None

def command(self, verb):
"""Keep repair and retry commands in this build's output selection."""
return verb if self.remote else f"{verb} --output local"

def build(model, schema=None):
"""The scratch tree. `schema` is the vendored CodeRabbit schema, already
read under `coderabbit-schema`'s own clause: reading it again here would
report a second failure as a `toml-schema` finding at
`[bot-instructions]`."""

def build(model, schema=None, output_mode="full", schema_loader=None, local_review_path=None):
"""The selected scratch tree. Load the remote schema only when needed;
the context's loader attributes its failures to `coderabbit-schema`."""
bots = model.config.bots
files, data = {}, {}
region = None
result = Build(model, files, data, None, output_mode)
# Local review reads the installed doctrine directly. It must not point
# at the remote doctrine file this output set deliberately leaves alone.
result.review_path = model.code_review_path if result.remote else local_review_path
if bots["codex"]:
region = render_markdown.agents_region_body(model)
result.region_body = render_markdown.agents_region_body(model, result.review_path)
if not result.remote:
return result
if bots["coderabbit"] and schema is None and schema_loader is not None:
schema = schema_loader()
result.schema = schema
if bots["copilot"]:
files[".github/copilot-instructions.md"] = render_markdown.copilot_instructions(model)
for surface in model.surfaces:
Expand Down Expand Up @@ -79,7 +92,7 @@ def build(model, schema=None):
"file, and only the later one would survive"
)
files[path] = render_markdown.code_review(model)
return Build(model, files, data, region)
return result


def load_schema(tree):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,25 @@ def block_paragraphs(model, bid, text):
return paragraphs(text)


def agents_directive(model):
def agents_directive(model, path=None):
"""The one line the owned region carries below its marker.

Read twice: the render writes it, and `adopt` asks whether the region it
found is already this. A second spelling there would let a region pass the
adopt report and still differ from what `render` writes.
"""
return AGENTS_DIRECTIVE.format(path=model.code_review_path)
return AGENTS_DIRECTIVE.format(path=path or model.code_review_path)


def agents_region_body(model):
def agents_region_body(model, path=None):
"""The body the write phase splices under the `## Code Review Rules` line.

The marker and the directive, and no doctrine at all. Every harness loads
this file at the start of every session and a working session never uses
the review rules, so what the region carries is the pointer to the file
that does.
"""
return "\n".join([model.marker("html"), "", agents_directive(model), ""])
return "\n".join([model.marker("html"), "", agents_directive(model, path), ""])


def code_review(model):
Expand Down
Loading
Loading