Skip to content

Commit 938aa20

Browse files
chrisleekrclaude
andauthored
feat(repo-config): add per-repo .github-app.yaml control surface (#286)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 1e66d72 commit 938aa20

47 files changed

Lines changed: 5872 additions & 417 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ jobs:
132132
# parity gate, so this keeps config.ts, the contract, and the docs aligned.
133133
run: bun run check:env-contract
134134

135+
- name: Config-schema guard
136+
# Fails when schema/github-app.schema.json drifts from
137+
# src/repo-config/schema.ts. That file is what editors consume via the
138+
# `# yaml-language-server: $schema=` modeline, so a stale copy would
139+
# advertise a config surface the runtime no longer accepts.
140+
run: bun run check:config-schema
141+
135142
- name: Docs-sync guard (bot workflows, FR-019)
136143
if: github.event_name == 'pull_request'
137144
env:

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ bun.lockb
88
# Generated/managed assets with intentional non-prettier formatting
99
.claude/skills/
1010
docs/index.md
11+
schema/github-app.schema.json

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ Validate locally with `bun run docs:build` before pushing. If no matching doc ex
171171
**CI-enforced doc gates.** Two project-specific checks run in `.github/workflows/docs.yml` ahead of `mkdocs build --strict` (which only validates internal links and snippet targets, not prose-vs-source agreement):
172172

173173
- Bun version strings in `docs/` **and root-level `README.md` / `CONTRIBUTING.md` / `CLAUDE.md`** are pinned to `.tool-versions` via `bun run scripts/check-docs-versions.ts` (also asserts `package.json` `engines.bun` / `packageManager` and the two `Dockerfile.*` `FROM oven/bun:<ver>` lines agree).
174-
- `src/<file>:<line>` citations in `docs/` **and the same three root-level files** are verified via `bun run scripts/check-docs-citations.ts` (file must exist; cited line / range must be in bounds). Citations may opt in to symbol anchoring with a trailing `#symbol` suffix (e.g. `` `src/core/prompt-builder.ts:155#buildPrompt` ``); when present, the anchor token must physically appear on the cited line range, which closes the silent line-shift hole the bounds-only path can't see (issue #158).
174+
- `src/<file>:<line>` citations in `docs/` **and the same three root-level files** are verified via `bun run scripts/check-docs-citations.ts` (file must exist; cited line / range must be in bounds). Citations may opt in to symbol anchoring with a trailing `#symbol` suffix (e.g. `` `src/core/prompt-builder.ts:179#buildPrompt` ``); when present, the anchor token must physically appear on the cited line range, which closes the silent line-shift hole the bounds-only path can't see (issue #158).
175175

176176
The `docs.yml` `pull_request:` trigger has no `paths:` filter, so these gates run on every PR, code-side bumps that invalidate doc facts (Renovate Bun bump, refactor that shifts cited line numbers) trip the build the same way doc edits do. `Deploy to GitHub Pages` is still gated on `push` / `workflow_dispatch`, so PRs validate but never publish.
177177

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/build/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ The agent executor (`src/core/executor.ts:208`) supports two prompt-layout strat
159159

160160
The `cacheable` layout splits the prompt by trust:
161161

162-
- **Trusted scaffolding** (`security_directive`, `freshness_directive`, workflow steps, commit / CAPABILITIES boilerplate) → `systemPrompt.append`. Built by `buildPromptParts()` in `src/core/prompt-builder.ts:448#buildPromptParts`. Byte-identical across jobs of the same shape, so the system-prompt prefix becomes a stable cache key.
162+
- **Trusted scaffolding** (`security_directive`, `freshness_directive`, workflow steps, commit / CAPABILITIES boilerplate) → `systemPrompt.append`. Built by `buildPromptParts()` in `src/core/prompt-builder.ts:481#buildPromptParts`. Byte-identical across jobs of the same shape, so the system-prompt prefix becomes a stable cache key.
163163
- **Attacker-influenceable data** (`formatted_context` with title / body / comments, `<untrusted_*>` spotlight blocks with per-call nonce, per-call metadata like delivery ID) → user-role message.
164164
- **Dynamic preset sections** stripped via `excludeDynamicSections: true`.
165165

docs/operate/configuration.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,24 @@ Controls the internal scheduler that runs prompt-based actions declared in a
170170
repo's `.github-app.yaml`. See [Scheduled actions](../use/scheduled-actions.md)
171171
for the file schema. Server mode only; a daemon process ignores these.
172172

173-
| Variable | Default | Notes |
174-
| ---------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
175-
| `SCHEDULER_ENABLED` | `false` | Master kill-switch. When false the scheduler never starts. It also will not start without `DATABASE_URL` and a non-empty `ALLOWED_OWNERS`. |
176-
| `SCHEDULER_SCAN_INTERVAL_MS` | `300000` (5 min) | Cadence of the scan that enumerates installations, fetches each `.github-app.yaml`, and enqueues due actions. A value outside `[60000, 3600000]` is rejected at startup. |
177-
| `SCHEDULER_ALLOW_AUTO_MERGE` | `false` | Hard kill-switch for unattended auto-merge. Effective auto-merge requires BOTH this AND a per-action `auto_merge: true`; otherwise no merge tool runs. |
178-
| `SCHEDULER_CONFIG_FILE` | `.github-app.yaml` | Filename read from each installed repo's default-branch root. |
173+
| Variable | Default | Notes |
174+
| ---------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
175+
| `SCHEDULER_ENABLED` | `false` | Master kill-switch. When false the scheduler never starts. It also will not start without `DATABASE_URL` and a non-empty `ALLOWED_OWNERS`. |
176+
| `SCHEDULER_SCAN_INTERVAL_MS` | `300000` (5 min) | Cadence of the scan that enumerates installations, fetches each `.github-app.yaml`, and enqueues due actions. A value outside `[60000, 3600000]` is rejected at startup. |
177+
| `SCHEDULER_ALLOW_AUTO_MERGE` | `false` | Hard kill-switch for unattended auto-merge. Effective auto-merge requires BOTH this AND a per-action `auto_merge: true`; otherwise no merge tool runs. |
178+
179+
## Per-repo config file
180+
181+
Selects the file each installed repo is read from. Unlike the scheduler
182+
variables above this is **not** server-mode only: the same document carries
183+
feature toggles, agent overrides, and trigger filters, so any process that
184+
resolves repo policy reads it. Only the default branch's copy is ever applied.
185+
See [Per-repo configuration](../use/repo-config.md) for the file schema.
186+
187+
| Variable | Default | Notes |
188+
| ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
189+
| `REPO_CONFIG_FILE` | `.github-app.yaml` | Filename read from each installed repo's default-branch root. Trimmed at load: a stray space would 404 on every repo and silence the whole surface with nothing logged. |
190+
| `SCHEDULER_CONFIG_FILE` | (unset) | **Deprecated** former name for `REPO_CONFIG_FILE`. Still honoured as a fallback so an upgrade does not silently change which file is read; logs a one-shot boot warning. |
179191

180192
## Review learnings
181193

@@ -217,9 +229,9 @@ Selects the system/user prompt split the agent executor passes to the Claude Age
217229

218230
**Why this exists.** The SDK's default systemPrompt (`{ type: "preset", preset: "claude_code" }`) embeds dynamic sections (cwd, platform, shell, OS) directly in the system-prompt prefix. Because each delivery clones to a unique `cwd` under `CLONE_BASE_DIR`, the system-prompt prefix is unique per job and the Anthropic prompt cache misses on every invocation, paying the 1-hour TTL `ephemeral_1h_input_tokens` cache-write surcharge (2× base price) with zero compensating reads.
219231

220-
**`legacy` (default).** Single user-role string built by `buildPrompt()` in `src/core/prompt-builder.ts:155#buildPrompt`. SystemPrompt is the unmodified `claude_code` preset. Backwards-compatible; safe rollback target.
232+
**`legacy` (default).** Single user-role string built by `buildPrompt()` in `src/core/prompt-builder.ts:179#buildPrompt`. SystemPrompt is the unmodified `claude_code` preset. Backwards-compatible; safe rollback target.
221233

222-
**`cacheable`.** Static scaffolding (`security_directive`, `freshness_directive`, workflow steps, commit/CAPABILITIES boilerplate) is lifted into `systemPrompt.append`, and `excludeDynamicSections: true` strips cwd / platform / shell / OS from the preset. Built by `buildPromptParts()` in `src/core/prompt-builder.ts:448#buildPromptParts`. The user-role message keeps only the per-call dynamic blocks (`formatted_context`, `untrusted_*` with per-call nonce, per-call metadata). The append is byte-identical across jobs of the same shape (PR vs issue), so the system-prompt prefix becomes a stable cache key.
234+
**`cacheable`.** Static scaffolding (`security_directive`, `freshness_directive`, workflow steps, commit/CAPABILITIES boilerplate) is lifted into `systemPrompt.append`, and `excludeDynamicSections: true` strips cwd / platform / shell / OS from the preset. Built by `buildPromptParts()` in `src/core/prompt-builder.ts:481#buildPromptParts`. The user-role message keeps only the per-call dynamic blocks (`formatted_context`, `untrusted_*` with per-call nonce, per-call metadata). The append is byte-identical across jobs of the same shape (PR vs issue), so the system-prompt prefix becomes a stable cache key.
223235

224236
**Rollout.** Flip the variable to `cacheable`, then verify cache hits by tailing the executor completion log for non-zero `cacheReadInputTokens`:
225237

0 commit comments

Comments
 (0)