Skip to content

Commit 9aefe66

Browse files
chrisleekrclaude
andauthored
feat: add env-var contract + CI gate for helm-charts sync (#258)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 22503c8 commit 9aefe66

8 files changed

Lines changed: 836 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ jobs:
124124
# is actually enforced. See issue #203.
125125
run: bun run check:no-destructive
126126

127+
- name: Env-contract guard
128+
# Fails on a stale env-contract.json (vs src/config.ts's loadConfig),
129+
# an env var undocumented in docs/operate/configuration.md, or a
130+
# secret-shaped name not classified in SECRET_ENV_VARS. chrisleekr/helm-charts
131+
# consumes env-contract.json (at v<appVersion>) for its ConfigMap/Secret
132+
# parity gate, so this keeps config.ts, the contract, and the docs aligned.
133+
run: bun run check:env-contract
134+
127135
- name: Docs-sync guard (bot workflows, FR-019)
128136
if: github.event_name == 'pull_request'
129137
env:

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ Four workflow files form the pipeline; each owns one responsibility.
124124
- Defense-in-depth on workflow injection: every dynamic input flowing into a `run:` block is passed via `env:` first.
125125
- **GitHub Actions are SHA-pinned.** Every third-party `uses:` reference is pinned to a full 40-char commit SHA (with a `# vX.Y.Z` comment), not a mutable tag, so a force-moved upstream tag cannot change the bytes a runner executes. Renovate keeps the SHAs current via the `helpers:pinGitHubActionDigests` preset behind the existing 7-day `minimumReleaseAge` soak. `ci.yml` runs `bun run check:action-pins` (`scripts/check-action-pins.ts`), which fails the build if any third-party `uses:` is on a tag. Local reusable-workflow calls (`uses: ./...`) are exempt.
126126
- **Test-glob + destructive-action guards run in CI.** `ci.yml` also runs `bun run check:test-globs` (`scripts/check-test-globs.ts`, fails if any `*.test.ts` is unreachable by the runner glob set, issue #201) and `bun run check:no-destructive` (`scripts/check-no-destructive-actions.ts`, FR-009: fails if `src/workflows/ship/` or a `src/daemon/scoped-*-executor.ts` contains a force-push / `reset --hard` / `gh pr merge` / merge-mutation call outside comments; the scoped-executor set is derived from the filesystem so it cannot go stale, issue #203). Its `FORBIDDEN` pattern set is shared with the paired runtime layer, the `PreToolUse` destructive-Bash hook (`src/core/hooks/forbidden-bash.ts`, security invariant #5), via `src/utils/forbidden-bash.ts` so the static and runtime gates stay in lockstep (issue #222).
127+
- **Env-contract guard runs in CI.** `ci.yml` runs `bun run check:env-contract` (`scripts/env-contract.ts --check`), which extracts every env var read by `loadConfig()` in `src/config.ts` and fails if the committed `env-contract.json` is stale, if any var is undocumented in `docs/operate/configuration.md`, or if a credential-shaped name (KEY/TOKEN/SECRET/PASSWORD/CREDENTIAL/BEARER segment, or a URL/DSN suffix that is not allowlisted) is missing from `SECRET_ENV_VARS` (`src/config-secret-env.ts`, the single source of secret-vs-config classification). After changing the env schema, rerun `bun run env-contract` and commit the regenerated `env-contract.json`. That file is the contract the `chrisleekr/helm-charts` `github-app` chart consumes (at `v<appVersion>`) to gate its ConfigMap/Secret parity.
127128

128129
## Security invariants (prompt-injection hardening)
129130

docs/operate/configuration.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,15 @@ The orchestrator also expects a pre-existing `daemon-secrets` Kubernetes Secret
113113

114114
## Triage
115115

116-
| Variable | Default | Notes |
117-
| ----------------------------- | ----------- | ------------------------------------------------------------------------------------------------------ |
118-
| `TRIAGE_ENABLED` | `true` | Kill-switch. When `false`, triage returns `heavy=false` and the job routes to `persistent-daemon`. |
119-
| `TRIAGE_MODEL` | `haiku-3-5` | Alias resolved at runtime. |
120-
| `TRIAGE_CONFIDENCE_THRESHOLD` | `1.0` | Below this, triage is treated as sub-threshold and the job routes to `persistent-daemon`. |
121-
| `TRIAGE_MAX_TOKENS` | `256` | Cap on the JSON response. Above ~100 is wasted budget. |
122-
| `TRIAGE_TIMEOUT_MS` | `5000` | Per-call wall clock. Beyond this, the circuit-breaker counter increments. |
123-
| `INTENT_CONFIDENCE_THRESHOLD` | `0.75` | Range `[0, 1]`. Below this, a mention-driven comment gets a clarification reply instead of a dispatch. |
116+
| Variable | Default | Notes |
117+
| ----------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------- |
118+
| `TRIAGE_ENABLED` | `true` | Kill-switch. When `false`, triage returns `heavy=false` and the job routes to `persistent-daemon`. |
119+
| `TRIAGE_TOOLS_ENABLED` | `true` | Kill-switch for the tool-driven triage classifier. When `false`, triage runs without the on-demand state-fetch tools. |
120+
| `TRIAGE_MODEL` | `sonnet-4-6` | Alias resolved at runtime. |
121+
| `TRIAGE_CONFIDENCE_THRESHOLD` | `1.0` | Below this, triage is treated as sub-threshold and the job routes to `persistent-daemon`. |
122+
| `TRIAGE_MAX_TOKENS` | `256` | Cap on the JSON response. Above ~100 is wasted budget. |
123+
| `TRIAGE_TIMEOUT_MS` | `5000` | Per-call wall clock. Beyond this, the circuit-breaker counter increments. |
124+
| `INTENT_CONFIDENCE_THRESHOLD` | `0.75` | Range `[0, 1]`. Below this, a mention-driven comment gets a clarification reply instead of a dispatch. |
124125

125126
## Discussion digest
126127

@@ -134,6 +135,17 @@ workflow prompt consumes in place of the raw thread. It is fail-open (any LLM or
134135
parse error falls back to body-only / raw-comment context) and has no comment-count
135136
cap, so there is nothing else to tune.
136137

138+
## Chat-thread executor
139+
140+
Tunables for the conversational scoped-intent path (`src/workflows/ship/scoped/chat-thread.ts`).
141+
142+
| Variable | Default | Notes |
143+
| -------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
144+
| `CHAT_THREAD_EXECUTE_THRESHOLD` | `0.8` | Range `[0, 1]`. Minimum classifier confidence before a chat-thread proposal is auto-executed rather than left pending. |
145+
| `CHAT_THREAD_PROPOSAL_TTL_HOURS` | `24` | Hours a pending chat-thread proposal stays valid before it expires. |
146+
| `CHAT_THREAD_MAX_TURNS` | `8` | Cap on conversational turns within a single chat-thread session. |
147+
| `CHAT_THREAD_TOOLS_ENABLED` | `true` | Kill-switch for the chat-thread agent's on-demand state-fetch tools. |
148+
137149
## Ship
138150

139151
| Variable | Default | Notes |
@@ -145,6 +157,7 @@ cap, so there is nothing else to tune.
145157
| `REVIEW_BARRIER_SAFETY_MARGIN_MS` | `1200000` (20 min) | Minimum elapsed time since the last bot push before the bot may declare `ready` without a non-bot review on the current head SHA. |
146158
| `FIX_ATTEMPTS_PER_SIGNATURE_CAP` | `3` | Max attempts per failure signature within a single intent. Cap firing terminates with `terminal_blocker_category='flake-cap'`. |
147159
| `SHIP_FORBIDDEN_TARGET_BRANCHES` | empty | Comma-separated branches the bot refuses to shepherd PRs against. |
160+
| `REVIEW_RESOLVE_MAX_ITERATIONS` | `2` | Range `[1, 5]`. Max review/resolve loop iterations in the composite ship flow before the intent yields. |
148161

149162
## Scheduled actions
150163

0 commit comments

Comments
 (0)