Skip to content

chore: promote staging to staging-promote/41c73878-24785020894 (2026-04-22 15:56 UTC) - #2860

Merged
henrypark133 merged 2 commits into
mainfrom
staging-promote/c559a588-24788480958
Apr 29, 2026
Merged

henrypark133 merged 2 commits into
mainfrom
staging-promote/c559a588-24788480958

Conversation

@ironclaw-ci

@ironclaw-ci ironclaw-ci Bot commented Apr 22, 2026 •

Copy link
Copy Markdown
Contributor

Auto-promotion from staging CI

Batch range: 7fb41555a9e55677d1aaea29ca567a5b369c2b05..c559a58810896093a5cf768b424906a6f94aad7b
Promotion branch: staging-promote/c559a588-24788480958
Base: staging-promote/41c73878-24785020894
Triggered by: Staging CI batch at 2026-04-22 15:56 UTC

Commits in this batch (79):

Current commits in this promotion (2)

Current base: staging-promote/41c73878-24785020894
Current head: staging-promote/c559a588-24788480958
Current range: origin/staging-promote/41c73878-24785020894..origin/staging-promote/c559a588-24788480958

Auto-updated by staging promotion metadata workflow

Waiting for gates:

  • Tests: pending
  • E2E: pending
  • Claude Code review: pending (will post comments on this PR)

Auto-created by staging-ci workflow

ilblackdragon and others added 2 commits April 23, 2026 00:36
* feat(safety): projection-exempt lint for gateway event sources

Phase 1 of the gateway state-convergence epic (#2792): add check #9 to
`scripts/pre-commit-safety.sh` that flags newly-added
`sse.broadcast(` / `sse.broadcast_for_user(` calls without a
`// projection-exempt: <reason>` annotation on the same line.

The invariant is documented in the new `.claude/rules/gateway-events.md`:

- Every `AppEvent` must project from a typed source log (engine
  `EventKind`, sandbox `JobEvent`, or a channel-lifecycle log).
- A short transport-only allowlist (`Heartbeat`, `StreamChunk`) covers
  the ephemeral variants with no state backing them.
- Direct emits are the root cause of the state-drift class — UI stream
  and replayable source end up with different stories. Four recent
  incidents (#2654, #2534, #2731, #2079) share this shape.

The lint is diff-based, so pre-existing unannotated call sites aren't
broken. Baseline annotation of the ~20 existing emit sites is the next
PR under Phase 1 — this one establishes the gate.

Suppressions require a named category (`bridge dispatcher`,
`channel-lifecycle`, `sandbox JobEvent`, `transport-only, heartbeat`,
or `migrate in #NNNN`). An unnamed `legacy` reason is rejected by
review, not by the lint itself.

Tested locally:
- Fires on unannotated `sse.broadcast(...)` in a new file.
- Suppressed by `// projection-exempt: transport-only, heartbeat`.
- Does not match `Channel::broadcast` (different trait).
- Does not match calls inside `#[cfg(test)] mod tests` blocks (via
  the shared `strip_test_mod_lines` filter).

Refs: #2792, #2654

* refactor(safety): address review feedback on projection-exempt check

Four review comments from Copilot and Gemini on #2840:

1. **Match rustfmt's method-chain wrapping.** The original regex only
   caught same-line `sse.broadcast(...)`. Long calls like
   `state\n    .sse\n    .broadcast_for_user(...)` — produced by
   rustfmt and already in-tree at
   `src/channels/web/features/extensions/mod.rs:645` — would bypass the
   check. New matcher adds a dangling-method alternation that catches
   `.broadcast_for_user(` at line start. Only the `_for_user` suffix
   (SseManager-unique) is matched in dangling form; bare
   `.broadcast(` can be `Channel::broadcast` trait, which is
   intentionally out of scope.

2. **Enforce the documented annotation format.** The check previously
   accepted any `// projection-exempt:` comment, including bare
   `// projection-exempt: legacy` that the rule doc explicitly forbids.
   Negative filter now requires `<category>, <detail>` — presence of a
   comma separating the category from the detail.

3. **Point at the real path in the warning.** Replace
   `bridge::thread_event_to_app_events` with `thread_event_to_app_events`
   in `src/bridge/router.rs` — the actual file location.

4. **Update suppression hint** to show the `<category>, <detail>`
   format rather than the generic `<reason>`.

Verified against a 6-case fixture (same-line fire + suppress,
dangling-chain fire + suppress, unnamed-category fire,
`Channel::broadcast` silent).

Refs: #2792, #2840 review

* fix(safety): match header exclusion against grep -n prefixed output

After `grep -nE '^\+'`, every line is prefixed with `N:`, so the
`^\+\+\+` anchor for filtering diff header lines (`+++ b/file.rs`)
never fires. The positive patterns already exclude header lines by
shape, so today this is harmless — but the dead branch masks future
defense-in-depth failures if the template is reused with a less
specific positive match.

Replace `^\+\+\+` with `:\+\+\+ ` in DISPATCH, CREDNAME, and PROJECTION
checks so the exclusion works against the `grep -n` output shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(safety): regression for grep-n-prefixed header exclusion

Covers PROJECTION / DISPATCH / CREDNAME pipelines:
- diff header lines (`+++ b/path`) are filtered after `grep -n`
- real broadcast/state/CredentialName lines are still flagged
- `// projection-exempt: <category>, <detail>` exempts
- bare `// projection-exempt: legacy` (no comma) is not exempt

Locks in that `:\+\+\+ ` (matches the `grep -n` prefixed shape)
behaves as intended, where the prior `^\+\+\+` anchor silently
never fired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(deny): ignore RUSTSEC-2026-0104 (rustls-webpki CRL panic)

Same transitive pin as 0049/0098/0099 — rustls-webpki 0.102.8 is
held by libsql 0.6.0 → rustls 0.22 → hyper-rustls 0.25. The
advisory explicitly notes that applications not parsing CRLs are
unaffected; we do not parse CRLs.

[skip-regression-check] — deny.toml-only config change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(safety): portable grep boundary + broadened broadcast_for_user match

Two PROJECTION bypass paths flagged in review:

1. `\b` is a GNU-grep extension (works in grep 3.x, not portable to BSD
   grep on macOS dev envs) — replace with `(^|[^[:alnum:]_])sse\.` so
   the check fires uniformly across `grep -E` implementations.

2. `broadcast_for_user(...)` on a non-`sse` receiver (e.g.
   `manager.broadcast_for_user(...)`) previously slipped through. The
   method is defined only on `SseManager`
   (`src/channels/web/platform/sse.rs:144`), so matching
   `\.broadcast_for_user\(` on any receiver is safe and makes the
   enforcement match the documented rule.

Regression tests extended: chained-receiver, non-`sse` receiver, bare
`sse.broadcast(`, and a portable-boundary negative case (identifier
ending in `sse`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(gateway-events): align matcher description with broadened check

Update the enforcement section to describe the two current PROJECTION
matcher shapes after the review follow-up in the preceding commit:

1. Any-receiver `.broadcast_for_user(...)` — catches the non-`sse`
   receiver bypass and rustfmt wraps alike.
2. `<word-boundary>sse.broadcast(...)` with a portable boundary
   (`(^|[^[:alnum:]_])`), which is needed because `grep -E`'s `\b`
   is a GNU extension and not available on BSD grep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(safety): tighten CREDNAME + projection-exempt lints, sync header

Three follow-ups from the review:

1. CREDNAME portability — `\bCredentialName\b` used GNU-grep `\b`,
   which BSD grep does not recognise. Replace with the same
   `(^|[^[:alnum:]_])…([^[:alnum:]_]|$)` boundary used for
   PROJECTION and matches cleanly across GNU and BSD `grep -E`.

2. Empty-detail suppression bypass — `// projection-exempt: [^,]+,`
   accepted `// projection-exempt: foo,` (empty detail) as exempt
   even though `.claude/rules/gateway-events.md` requires a
   non-empty detail. Tighten to `[^,]+,[[:space:]]*[^[:space:]]`
   so a comma without a trailing token still fires the check.

3. Header suppression hint (`#24`) said
   `// projection-exempt: <reason>` — update to
   `<category>, <detail>` to match what the check actually accepts
   so contributors don't copy an unsupported format.

Regression tests extended: `PROJECTION: empty detail after comma
still flagged`, `PROJECTION: comma + whitespace-only detail still
flagged`, `CREDNAME: CredentialNameExt (different type) is not
flagged`. All 16 cases pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(bridge): project 7 more engine events to AppEvents

Second slice of #2654 bridge-coverage under #2792 Phase 1. Builds on
#2797 (StepFailed, ChildCompleted, CodeExecutionFailed) by closing the
remaining cheap `EventKind` drops:

| `EventKind` | `AppEvent` |
|---|---|
| `LeaseGranted { lease_id, capability_name }` | new `LeaseGranted` |
| `LeaseRevoked { lease_id, reason }` | new `LeaseRevoked` |
| `LeaseExpired { lease_id }` | new `LeaseExpired` |
| `SelfImprovementStarted` | new `SelfImprovement { phase: Started, .. }` |
| `SelfImprovementComplete { prompt_updated, patterns_added }` | `SelfImprovement { phase: Complete, prompt_updated, patterns_added, .. }` |
| `SelfImprovementFailed { error }` | `SelfImprovement { phase: Failed, error, .. }` |
| `OrchestratorRollback { from, to, reason }` | new `OrchestratorRollback` |

The three engine `SelfImprovement*` variants collapse into one wire
event with a `SelfImprovementPhase` discriminator — consumers need one
handler, variant-specific data is conveyed via optional phase-scoped
fields. Following the types.md "Wire-stable enums" pattern — the phase
enum is snake_case serde, not a stringly-typed `status` field.

The lease events are security-visible: capability grants, revocations,
and expiries should be auditable on the UI stream. `LeaseExpired` in
particular closes a "tools start failing after TTL with no visible
reason" gap.

Also:

- Adds `impl fmt::Display for LeaseId` in the engine alongside the
  existing `ThreadId` / `ProjectId` impls. The bridge code stringifies
  `LeaseId` for the wire; missing `Display` blocked the first compile.
- Cleans up a stray doc-comment misplacement from #2797 where the
  `thread_event_to_app_events` docstring was attached to
  `code_execution_category_to_wire`.

Regression tests mirror the #2797 pattern — one per representative arm
(lease grant for the lease family, self-improvement complete for the
richest phase, orchestrator rollback). The two remaining lease
variants and two remaining self-improvement phases are covered by the
existing `event_type_matches_serde_type_field` drift-catch test.

Approval pair (`EventKind::ApprovalRequested` / `ApprovalReceived`) is
still deferred — they need to land together with the gate-manager
migration in Phase 1 PR 3 to avoid duplicate-emit with the direct
`GateRequired` / `GateResolved` broadcasts.

Refs: #2792, #2654

* refactor(bridge): typed SelfImprovementPhase + exhaustive match

Addresses two Gemini review comments on #2844.

**1. `SelfImprovementPhase` as a typed internally-tagged enum.**

Previously the `AppEvent::SelfImprovement` variant carried three
`Option<T>` fields (`prompt_updated`, `patterns_added`, `error`), only
some of which were populated per phase. Per `.claude/rules/types.md` —
and the reviewer's note — this is an `Option`-that-can-lie pattern the
type system should rule out. Phase-specific data now lives on the
variant:

```rust
enum SelfImprovementPhase {
    Started,
    Complete { prompt_updated: bool, patterns_added: usize },
    Failed { error: String },
}
```

Wire shape is preserved via `#[serde(tag = "phase")]` on the enum and
`#[serde(flatten)]` on the `AppEvent::SelfImprovement.phase` field —
JSON still looks like a flat object:
`{"type": "self_improvement", "phase": "complete", "prompt_updated": true, ...}`.

**2. Exhaustive `thread_event_to_app_events` match.**

Dropped the `_ => vec![]` wildcard in favour of explicit arms for
every `EventKind` variant. Deferred-bridge variants get `vec![]` with
a comment naming the migration plan:

- `ApprovalRequested` / `ApprovalReceived` → waiting on the gate
  manager migration in #2792 Phase 1 PR 3 to avoid duplicate-emit with
  the existing direct `GateRequired` / `GateResolved` broadcasts.
- `Unknown` → forward-compat catch-all in the engine enum; nothing
  useful to project from a variant written by a newer binary during a
  rolling deploy.

New engine variants now fail the bridge to compile, which is exactly
what the state-convergence epic (#2792) needs — no more silent drops.

Refs: #2792, #2844 review

* fix(bridge): sanitize OrchestratorRollback.reason before SSE projection

`EventKind::OrchestratorRollback.reason` originates from
`format!("execution failed: {e}")` in
`crates/ironclaw_engine/src/executor/loop_engine.rs:327`, where
`e: EngineError`. Variants like `Store { reason }` and
`Llm { reason }` render DB connection strings, file paths, and raw
upstream HTTP bodies — all of which reached every authenticated SSE
consumer verbatim through the new `AppEvent::OrchestratorRollback`
projection.

Route the reason through a new `user_facing_rollback_reason`
classifier that maps the existing `FailureCategory` taxonomy to
short operator-facing messages (`"LLM provider unavailable"`,
`"execution failed"`, etc.). The raw text still lives in the
`debug!` log for operator triage, matching the pattern already
used for `AppEvent::Error`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(deny): ignore RUSTSEC-2026-0104 (rustls-webpki CRL panic)

Same transitive pin as 0049/0098/0099 — rustls-webpki 0.102.8 is
held by libsql 0.6.0 → rustls 0.22 → hyper-rustls 0.25. The
advisory explicitly notes that applications not parsing CRLs are
unaffected; we do not parse CRLs.

[skip-regression-check] — deny.toml-only config change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(bridge): cover 4 new engine→AppEvent arms; sharpen rollback test

Review follow-ups on PR #2844:

- Add unit tests for `LeaseRevoked`, `LeaseExpired`,
  `SelfImprovementStarted`, and `SelfImprovementFailed` bridge arms
  — only `LeaseGranted` / `SelfImprovementComplete` /
  `OrchestratorRollback` had coverage before, leaving four new
  projections untested.
- Rewrite `rollback_reason_drops_engine_error_detail` to drive the
  sanitiser with two unrelated leaky inputs and assert identical
  outputs (`execution failed`). The load-bearing check is
  input-independence; `!contains` probes remain as sentinel sniffs
  for the specific leak shapes. Avoids classifier-triggering tokens
  (no `upstream`, no `http 5xx`) so both inputs fall through to
  `Unknown`.
- Reword the `ApprovalRequested` / `ApprovalReceived` comment: they
  are temporarily suppressed pending the gate-manager migration, not
  permanently dropped. The bridge will eventually map them here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added scope: docs Documentation size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Apr 22, 2026
@claude

claude Bot commented Apr 22, 2026

Copy link
Copy Markdown

Code review

Found 3 issues:

  1. [MEDIUM:75] Pre-commit safety script test coverage not confirmed in CI pipeline

The PR adds comprehensive regression tests for the new PROJECTION check in scripts/test-pre-commit-safety.sh, but it's unclear if this test script is run as a required CI check. Without CI integration, violations of the PROJECTION rule could silently pass.

https://github.com/anthropics/ironclaw/blob/c559a58810896093a5cf768b424906a6f94aad7b/scripts/test-pre-commit-safety.sh#L418-L588

Recommendation: Verify that scripts/test-pre-commit-safety.sh is invoked in .github/workflows/*.yml or .husky/pre-commit as a blocking check on PRs that touch scripts/pre-commit-safety.sh.

  1. [LOW:50] LeaseId Display impl contradicts newtype philosophy

The new Display impl for LeaseId in crates/ironclaw_engine/src/types/error.rs enables implicit formatting via format!() instead of requiring explicit .as_str() calls. Per .claude/rules/types.md, newtypes should use explicit conversions to prevent accidental implicit coercions.

https://github.com/anthropics/ironclaw/blob/c559a58810896093a5cf768b424906a6f94aad7b/crates/ironclaw_engine/src/types/error.rs#L224-L230

Recommendation: Either remove the Display impl (preferred) or add a comment explaining why this impl doesn't violate the rule's spirit.

  1. [LOW:75] Pre-commit safety script regex edge case with colon boundary

The fix to make the diff-header filter portable (:+++ instead of ^\+\+\+) assumes grep -n output format. A source file containing the exact sequence :+++ in a string or comment could be a false negative. This is unlikely in practice but not zero-risk.

https://github.com/anthropics/ironclaw/blob/c559a58810896093a5cf768b424906a6f94aad7b/scripts/pre-commit-safety.sh#L345-L347

Recommendation: Use an explicit pattern like grep -n ... | grep -E ':[[:space:]]*\+\+\+' to anchor after the line-number prefix unambiguously.


Positive findings:

  • Event bridging and type design are exemplary (SelfImprovementPhase enum avoids impossible states)
  • Security-aware error sanitization with comprehensive leak-detection tests
  • No panics, unwraps, or blocking operations in async code
  • All CLAUDE.md patterns and safety rules properly followed

Base automatically changed from staging-promote/41c73878-24785020894 to main April 29, 2026 04:09
@henrypark133
henrypark133 merged commit c559a58 into main Apr 29, 2026
127 of 178 checks passed
@henrypark133
henrypark133 deleted the staging-promote/c559a588-24788480958 branch April 29, 2026 04:09

This branch had an error being deployed

1 failed and 5 inactive deployments
venice-ironclaw / production — c559a588 Deployed Apr 22, 2026 by railway-app[bot]
cosmose-ironclaw / production — c559a588 Deployed Apr 22, 2026 by railway-app[bot]
Ironclaw-QA / production — c559a588 Deployed Apr 22, 2026 by railway-app[bot]
humble-cat / staging-cameron — c559a588 Deployed Apr 22, 2026 by railway-app[bot]
Near Foundation Ironclaw / production — c559a588 Deployed Apr 22, 2026 by railway-app[bot]
ironclaw-nearai / production — c559a588 Deployed Apr 22, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: docs Documentation size: XL 500+ changed lines staging-promotion

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants