Skip to content

Commit 7edc5cd

Browse files
chrisleekrclaude
andauthored
feat(repo-config): add per-repo .github-app.yaml control surface
Promotes the scheduler-only config reader into a first-class per-repo control surface under `src/repo-config/`, and adds the two gates that consume it. - Move `src/scheduler/config-schema.ts` + `config-fetcher.ts` to `src/repo-config/{schema,fetcher}.ts` and widen the document schema beyond scheduled actions. The fetcher now returns a discriminated `ok` / `absent` / `invalid` result instead of a nullable value. - Add `src/repo-config/effective.ts` to merge `workflows.<name>` over `defaults` and clamp the result against the server env ceilings, failing open to `DEFAULT_REPO_POLICY`. - Add `src/repo-config/gate.ts` (Gate 1): a narrowing-only pre-dispatch check. Every rule can refuse, none can permit, so no YAML value can readmit a repo the `ALLOWED_OWNERS` env allowlist rejected. - Add `src/repo-config/pr-check.ts`, the one module that reads a head-ref copy. It is read-only by construction and imports neither `fetchRepoConfig` nor `loadRepoPolicy`, so a head-ref read can never populate the fetcher caches or reach the applied policy. - Add `src/core/agent-policy.ts` (Gate 2) and apply the resolved knobs in the pipeline and prompt builder. - Rename `SCHEDULER_CONFIG_FILE` to `REPO_CONFIG_FILE`, keeping the old name as a deprecated fallback with a one-shot boot warning. - Generate `schema/github-app.schema.json` from the zod schema and gate it in CI via `check:config-schema`, so the `$schema` modeline authors consume cannot advertise a surface the runtime rejects. Only the default branch's copy is ever applied: `fetchRepoConfig` calls `getContent` with no `ref`, so a config edit inside a pull request is inert for that pull request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUPpJPtxAaHWrBsjytRGyM
1 parent 2790486 commit 7edc5cd

51 files changed

Lines changed: 5890 additions & 428 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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ for the file schema. Server mode only; a daemon process ignores these.
175175
| `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`. |
176176
| `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. |
177177
| `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. |
178+
| `REPO_CONFIG_FILE` | `.github-app.yaml` | Filename read from each installed repo's default-branch root. No longer scheduler-specific: also carries feature toggles, agent overrides, and trigger filters. |
179+
| `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. |
179180

180181
## Review learnings
181182

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

218219
**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.
219220

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.
221+
**`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.
221222

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.
223+
**`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.
223224

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

0 commit comments

Comments
 (0)