Skip to content

feat(gateway): phase A design-system tokens (Defuse OmniSwap palette) - #2689

Closed
ilblackdragon wants to merge 2 commits into
mainfrom
feat/phase-a-tokens-defuse
Closed

ilblackdragon wants to merge 2 commits into
mainfrom
feat/phase-a-tokens-defuse

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Summary

First visible step of the design-system adoption planned in /home/illia/.claude/plans/quiet-nibbling-moth.md. Restructures theme.css with Radix Themes-style 12-step scales anchored on Defuse's OmniSwap whitelabel palette (the closest in-house Defuse palette to IronClaw's current emerald), introduces semantic role tokens, a layered shadow system, extended radii + type scale, and swaps the webfont to Geist (Geist Mono for monospace).

Every legacy token (--bg, --text, --accent, --shadow-card, …) is preserved and aliased onto the new scales — the 20 split surface stylesheets under styles/ require zero changes. Palette swaps are now a one-file edit.

What changed

  • Accent scale --accent-1..12 + --accent-a1..12 from OmniSwap osa*: mint→forest emerald. Brand anchor at --accent-9 = #73e2a5 (dark) / #0a5b38 (light). Old single --accent: #34d399 is now an alias onto --accent-9.
  • Neutral scale --sand-1..12 + --sand-a1..12 from OmniSwap os*: green-tinged warm grays.
  • Semantic role aliases: --color-{bg,surface,surface-elevated,surface-hover,border,border-hover,text,text-muted,text-subtle,label,warning,warning-foreground} + --accent-contrast.
  • Layered shadows: --shadow-{paper,widget,card,inset} replace a single --shadow.
  • Radius: adds --radius-{xl:16,2xl:20,4xl:30}.
  • Type scale: adds --text-{4xl:32,5xl:48,6xl:64} + display tracking.
  • Typography: --font-sans: 'Geist', --font-mono: 'Geist Mono' via Google Fonts (CSP already allows fonts.googleapis.com / fonts.gstatic.com).

Admin panel falls back to the system font chain — admin's CSP blocks the Google Fonts origin on purpose (admin is strictly same-origin).

Visual diff

Captured with tests/e2e/scenarios/capture_design_surfaces.py (opt-in Playwright scenario added in this PR, viewport 1280×800, Chromium). Full gallery at docs/design-system/phase-a/.

Dark mode

Surface Before After
Chat before after
Workspace before after
Settings before after
Jobs before after
Auth before after

Light mode

Surface Before After
Chat before after
Settings before after
Auth before after

Test plan

  • cargo fmt --check — clean
  • cargo clippy --all --benches --tests --examples --all-features — zero warnings
  • cargo test --lib 'channels::web' — 431 pass
  • bash scripts/pre-commit-safety.sh — pass
  • Baseline + after Playwright captures succeed (see docs/design-system/phase-a/)
  • Reviewer eyeballs before/after screenshots and flags any surface that regressed

Non-goals

  • Does not touch the 20 split surface stylesheets under static/styles/ — those consume legacy tokens, which now alias onto the new scales.
  • Does not introduce primitives (Dialog, Popover, etc.) — that's Phase B.
  • Does not restyle individual components to match the new radii/shadows — that's Phase C, rolled out per surface.

🤖 Generated with Claude Code

First visible step of the design-system adoption tracked in the
quiet-nibbling-moth plan. Restructures `theme.css` with Radix
Themes-style 12-step scales anchored on Defuse's OmniSwap whitelabel
palette (the closest in-house palette to IronClaw's current emerald),
introduces semantic role tokens, a layered shadow system, extended
radii and type scale, and swaps the webfont from DM Sans to Geist
(Geist Mono for monospace).

- Accent scale (--accent-1..12 + --accent-a1..12) from OmniSwap osa*:
  mint→forest emerald, brand anchor at --accent-9 (#73e2a5 dark /
  #0a5b38 light).
- Neutral scale (--sand-1..12 + --sand-a1..12) from OmniSwap os*:
  green-tinged warm grays.
- Semantic role aliases: --color-{bg,surface,surface-elevated,
  surface-hover,border,border-hover,text,text-muted,text-subtle,
  label,warning,warning-foreground}, plus --accent-contrast.
- Layered shadows: --shadow-{paper,widget,card,inset} replace a
  single --shadow.
- Radius scale extended with --radius-{xl:16,2xl:20,4xl:30}.
- Type scale extended with --text-{4xl:32,5xl:48,6xl:64} + tracking.
- Typography: --font-sans: 'Geist', --font-mono: 'Geist Mono'
  loaded via Google Fonts (CSP already allows fonts.googleapis.com /
  fonts.gstatic.com).

Every legacy token (--bg, --text, --accent, --shadow-card, …) is
preserved and aliased onto the new scales so the 20 split surface
stylesheets under `styles/` need zero changes — swapping palettes
happens entirely in theme.css.

Includes:
- tests/e2e/scenarios/capture_design_surfaces.py — opt-in Playwright
  scenario (CAPTURE_SCREENSHOTS=1) that captures every main tab in
  both themes for design-system PR review. Safe to leave in the suite;
  default pytest runs skip it.
- docs/design-system/phase-a/{before,after}/*.png — 1280×800
  Chromium screenshots captured pre- and post-change.
- docs/design-system/phase-a/README.md — side-by-side table and
  reproduction instructions.

Admin panel falls back to the system font chain (its CSP blocks the
Google Fonts origin, which is intentional — admin stays same-origin
only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 19, 2026 15:48
@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 19, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements Phase A of a design system migration, introducing Radix-style 12-step color scales, semantic role tokens, and a layered shadow system. It also updates the typography to the Geist font family and adds an E2E utility for UI screenshot capture. Feedback highlights the need to override semantic warning tokens for light mode, use scale variables instead of hardcoded hex values for consistency, and improve exception handling in the test suite.

Comment on lines +341 to +350
/* Light-mode overrides for the few legacy tokens that aren't a
* pure scale-step reference. */
--bg-overlay: rgba(0, 0, 0, 0.30);
--bg-modal: var(--sand-1);
--border-modal: var(--sand-a6);
--text-secondary: var(--sand-8);
--text-tertiary: var(--sand-10);
--text-muted: var(--sand-7);
--text-dimmed: var(--sand-6);
--text-on-accent: #ffffff;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new semantic role tokens --color-warning and --color-warning-foreground (defined in :root at lines 122-123) use hardcoded colors specific to the dark theme. They should be overridden here in the light theme block to use the appropriate light-mode warning colors. Additionally, --text-on-accent should ideally reference --accent-contrast for consistency.

  /* Light-mode overrides for semantic roles and legacy tokens. */
  --color-warning:            var(--warning-soft);
  --color-warning-foreground: var(--warning-text);

  --bg-overlay:       rgba(0, 0, 0, 0.30);
  --bg-modal:         var(--sand-1);
  --border-modal:     var(--sand-a6);
  --text-secondary:   var(--sand-8);
  --text-tertiary:    var(--sand-10);
  --text-muted:       var(--sand-7);
  --text-dimmed:      var(--sand-6);
  --text-on-accent:   var(--accent-contrast);


/* Contrast text-on-solid-accent — used for button foregrounds when the
* background is `--accent-9`. Defuse sets this to the inverted step 1. */
--accent-contrast: #031c12;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of hardcoding the hex value, consider referencing --accent-1 which contains the same value. This ensures consistency if the scale is adjusted in the future.

  --accent-contrast: var(--accent-1);

--accent-a11: rgba(4, 42, 27, 0.920);
--accent-a12: rgba(3, 28, 18, 0.970);

--accent-contrast: #edfcf3;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider using var(--accent-1) here to maintain consistency with the scale, matching the pattern used in the dark mode definition.

  --accent-contrast: var(--accent-1);

btn = SEL["tab_button"].format(tab=tab)
try:
await page.locator(btn).click(timeout=3000)
except Exception:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a broad Exception can mask legitimate failures (e.g., selector syntax errors or unexpected browser states). For Playwright operations like .click() or .wait_for_selector() (as seen on line 89), it is better to catch the specific TimeoutError from playwright.async_api to ensure that only expected timeouts are suppressed while other errors remain visible.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces Phase A of the gateway design-system adoption by rebuilding theme.css around Radix-style 12-step color scales (Defuse OmniSwap palette), adding semantic role tokens, and adding an opt-in Playwright scenario + screenshot gallery for visual diffs.

Changes:

  • Reworks gateway theme tokens into neutral/accent 12-step scales with semantic aliases and expanded shadows/radii/type scale.
  • Switches the main gateway Google Fonts link to Geist / Geist Mono.
  • Adds an opt-in Playwright screenshot capture scenario and publishes before/after screenshot artifacts + documentation.

Reviewed changes

Copilot reviewed 4 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/e2e/scenarios/capture_design_surfaces.py Adds opt-in Playwright scenario to capture screenshots across major UI surfaces and auth screen.
crates/ironclaw_gateway/static/theme.css Introduces 12-step sand/accent scales, semantic role tokens, layered shadows, and legacy token aliases.
crates/ironclaw_gateway/static/index.html Updates Google Fonts to load Geist / Geist Mono.
docs/design-system/phase-a/README.md Documents Phase A token changes and links screenshot gallery + reproduction steps.
docs/design-system/phase-a/before/light-auth.png “Before” light auth screenshot asset.
docs/design-system/phase-a/before/dark-routines.png “Before” dark routines screenshot asset.
docs/design-system/phase-a/before/dark-missions.png “Before” dark missions screenshot asset.
docs/design-system/phase-a/before/dark-auth.png “Before” dark auth screenshot asset.
docs/design-system/phase-a/after/light-auth.png “After” light auth screenshot asset.
docs/design-system/phase-a/after/dark-auth.png “After” dark auth screenshot asset.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

CAPTURE_SCREENSHOTS=1 SCREENSHOT_DIR=/tmp/after \
pytest tests/e2e/scenarios/capture_design_surfaces.py

Outputs two sets of 1280×800 PNGs per tab + theme; reference them in

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims screenshots are 1280×800, but this test uses the shared page fixture which creates a 1280×720 viewport (tests/e2e/conftest.py:1117). Either set the viewport to 1280×800 in this scenario (e.g., create a new context like test_capture_auth_screen or call set_viewport_size) or update the documentation to match the actual output size.

Suggested change
Outputs two sets of 1280×800 PNGs per tab + theme; reference them in
Outputs two sets of 1280×720 PNGs per tab + theme; reference them in

Copilot uses AI. Check for mistakes.
* =========================================================== */
--font-sans: 'Geist', -apple-system, BlinkMacSystemFont, 'DM Sans', 'Segoe UI', sans-serif;
--font-mono: 'Geist Mono', 'IBM Plex Mono', 'SF Mono', 'Fira Code', Consolas, monospace;

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--font-sans is introduced (and index.html now loads Geist), but the main UI base styles still hardcode font-family: 'DM Sans' ... (e.g., static/styles/base.css). As-is, Geist likely won’t be used for the primary UI text. Consider switching the base body font-family to var(--font-sans) (or adding a global rule here) so the font token actually takes effect.

Suggested change
/* Apply the theme font token globally so the primary UI actually
* uses the configured sans stack. */
body {
font-family: var(--font-sans);
}

Copilot uses AI. Check for mistakes.
Comment on lines +279 to +283
/* ==============================================================
* Light theme — the OmniSwap scales are literally reversed
* between modes, so the role tokens stay defined in :root above
* and we redefine only the scales + a couple of alpha overlays.
* ============================================================== */

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new layered shadow tokens (--shadow-paper, --shadow-widget, --shadow-card, --shadow-inset) are only defined in :root and aren’t overridden in [data-theme="light"]. That leaves light mode using the dark-mode shadow recipe (notably the rgba(0,0,0,0.40) parts), which is likely too heavy and also defeats the intent of palette/theme-specific tokens. Define light-mode values for these --shadow-* tokens inside the light theme block.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +8
First visible step of the design-system adoption tracked in
`/home/illia/.claude/plans/quiet-nibbling-moth.md`. Restructures
`theme.css` with Radix Themes-style 12-step scales anchored on
Defuse's OmniSwap whitelabel palette, introduces semantic role tokens,
a layered shadow system, extended radii + type scale, and swaps the
webfont from DM Sans → Geist (plus Geist Mono for monospace).

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc references a local absolute path (/home/illia/.claude/plans/quiet-nibbling-moth.md) that won’t exist for other contributors or in CI. Prefer a repo-relative link (e.g., docs/...) or remove the path and describe the plan location generically.

Suggested change
First visible step of the design-system adoption tracked in
`/home/illia/.claude/plans/quiet-nibbling-moth.md`. Restructures
`theme.css` with Radix Themes-style 12-step scales anchored on
Defuse's OmniSwap whitelabel palette, introduces semantic role tokens,
a layered shadow system, extended radii + type scale, and swaps the
webfont from DM Sans → Geist (plus Geist Mono for monospace).
First visible step of the design-system adoption tracked in the
implementation plan. Restructures `theme.css` with Radix Themes-style
12-step scales anchored on Defuse's OmniSwap whitelabel palette,
introduces semantic role tokens, a layered shadow system, extended
radii + type scale, and swaps the webfont from DM Sans → Geist (plus
Geist Mono for monospace).

Copilot uses AI. Check for mistakes.
@serrrfirat

Copy link
Copy Markdown
Collaborator

I think there is a problem with the dark mode? background is white

…k mode

theme.css had a legacy-tokens comment block containing the glob pattern
`styles/**/*.css`. Chrome's CSS parser matches `*/` inside the `**/` +
`*` + `.` sequence, closing the comment prematurely. Error recovery
then swallowed the very next declaration — `--bg: var(--color-bg);` —
which left body background resolving to `transparent` and every surface
falling through to the browser's default white.

Side effect: the initial Phase A screenshots in
`docs/design-system/phase-a/after/` showed the broken state, which
looked like a palette issue but was actually a parser issue.

Fix: rewrite the comment body to drop the glob, add a warning note so
the pattern doesn't regress, and re-capture the "after" screenshots.

Verified: `getComputedStyle(documentElement).getPropertyValue('--bg')`
now returns `#080c0a` in dark mode and `#fcfdfc` in light mode;
`document.body.style.backgroundColor` follows through. Every surface
now renders on the intended dark background.

Regression guard added in the comment itself — CI would catch a
recurrence via the updated screenshots on the next design-system PR.

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

Copy link
Copy Markdown
Member Author

Dark mode fix pushed as 269cc66. The initial "after" screenshots I attached showed a white page background, which was actually a CSS parser bug, not a palette regression:

  • The new legacy-tokens comment block in theme.css contained the glob styles/**/*.css.
  • Chrome's CSS parser matches */ inside that pattern (the ** → / → * sequence closes the comment early).
  • Error recovery swallowed the very next declaration — --bg: var(--color-bg);.
  • --bg resolved to empty, body { background: var(--bg); } fell through to transparent, and every surface showed the browser's default white.

The fix rewrites the comment to avoid the glob pattern and adds a warning note so it doesn't regress. Re-captured screenshots replace the "after" set in docs/design-system/phase-a/after/ — the side-by-side table in the PR body now shows actual dark backgrounds.

Also tested: getComputedStyle(documentElement).getPropertyValue('--bg') → #080c0a (dark) / #fcfdfc (light), and every dark-mode surface renders on the intended background.

ilblackdragon added a commit that referenced this pull request Apr 20, 2026
Synthesizes recurring patterns from ~30 merged PRs, 147 bot review
comments (Copilot/Gemini), human reviews, and ~50 issues filed in the
past 2 weeks. Each rule cites the motivating PR/issue numbers.

New files:
- error-handling.md — silent-failure taxonomy (unwrap_or_default, .ok()?,
  poisoned caches), persist-then-reload atomicity, channel-edge error
  mapping. (#2526, #2633, #2653, #2673, #2546, #2407, #2408)
- agent-evidence.md — side-effect claims must cite tool evidence,
  empty-fast outputs are errors, external-effect tools must read back,
  setup UI round-trip. (#2544, #2580, #2582, #2541, #2545, #2411, #2543,
  #2586)
- lifecycle.md — discovery vs. activation, terminal auth rejection,
  list_installed vs. list_active, deactivation unwinds, snapshot
  rehydrate must re-validate. (#2556, #2557, #2558, #2564, #2419,
  PR #2617, PR #2631)

Extended:
- types.md — from_trusted boundary rule, validated-newtype template with
  shared validate(&str), serde(try_from) required for validated types,
  wire-stable enums (no Debug; serde alias for migrations), canonical
  wire-contract field naming. (PR #2685, #2681, #2687, #2678, #2669,
  #2665, #2683, #2702)
- safety-and-sandbox.md — every new ingress scans pre-transform/pre-
  injection, bounded resources (interners/streams/fan-out caps), cache
  keys must be complete. (#2491, #2676, #2470, #2633, #2673, #2710,
  PR #2702)
- review-discipline.md — PR scope discipline, guardrail scripts are
  code (regression tests, grouped-import parsing, CI has_code inclusion),
  absolute-path ban in committed docs, stale comments after refactors.
  (PR #2668, #2628, #2680, #2687, #2647, #2689, #2701)

All new files carry paths: frontmatter so they auto-load only on
matching files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* / admin.html — CSP already allows `fonts.googleapis.com` and
* `fonts.gstatic.com`.
* =========================================================== */
--font-sans: 'Geist', -apple-system, BlinkMacSystemFont, 'DM Sans', 'Segoe UI', sans-serif;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High Severity
Typography swap is only partially wired, so the gateway will mostly fall back to system fonts instead of actually using Geist.

This PR changes index.html to load Geist / Geist Mono and defines --font-sans / --font-mono here, but the main gateway CSS still hardcodes the old families in the existing consumers:

  • crates/ironclaw_gateway/static/styles/base.css keeps body { font-family: 'DM Sans', ... }
  • crates/ironclaw_gateway/static/styles/surfaces/settings.css still hardcodes 'IBM Plex Mono' in several controls

Because the old Google Fonts import was removed, those surfaces now fall back to system UI / generic monospace instead of using the new design-system fonts. That means the advertised typography change does not actually land across the app, and some surfaces visibly regress.

Suggested fix: update the gateway CSS consumers to use the new tokens (font-family: var(--font-sans) / var(--font-mono)) before switching the imported font families.

("memory", "#memory-tab-container, #memory-tree, .empty-state"),
("jobs", ".jobs-container, .empty-state"),
("missions", ".missions-container, .empty-state"),
("routines", ".routines-container, .empty-state"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity
The screenshot harness is claiming to capture both missions and routines, but with engine v2 forced on it can silently capture missions twice.

This test flips engineV2Enabled = true before the tab walk, but still includes ("routines", ...) in DARK_SURFACES. In v2 mode the router normalizes #/routines to missions, and _capture_tab() then waits on selectors that include the generic .empty-state, so the screenshot step still succeeds even if it never reached a real routines view.

The committed evidence already shows this: the before/dark-missions.png and before/dark-routines.png files are byte-identical, and the same is true for the after/ pair. So the README currently overstates coverage and this harness would miss a routines-only regression.

Suggested fix: either capture routines only with v1 mode enabled, or assert the active tab/panel before taking the screenshot so a misrouted capture fails instead of silently producing a duplicate PNG.

ilblackdragon added a commit that referenced this pull request Apr 20, 2026
* docs(rules): add review-driven guidance for Claude Code

Synthesizes recurring patterns from ~30 merged PRs, 147 bot review
comments (Copilot/Gemini), human reviews, and ~50 issues filed in the
past 2 weeks. Each rule cites the motivating PR/issue numbers.

New files:
- error-handling.md — silent-failure taxonomy (unwrap_or_default, .ok()?,
  poisoned caches), persist-then-reload atomicity, channel-edge error
  mapping. (#2526, #2633, #2653, #2673, #2546, #2407, #2408)
- agent-evidence.md — side-effect claims must cite tool evidence,
  empty-fast outputs are errors, external-effect tools must read back,
  setup UI round-trip. (#2544, #2580, #2582, #2541, #2545, #2411, #2543,
  #2586)
- lifecycle.md — discovery vs. activation, terminal auth rejection,
  list_installed vs. list_active, deactivation unwinds, snapshot
  rehydrate must re-validate. (#2556, #2557, #2558, #2564, #2419,
  PR #2617, PR #2631)

Extended:
- types.md — from_trusted boundary rule, validated-newtype template with
  shared validate(&str), serde(try_from) required for validated types,
  wire-stable enums (no Debug; serde alias for migrations), canonical
  wire-contract field naming. (PR #2685, #2681, #2687, #2678, #2669,
  #2665, #2683, #2702)
- safety-and-sandbox.md — every new ingress scans pre-transform/pre-
  injection, bounded resources (interners/streams/fan-out caps), cache
  keys must be complete. (#2491, #2676, #2470, #2633, #2673, #2710,
  PR #2702)
- review-discipline.md — PR scope discipline, guardrail scripts are
  code (regression tests, grouped-import parsing, CI has_code inclusion),
  absolute-path ban in committed docs, stale comments after refactors.
  (PR #2668, #2628, #2680, #2687, #2647, #2689, #2701)

All new files carry paths: frontmatter so they auto-load only on
matching files.

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

* refactor(rules): split agent-evidence into prompt + code rule

agent-evidence.md mixed two concerns: runtime agent instruction (what
the LLM should do when concluding a turn) and code-enforcement rules
(what the dispatcher, engine, and tools must implement). Rules under
.claude/rules/ only guide Claude Code when editing the repo — the
runtime agent never reads them.

Splits the two:

- crates/ironclaw_engine/prompts/codeact_postamble.md — new section
  "Evidence before claiming side effects". Sits next to the existing
  "FINAL() answer quality" guidance; loaded via include_str! in
  executor/prompt.rs (no Rust change needed).
- .claude/rules/tool-evidence.md — renamed from agent-evidence.md,
  keeps only the code invariants (engine v2 side-effect gate,
  empty-fast ToolError::EmptyResult, external-effect tools must read
  back, setup UI round-trip).

Prompt tests pass unchanged; the postamble addition is pure text.

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

* prompt: tighten evidence rule to FINAL() claims only, not tool use

Live-test validation of the "Evidence before claiming side effects"
section (added in the prior commit) showed it inhibited legitimate
tool use. With the original wording, `zizmor_scan_v2` live-recording
timed out at 302s with zero responses; reverting the postamble
restored healthy behavior (88s run, 8 shell calls including
`cargo install zizmor` and full workflow analysis).

The original phrasing conflated two things: what the agent should
claim and what tools it should call. The rule is only about the
claim. Re-tunes the section to:

- Open with an explicit "this does not restrict tool calls" scope.
- Drop the "<1ms = failure" heuristic (too broad — normal tools like
  `tool_info(schema)` are legitimately fast).
- Drop the full enumeration of forbidden side-effect verbs; keep the
  rule narrower and clearer.
- Shorten the code example (remove redundant early-return).

Re-tuned run: agent is active (shell calls, real reasoning), live
recording completes in ~9s. The remaining test failure is a
pre-existing assertion bug (exact `t == "shell"` match against tool
strings that now carry arguments like `"shell(cmd)"`) — reproduces
with the old postamble too.

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

* test(live): fix tool-name assertions + re-record zizmor traces

The two `zizmor_scan*` live tests had four broken tool-name assertions
that silently failed to match: `tools.iter().any(|t| t == "shell")`
against a tool list that now contains `"shell(cmd)"` strings (tool
events carry args via `format_action_display_name` in
`src/bridge/router.rs`). Two of the four were negative assertions
checking for the absence of `tool_install` recovery loops — those
silently passed even when a recovery loop actually ran. `sandbox_live_e2e.rs:203`
already used the correct `t == "shell" || t.starts_with("shell(")`
pattern; applied it consistently to all four sites.

Verified live:

- `IRONCLAW_LIVE_TEST=1 cargo test --test e2e_live -- zizmor_scan --ignored --test-threads=1`
  → 2 passed, 0 failed, 51.78s. Agent installs and runs zizmor
  end-to-end, producing real findings (exit code 14, dangerous
  triggers, excessive permissions, etc.).

Traces re-recorded with the tuned postamble (commit 50d8517) and
scrubbed: replaced `/home/illia/.cargo/bin/zizmor` with
`/home/user/.cargo/bin/zizmor` per the developer-local-path ban in
`.claude/rules/review-discipline.md`. No credentials, PII, or
high-entropy secrets in either trace (only git SHAs from zizmor's
workflow analysis output).

Replay still passes: `cargo test --test e2e_live -- zizmor_scan --ignored`
→ 2/2 ok.

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

* test(replay): update zizmor_scan_v2 insta snapshot

The engine v2 replay-snapshot gate (`engine_v2_tests::snapshot_zizmor_scan_v2`)
failed against the re-recorded trace from 1691efe because the old
snapshot encoded a broken run:

- final_state: Failed
- Missing Assistant message role
- 3 issues: thread_failure (error), no_response (warning), llm_error (error)
- 6 tool calls that never produced a final answer

The new trace completes cleanly:

- final_state: Done
- System / User / Assistant roles present
- 1 issue: mixed_mode (info)
- 3 shell tool calls + successful `FINAL()` with real findings

The snapshot was pinning a regression. Regenerated with
`INSTA_UPDATE=always cargo test --test e2e_engine_v2 -- snapshot_zizmor_scan_v2`;
passes on replay.

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

* docs(rules): address PR #2714 review feedback

- review-discipline: reword "Doc Absolute Paths" as a review convention
  (pre-commit only scans .rs; the rule misleadingly claimed enforcement).
- safety-and-sandbox: broaden `paths:` frontmatter to include the actual
  ingress owners (`bridge`, `channels`, `workspace`, `agent`, engine
  crate) so the rule auto-loads where it applies.
- tool-evidence: mark the side-effect gate, empty-fast rule, and
  `unverified` flag as target/aspirational invariants — neither
  `ToolError::EmptyResult`, an `unverified` field on `ToolOutput`, nor a
  byte-count field on `ActionRecord` exist today. Point at concrete
  interim conventions (`ToolError::ExecutionFailed`, `unverified: true`
  in the JSON result body).
- types: scope "Validated newtypes must gate Deserialize" to *new*
  types, document the `CredentialName`/`ExtensionName` exception (they
  intentionally use `#[serde(transparent)]` + derived `Deserialize`
  under the `serde_does_not_revalidate` test). Clarify the
  `from_trusted` trust boundary (trusted = typed upstream, untrusted =
  raw JSON field even if the field *name* is "registry entry").
  Switch `new` template to `impl Into<String>` to avoid an unnecessary
  clone when an owned `String` is passed.

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

* docs(rules): simplify types.md + split doc-hygiene; address review round 2

- types: collapse two templates into one canonical validated-newtype
  shape. New types use `#[serde(try_from = "String")]` with a shared
  `validate(&str)` helper — no more dual "transparent for some /
  try_from for others" guidance. `CredentialName`/`ExtensionName` are
  documented as the sole legacy exception (locked in by the
  `serde_does_not_revalidate` test); new code must not copy their
  `transparent` + `from_trusted` pattern. Removes the long "Using
  `from_trusted` safely" section and the separate "Validated newtypes
  must gate Deserialize" subsection that contradicted the Don'ts list.
- doc-hygiene: new tiny rule file scoped to `**/*.md`, `**/*.py`,
  `docs/**` that carries the "no developer-local absolute paths in
  committed docs" convention. Removed from review-discipline.md where
  its `src/**/*.rs` scope meant the rule never loaded on the files it
  governed.

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

* test(live): match hyphenated tool-install in attempted_relevant_tool

The engine records `action_name` as the raw string the LLM emitted
(`crates/ironclaw_engine/src/executor/structured.rs:381`), and the
registry's lookup canonicalization only affects dispatch — not the
name that reaches `StatusUpdate::ToolStarted`. The two other predicates
in this file (`bad_recovery` at :420, `phase_b_recovery` at :531)
already defend against both forms; this one should too, for
consistency. Addresses PR #2714 review.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@serrrfirat

Copy link
Copy Markdown
Collaborator

Paranoid Architect Review — Request Changes

2 Critical, 4 High, 4 Medium findings. The core design-system architecture is sound, but the token migration is incomplete and introduces several accessibility regressions and visual breakages.

Critical

  • White text on accent unreadable (1.60:1): #fff on --accent-9 (#73e2a5). Affects .badge-admin, .btn-primary, .tool-permission-toggle — uses hardcoded #fff instead of var(--text-on-accent).
  • --shadow-card light-mode override dropped: Old theme had explicit rgba(0,0,0,0.08) for light mode. New theme only defines in :root with rgba(0,0,0,0.40) — 5x heavier card shadows in light mode.

High

  • #000 on light-mode --success fails AA (2.57:1 contrast). Stepper completed steps unreadable. Old #059669 gave 5.57:1.
  • Undefined tokens consumed: --bg-primary (2 uses), --text-primary (12 uses), --radius-md (2 uses) — none defined anywhere in theme.css.
  • admin.html missing Geist font load: Only index.html updated to load Geist. Admin panel falls through to system fonts.
  • --warning-bg dark mode nearly invisible: 30x opacity reduction (#1e1400 → rgba(245,166,35,0.05)). Warning modals invisible.

Medium

  • Body font-family still hardcodes 'DM Sans' — var(--font-sans) defined but never consumed. Geist webfont loaded but unused.
  • 4 hardcoded 'IBM Plex Mono' references bypass var(--font-mono) in settings.css.
  • --text-dimmed fails WCAG AA in dark mode (2.48:1 contrast).
  • --text-muted fails WCAG AA for normal text in dark mode (4.03:1 vs required 4.5:1).

Blocking before merge

  1. Fix contrast: Replace #fff with var(--text-on-accent) in .badge-admin, .btn-primary, .tool-permission-toggle
  2. Add light-mode overrides for --shadow-card, --shadow-paper, --shadow-widget, --shadow-inset
  3. Define --bg-primary, --text-primary, --radius-md in theme.css or migrate consumers to existing tokens
  4. Fix --warning-bg opacity (restore opaque or semi-opaque value for dark mode)
  5. Wire var(--font-sans) into base.css body

@serrrfirat

Copy link
Copy Markdown
Collaborator

Paranoid Architect Review — Request Changes

2 Critical, 4 High, 4 Medium findings. The core design-system architecture (12-step Radix-style scales, semantic role aliases, legacy token preservation) is sound, but the token migration is incomplete and introduces several accessibility regressions.

Critical

  • White text on accent unreadable (1.60:1 contrast): #fff on --accent-9 (#73e2a5) in .badge-admin, .btn-primary, .tool-permission-toggle. Uses hardcoded #fff instead of var(--text-on-accent).
  • --shadow-card light-mode override dropped: Old theme had explicit rgba(0,0,0,0.08) for light mode. New theme only defines in :root with rgba(0,0,0,0.40) — 5x heavier card shadows in light mode.

High

  • #000 on light-mode --success fails AA (2.57:1). Old #059669 gave 5.57:1 — this is a regression.
  • Undefined tokens consumed: --bg-primary (2 uses), --text-primary (12 uses), --radius-md (2 uses) — none defined anywhere. Silently resolve to CSS initial values.
  • admin.html missing Geist font load — only index.html updated. Admin panel falls through to system fonts.
  • --warning-bg dark mode nearly invisible: 30x opacity reduction (#1e1400 → rgba(245,166,35,0.05)). Warning modals invisible on dark backgrounds.

Medium

  • Body font-family still hardcodes 'DM Sans' — var(--font-sans) defined but never consumed. Geist loaded but unused for sans text.
  • 4 hardcoded 'IBM Plex Mono' references in settings.css bypass var(--font-mono).
  • --text-dimmed fails WCAG AA in dark mode (2.48:1 contrast).
  • --text-muted fails WCAG AA for normal text in dark mode (4.03:1 vs required 4.5:1).

Blocking before merge

  1. Replace #fff with var(--text-on-accent) in accent-background selectors
  2. Add light-mode overrides for all shadow tokens
  3. Define --bg-primary, --text-primary, --radius-md or migrate consumers
  4. Fix --warning-bg opacity (restore opaque or semi-opaque value)
  5. Wire var(--font-sans) into base.css body

--accent-6: #0b9053;
--accent-7: #17b267;
--accent-8: #3bcc81;
--accent-9: #73e2a5; /* brand anchor (interactive solid) */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: White text on this accent is unreadable (1.60:1 contrast). --accent-9 (#73e2a5) is light pastel green. At least 3 selectors in style.css use color: #fff with background: var(--accent): .badge-admin, .btn-primary, .tool-permission-toggle button.active. WCAG AA requires 4.5:1 for normal text.

The --accent-contrast: #031c12 token exists and would give proper contrast, but these selectors use hardcoded #fff instead of var(--text-on-accent).

Fix: Replace color: #fff with color: var(--text-on-accent) in all accent-background selectors.

* =========================================================== */
--shadow-paper: 0 1px 0 var(--sand-a4), 0 8px 24px -8px rgba(0, 0, 0, 0.35);
--shadow-widget: 0 0 0 1px var(--sand-a4), 0 12px 32px -12px rgba(0, 0, 0, 0.50);
--shadow-card: 0 1px 2px var(--sand-a3), 0 4px 16px -6px rgba(0, 0, 0, 0.40);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: --shadow-card light-mode override was dropped. Old theme had [data-theme="light"] { --shadow-card: 0 4px 24px rgba(0,0,0,0.08) }. This :root definition with rgba(0,0,0,0.40) is only appropriate for dark mode — in light mode it produces 5x heavier card shadows.

Same issue for --shadow-paper, --shadow-widget, --shadow-inset defined nearby.

Fix: Add these tokens to the [data-theme="light"] block with appropriate light-mode opacities (e.g., 0.06-0.10).

--success: var(--accent-9);
--info: #60a5fa;
--warning: #F5A623;
--warning-bg: rgba(245, 166, 35, 0.05);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: Warning background nearly invisible in dark mode. Old value: #1e1400 (opaque dark amber). New value: rgba(245, 166, 35, 0.05) (5% alpha). On --bg (#080c0a) this is virtually indistinguishable from the background — a 30x reduction in effective opacity.

The .restart-modal-warning component and other warning surfaces will be invisible.

Fix: Increase opacity to at least 0.15, or restore an opaque dark amber value.

@henrypark133
henrypark133 changed the base branch from staging to main May 1, 2026 06:15
@ilblackdragon

Copy link
Copy Markdown
Member Author

Won't merge. Not applicable anymore.

henrypark133 added a commit that referenced this pull request Jun 8, 2026
#4572)

* docs(reborn): planner subagent + spawn_subagent schema redesign spec

Scope A: replace researcher flavor with planner, expose flavor enum to model,
rewrite tool description to nudge parent toward planning. Out-of-scope:
plan-mode for parent, durable plan files, nested spawning.

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

* feat(reborn): replace researcher flavor with planner

- Drop Researcher variant + entry + direction file
- Add Planner flavor: read codebase + http (web research)
- Returns structured Markdown plan (Goal/Plan/Files/Risks)
- Export builtin_flavor_catalog() for downstream schema builders
- Update prompt_material.rs test to use Planner instead of Researcher

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

* feat(loop_support): spawn_subagent schema redesign

- Wire rename: flavor_id -> subagent_type (serde alias keeps old format readable)
- Dynamic schema builder takes flavor catalog; renders enum + per-flavor descriptions
- Tool description rewrite nudges parent toward planner for complex tasks
- New SpawnSubagentFlavorDescriptor type (avoids cyclic dep on ironclaw_reborn)

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

* feat(reborn): wire flavor catalog into spawn_subagent decorator

Thread builtin_flavor_catalog() through SubagentSpawnCapabilityDecorator
to populate the new subagent_type enum + per-flavor description. Convert
&'static str descriptors to owned strings at the boundary to avoid
leaking 'static lifetimes across crates.

Test fixture cleanup: prompt_material.rs and spawn_result.rs now use
"planner" instead of the removed "researcher" flavor id.

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

* docs(reborn): scrub local paths from spec; match sibling style in planner.md

- docs/superpowers/specs/2026-06-08-planner-subagent-design.md: replace
  /Users/henry/Code/pi/... developer-local paths with repo-relative refs
  (doc-hygiene rule, PR #2689)
- directions/planner.md: open with prose to match general.md/explorer.md/
  coder.md siblings (was: H1 heading)

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

* refactor(reborn): derive flavor catalog from registry; cache spawn schema

Code-review cleanup:
- Add summary field to SubagentFlavor; derive builtin_flavor_catalog()
  from BUILTIN_SUBAGENT_FLAVORS instead of a hardcoded parallel list
  (single source of truth, no drift risk — for real this time)
- Delete FlavorDescriptor; return SpawnSubagentFlavorDescriptor directly,
  removing the .to_string() adapter at the runtime.rs boundary
- Drop now-vacuous parity test
- Guard empty-catalog schema: omit enum key when catalog is empty
  (avoids unsatisfiable JSON Schema enum: [] constraint)
- Cache parameters_schema in SubagentSpawnCapabilityPort at new() instead
  of rebuilding per LLM turn
- Drop unjustified pub(crate) on PLANNER_DIRECTION; match siblings

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

* test(reborn): update prompt_material assertion after planner.md prose rewrite

Fix Y rewrote planner.md opener from `# Planner subagent` H1 to prose
matching siblings. The test assertion in prompt_material.rs (added in
WU-A wire-up) needs to match the new lowercase prose form.

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

* test(reborn): cover empty-catalog schema, subagent_type canonical key, decorator threading

Closes code-review test-coverage gaps (.review/findings.json):
- empty catalog produces enum-less schema (regression guard on the empty-enum fix)
- invoke path accepts subagent_type as canonical wire key (not just deser roundtrip)
- decorator+catalog threading verified by source-of-truth assertion on builtin_flavor_catalog()

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

* refactor(reborn): generalize planner direction for non-code domains

The previous planner.md was code-coded: `Files to Modify`, `path/to/file.rs`,
`coder subagent will execute`, etc. Made the prompt useless for non-engineering
planning (trip planning, test plans, research outlines).

Rewrite to be domain-neutral while keeping the same structural value:
- Drop `Files to Modify` + `New Files` sections — Plan steps already carry
  what's produced; specific-naming discipline replaces the implicit "file"
  vocabulary
- Replace `implementation plan` / `the coder will execute` framing with
  `parent will execute` — works for any executor (coder, scheduler, human)
- Specificity examples broadened: `files, places, libraries, deadlines,
  URLs, costs`
- Trimmed length from ~60 to ~30 lines to match sibling direction prompts

Output format kept strict (Goal/Plan/Risks/References) — the structure IS
the value across domains, just the vocabulary needed neutralizing.

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

* refactor(reborn): address PR #4572 review feedback (6 of 7)

- subagent_spawn_port.rs: deny unknown fields on wire args (matches
  schema additionalProperties: false contract)
- subagent_spawn_port.rs: move SPAWN_SUBAGENT_DESCRIPTION to prompts/*.md
  per CLAUDE.md rule on multi-line prompt strings
- runtime.rs: decorator precomputes schema once instead of cloning the
  catalog + rebuilding on every decorate() call
- tests: codec decode with subagent_type canonical key, single-entry
  catalog schema, summary non-empty regression guard

C5 (SubagentToolId::WebSearch -> builtin.http naming) deferred for
discussion — pre-existing on origin/main, scope question.

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

* refactor(reborn): rename SubagentToolId::WebSearch -> Http

Closes PR #4572 C5: enum variant now matches the capability name it maps
to (builtin.http), consistent with every other variant in SubagentToolId.
Reader grepping for builtin.http now finds the variant; the future
distinction between general HTTP and a search-specific capability is no
longer blocked by name collision.

Pre-existing on origin/main but only the planner allowlist references it
now (researcher was removed earlier in this PR), and SubagentToolId is
flavors.rs-internal with no external consumers — pure mechanical rename.

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

* fix(host_runtime): align spawn_subagent first-party schema with decorator

Copilot flagged a second hardcoded schema source at
`crates/ironclaw_host_runtime/src/first_party_tools/schemas.rs` that still
advertised `flavor_id` and listed the deprecated `researcher` flavor.

The decorator's `tool_definitions()` skips publishing its dynamic schema
when `inner` already exposes spawn_subagent — so in production the model
could see the stale schemas.rs shape (flavor_id, researcher) instead of
the canonical decorator shape (subagent_type enum, planner).

This commit updates the host_runtime entry to publish the SAME shape the
decorator builds dynamically: `subagent_type` enum of
[general, explorer, coder, planner] + per-flavor description. Adds an
inline comment noting the dual source of truth and the long-term fix
(route through `build_spawn_subagent_parameters_schema`).

Test fixture: `first_party_builtin_tools.rs` updated to assert the
`subagent_type` key (was: `flavor_id`).

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

* refactor(loop_support): address PR #4572 round-3 feedback (C8-C12)

- prompts/spawn_subagent_description.md: add trailing newline (sibling parity)
- SpawnSubagentFlavorDescriptor.id: String -> SubagentKindId (typed id)
- Test: register/validate rejects unknown wire field (covers deny_unknown_fields)
- Test: new_with_schema propagates precomputed schema to tool_definition
- Port parameters_schema: Value -> Arc<Value> to avoid per-decorate() deep clone

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

* fix(ci): inline safety comment on .expect() in builtin_flavor_catalog

CI no-panics check scans line-by-line for // safety: markers on the same
line as .expect()/.unwrap()/assert!. The previous form put the reason
inside the .expect() message string (split across 3 lines after rustfmt),
which the script can't recognize.

Restructured so the .expect( call and // safety: comment land on the same
line after rustfmt.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
theredspoon pushed a commit to theredspoon/ironclaw that referenced this pull request Jun 21, 2026
* docs(rules): add review-driven guidance for Claude Code

Synthesizes recurring patterns from ~30 merged PRs, 147 bot review
comments (Copilot/Gemini), human reviews, and ~50 issues filed in the
past 2 weeks. Each rule cites the motivating PR/issue numbers.

New files:
- error-handling.md — silent-failure taxonomy (unwrap_or_default, .ok()?,
  poisoned caches), persist-then-reload atomicity, channel-edge error
  mapping. (nearai#2526, nearai#2633, nearai#2653, nearai#2673, nearai#2546, nearai#2407, nearai#2408)
- agent-evidence.md — side-effect claims must cite tool evidence,
  empty-fast outputs are errors, external-effect tools must read back,
  setup UI round-trip. (nearai#2544, nearai#2580, nearai#2582, nearai#2541, nearai#2545, nearai#2411, nearai#2543,
  nearai#2586)
- lifecycle.md — discovery vs. activation, terminal auth rejection,
  list_installed vs. list_active, deactivation unwinds, snapshot
  rehydrate must re-validate. (nearai#2556, nearai#2557, nearai#2558, nearai#2564, nearai#2419,
  PR nearai#2617, PR nearai#2631)

Extended:
- types.md — from_trusted boundary rule, validated-newtype template with
  shared validate(&str), serde(try_from) required for validated types,
  wire-stable enums (no Debug; serde alias for migrations), canonical
  wire-contract field naming. (PR nearai#2685, nearai#2681, nearai#2687, nearai#2678, nearai#2669,
  nearai#2665, nearai#2683, nearai#2702)
- safety-and-sandbox.md — every new ingress scans pre-transform/pre-
  injection, bounded resources (interners/streams/fan-out caps), cache
  keys must be complete. (nearai#2491, nearai#2676, nearai#2470, nearai#2633, nearai#2673, nearai#2710,
  PR nearai#2702)
- review-discipline.md — PR scope discipline, guardrail scripts are
  code (regression tests, grouped-import parsing, CI has_code inclusion),
  absolute-path ban in committed docs, stale comments after refactors.
  (PR nearai#2668, nearai#2628, nearai#2680, nearai#2687, nearai#2647, nearai#2689, nearai#2701)

All new files carry paths: frontmatter so they auto-load only on
matching files.

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

* refactor(rules): split agent-evidence into prompt + code rule

agent-evidence.md mixed two concerns: runtime agent instruction (what
the LLM should do when concluding a turn) and code-enforcement rules
(what the dispatcher, engine, and tools must implement). Rules under
.claude/rules/ only guide Claude Code when editing the repo — the
runtime agent never reads them.

Splits the two:

- crates/ironclaw_engine/prompts/codeact_postamble.md — new section
  "Evidence before claiming side effects". Sits next to the existing
  "FINAL() answer quality" guidance; loaded via include_str! in
  executor/prompt.rs (no Rust change needed).
- .claude/rules/tool-evidence.md — renamed from agent-evidence.md,
  keeps only the code invariants (engine v2 side-effect gate,
  empty-fast ToolError::EmptyResult, external-effect tools must read
  back, setup UI round-trip).

Prompt tests pass unchanged; the postamble addition is pure text.

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

* prompt: tighten evidence rule to FINAL() claims only, not tool use

Live-test validation of the "Evidence before claiming side effects"
section (added in the prior commit) showed it inhibited legitimate
tool use. With the original wording, `zizmor_scan_v2` live-recording
timed out at 302s with zero responses; reverting the postamble
restored healthy behavior (88s run, 8 shell calls including
`cargo install zizmor` and full workflow analysis).

The original phrasing conflated two things: what the agent should
claim and what tools it should call. The rule is only about the
claim. Re-tunes the section to:

- Open with an explicit "this does not restrict tool calls" scope.
- Drop the "<1ms = failure" heuristic (too broad — normal tools like
  `tool_info(schema)` are legitimately fast).
- Drop the full enumeration of forbidden side-effect verbs; keep the
  rule narrower and clearer.
- Shorten the code example (remove redundant early-return).

Re-tuned run: agent is active (shell calls, real reasoning), live
recording completes in ~9s. The remaining test failure is a
pre-existing assertion bug (exact `t == "shell"` match against tool
strings that now carry arguments like `"shell(cmd)"`) — reproduces
with the old postamble too.

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

* test(live): fix tool-name assertions + re-record zizmor traces

The two `zizmor_scan*` live tests had four broken tool-name assertions
that silently failed to match: `tools.iter().any(|t| t == "shell")`
against a tool list that now contains `"shell(cmd)"` strings (tool
events carry args via `format_action_display_name` in
`src/bridge/router.rs`). Two of the four were negative assertions
checking for the absence of `tool_install` recovery loops — those
silently passed even when a recovery loop actually ran. `sandbox_live_e2e.rs:203`
already used the correct `t == "shell" || t.starts_with("shell(")`
pattern; applied it consistently to all four sites.

Verified live:

- `IRONCLAW_LIVE_TEST=1 cargo test --test e2e_live -- zizmor_scan --ignored --test-threads=1`
  → 2 passed, 0 failed, 51.78s. Agent installs and runs zizmor
  end-to-end, producing real findings (exit code 14, dangerous
  triggers, excessive permissions, etc.).

Traces re-recorded with the tuned postamble (commit 50d8517) and
scrubbed: replaced `/home/illia/.cargo/bin/zizmor` with
`/home/user/.cargo/bin/zizmor` per the developer-local-path ban in
`.claude/rules/review-discipline.md`. No credentials, PII, or
high-entropy secrets in either trace (only git SHAs from zizmor's
workflow analysis output).

Replay still passes: `cargo test --test e2e_live -- zizmor_scan --ignored`
→ 2/2 ok.

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

* test(replay): update zizmor_scan_v2 insta snapshot

The engine v2 replay-snapshot gate (`engine_v2_tests::snapshot_zizmor_scan_v2`)
failed against the re-recorded trace from 1691efe because the old
snapshot encoded a broken run:

- final_state: Failed
- Missing Assistant message role
- 3 issues: thread_failure (error), no_response (warning), llm_error (error)
- 6 tool calls that never produced a final answer

The new trace completes cleanly:

- final_state: Done
- System / User / Assistant roles present
- 1 issue: mixed_mode (info)
- 3 shell tool calls + successful `FINAL()` with real findings

The snapshot was pinning a regression. Regenerated with
`INSTA_UPDATE=always cargo test --test e2e_engine_v2 -- snapshot_zizmor_scan_v2`;
passes on replay.

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

* docs(rules): address PR nearai#2714 review feedback

- review-discipline: reword "Doc Absolute Paths" as a review convention
  (pre-commit only scans .rs; the rule misleadingly claimed enforcement).
- safety-and-sandbox: broaden `paths:` frontmatter to include the actual
  ingress owners (`bridge`, `channels`, `workspace`, `agent`, engine
  crate) so the rule auto-loads where it applies.
- tool-evidence: mark the side-effect gate, empty-fast rule, and
  `unverified` flag as target/aspirational invariants — neither
  `ToolError::EmptyResult`, an `unverified` field on `ToolOutput`, nor a
  byte-count field on `ActionRecord` exist today. Point at concrete
  interim conventions (`ToolError::ExecutionFailed`, `unverified: true`
  in the JSON result body).
- types: scope "Validated newtypes must gate Deserialize" to *new*
  types, document the `CredentialName`/`ExtensionName` exception (they
  intentionally use `#[serde(transparent)]` + derived `Deserialize`
  under the `serde_does_not_revalidate` test). Clarify the
  `from_trusted` trust boundary (trusted = typed upstream, untrusted =
  raw JSON field even if the field *name* is "registry entry").
  Switch `new` template to `impl Into<String>` to avoid an unnecessary
  clone when an owned `String` is passed.

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

* docs(rules): simplify types.md + split doc-hygiene; address review round 2

- types: collapse two templates into one canonical validated-newtype
  shape. New types use `#[serde(try_from = "String")]` with a shared
  `validate(&str)` helper — no more dual "transparent for some /
  try_from for others" guidance. `CredentialName`/`ExtensionName` are
  documented as the sole legacy exception (locked in by the
  `serde_does_not_revalidate` test); new code must not copy their
  `transparent` + `from_trusted` pattern. Removes the long "Using
  `from_trusted` safely" section and the separate "Validated newtypes
  must gate Deserialize" subsection that contradicted the Don'ts list.
- doc-hygiene: new tiny rule file scoped to `**/*.md`, `**/*.py`,
  `docs/**` that carries the "no developer-local absolute paths in
  committed docs" convention. Removed from review-discipline.md where
  its `src/**/*.rs` scope meant the rule never loaded on the files it
  governed.

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

* test(live): match hyphenated tool-install in attempted_relevant_tool

The engine records `action_name` as the raw string the LLM emitted
(`crates/ironclaw_engine/src/executor/structured.rs:381`), and the
registry's lookup canonicalization only affects dispatch — not the
name that reaches `StatusUpdate::ToolStarted`. The two other predicates
in this file (`bad_recovery` at :420, `phase_b_recovery` at :531)
already defend against both forms; this one should too, for
consistency. Addresses PR nearai#2714 review.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
theredspoon pushed a commit to theredspoon/ironclaw that referenced this pull request Jun 21, 2026
nearai#4572)

* docs(reborn): planner subagent + spawn_subagent schema redesign spec

Scope A: replace researcher flavor with planner, expose flavor enum to model,
rewrite tool description to nudge parent toward planning. Out-of-scope:
plan-mode for parent, durable plan files, nested spawning.

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

* feat(reborn): replace researcher flavor with planner

- Drop Researcher variant + entry + direction file
- Add Planner flavor: read codebase + http (web research)
- Returns structured Markdown plan (Goal/Plan/Files/Risks)
- Export builtin_flavor_catalog() for downstream schema builders
- Update prompt_material.rs test to use Planner instead of Researcher

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

* feat(loop_support): spawn_subagent schema redesign

- Wire rename: flavor_id -> subagent_type (serde alias keeps old format readable)
- Dynamic schema builder takes flavor catalog; renders enum + per-flavor descriptions
- Tool description rewrite nudges parent toward planner for complex tasks
- New SpawnSubagentFlavorDescriptor type (avoids cyclic dep on ironclaw_reborn)

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

* feat(reborn): wire flavor catalog into spawn_subagent decorator

Thread builtin_flavor_catalog() through SubagentSpawnCapabilityDecorator
to populate the new subagent_type enum + per-flavor description. Convert
&'static str descriptors to owned strings at the boundary to avoid
leaking 'static lifetimes across crates.

Test fixture cleanup: prompt_material.rs and spawn_result.rs now use
"planner" instead of the removed "researcher" flavor id.

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

* docs(reborn): scrub local paths from spec; match sibling style in planner.md

- docs/superpowers/specs/2026-06-08-planner-subagent-design.md: replace
  /Users/henry/Code/pi/... developer-local paths with repo-relative refs
  (doc-hygiene rule, PR nearai#2689)
- directions/planner.md: open with prose to match general.md/explorer.md/
  coder.md siblings (was: H1 heading)

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

* refactor(reborn): derive flavor catalog from registry; cache spawn schema

Code-review cleanup:
- Add summary field to SubagentFlavor; derive builtin_flavor_catalog()
  from BUILTIN_SUBAGENT_FLAVORS instead of a hardcoded parallel list
  (single source of truth, no drift risk — for real this time)
- Delete FlavorDescriptor; return SpawnSubagentFlavorDescriptor directly,
  removing the .to_string() adapter at the runtime.rs boundary
- Drop now-vacuous parity test
- Guard empty-catalog schema: omit enum key when catalog is empty
  (avoids unsatisfiable JSON Schema enum: [] constraint)
- Cache parameters_schema in SubagentSpawnCapabilityPort at new() instead
  of rebuilding per LLM turn
- Drop unjustified pub(crate) on PLANNER_DIRECTION; match siblings

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

* test(reborn): update prompt_material assertion after planner.md prose rewrite

Fix Y rewrote planner.md opener from `# Planner subagent` H1 to prose
matching siblings. The test assertion in prompt_material.rs (added in
WU-A wire-up) needs to match the new lowercase prose form.

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

* test(reborn): cover empty-catalog schema, subagent_type canonical key, decorator threading

Closes code-review test-coverage gaps (.review/findings.json):
- empty catalog produces enum-less schema (regression guard on the empty-enum fix)
- invoke path accepts subagent_type as canonical wire key (not just deser roundtrip)
- decorator+catalog threading verified by source-of-truth assertion on builtin_flavor_catalog()

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

* refactor(reborn): generalize planner direction for non-code domains

The previous planner.md was code-coded: `Files to Modify`, `path/to/file.rs`,
`coder subagent will execute`, etc. Made the prompt useless for non-engineering
planning (trip planning, test plans, research outlines).

Rewrite to be domain-neutral while keeping the same structural value:
- Drop `Files to Modify` + `New Files` sections — Plan steps already carry
  what's produced; specific-naming discipline replaces the implicit "file"
  vocabulary
- Replace `implementation plan` / `the coder will execute` framing with
  `parent will execute` — works for any executor (coder, scheduler, human)
- Specificity examples broadened: `files, places, libraries, deadlines,
  URLs, costs`
- Trimmed length from ~60 to ~30 lines to match sibling direction prompts

Output format kept strict (Goal/Plan/Risks/References) — the structure IS
the value across domains, just the vocabulary needed neutralizing.

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

* refactor(reborn): address PR nearai#4572 review feedback (6 of 7)

- subagent_spawn_port.rs: deny unknown fields on wire args (matches
  schema additionalProperties: false contract)
- subagent_spawn_port.rs: move SPAWN_SUBAGENT_DESCRIPTION to prompts/*.md
  per CLAUDE.md rule on multi-line prompt strings
- runtime.rs: decorator precomputes schema once instead of cloning the
  catalog + rebuilding on every decorate() call
- tests: codec decode with subagent_type canonical key, single-entry
  catalog schema, summary non-empty regression guard

C5 (SubagentToolId::WebSearch -> builtin.http naming) deferred for
discussion — pre-existing on origin/main, scope question.

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

* refactor(reborn): rename SubagentToolId::WebSearch -> Http

Closes PR nearai#4572 C5: enum variant now matches the capability name it maps
to (builtin.http), consistent with every other variant in SubagentToolId.
Reader grepping for builtin.http now finds the variant; the future
distinction between general HTTP and a search-specific capability is no
longer blocked by name collision.

Pre-existing on origin/main but only the planner allowlist references it
now (researcher was removed earlier in this PR), and SubagentToolId is
flavors.rs-internal with no external consumers — pure mechanical rename.

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

* fix(host_runtime): align spawn_subagent first-party schema with decorator

Copilot flagged a second hardcoded schema source at
`crates/ironclaw_host_runtime/src/first_party_tools/schemas.rs` that still
advertised `flavor_id` and listed the deprecated `researcher` flavor.

The decorator's `tool_definitions()` skips publishing its dynamic schema
when `inner` already exposes spawn_subagent — so in production the model
could see the stale schemas.rs shape (flavor_id, researcher) instead of
the canonical decorator shape (subagent_type enum, planner).

This commit updates the host_runtime entry to publish the SAME shape the
decorator builds dynamically: `subagent_type` enum of
[general, explorer, coder, planner] + per-flavor description. Adds an
inline comment noting the dual source of truth and the long-term fix
(route through `build_spawn_subagent_parameters_schema`).

Test fixture: `first_party_builtin_tools.rs` updated to assert the
`subagent_type` key (was: `flavor_id`).

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

* refactor(loop_support): address PR nearai#4572 round-3 feedback (C8-C12)

- prompts/spawn_subagent_description.md: add trailing newline (sibling parity)
- SpawnSubagentFlavorDescriptor.id: String -> SubagentKindId (typed id)
- Test: register/validate rejects unknown wire field (covers deny_unknown_fields)
- Test: new_with_schema propagates precomputed schema to tool_definition
- Port parameters_schema: Value -> Arc<Value> to avoid per-decorate() deep clone

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

* fix(ci): inline safety comment on .expect() in builtin_flavor_catalog

CI no-panics check scans line-by-line for // safety: markers on the same
line as .expect()/.unwrap()/assert!. The previous form put the reason
inside the .expect() message string (split across 3 lines after rustfmt),
which the script can't recognize.

Restructured so the .expect( call and // safety: comment land on the same
line after rustfmt.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants