Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Strict Standards

Beyond the language-specific security rules that every plan inherits, Wiggum offers an opt-in strict mode that injects a richer, harder-to-satisfy rule set into every prompt. Strict mode is for projects where “the build passes” is not enough — every commit must produce code that complies with a verified-modern toolchain baseline for the target language.

Enabling strict mode

Add strict = true to your [style] section:

[style]
strict = true   # inject the language's strict ruleset

When strict = true, the orchestrator injects the matching language block into every subagent’s task prompt and escalates the profile’s lint/audit commands — it does not replace the existing lint_cmd / audit_cmd, it adds to them. The intent is the same as the Rust profile: fail-secure by default, parse at boundaries, no panics on untrusted input, no weak crypto, supply-chain audited in CI, warnings treated as errors.

Disable per-task if a specific task is a spike or prototype:

[[phases.tasks]]
slug = "experiment-with-rust-macros"
title = "Investigate derive macro ergonomics"
goal = "Spike to evaluate macro options; produce a recommendation document."
strict = false

What strict mode adds

Each language profile defines a strict_rules array that is injected as a new section in the generated prompts only when strict = true. The rules are language-specific but share a common theme: they encode the modern, security-centric baseline that the language’s toolchain makes possible when fully engaged.

For Rust, strict mode mirrors the rules in your project’s nick-v2.md (the personal standards file; supersedes the original nick.md). New rules are sourced from the Rust 1.78→1.99 catchup guide at ~/Projects/rust/rust-docs/rust-catchup-1.78-1.99.md. For every other language, the ruleset is documented in the companion file docs/strict-lints.md at the root of the wiggum repo.

What strict mode adds for Rust (1.95+)

The Rust strict_rules array is sourced from two documents:

  • ~/Projects/nick-v2.md — DDD-lite hexagonal layout, narrow port traits, AuthContext + idempotency keys, with_tx transaction boundaries, object-safe async port traits, CLI input parsing at adapter boundaries, the strengthened #[expect] rule, OWASP web security defaults.
  • rust-catchup-1.78-1.99.md — Rust 1.95+ syntax (if let arm guards, let chains on 2024 edition), std::io::pipe() + Vec::extract_if (1.87+), the 1.97 pin! deref coercion soundness fix, deny(dead_code_pub_in_binary) for tight binaries, cross-platform #[cfg(unix)] hygiene.

Code snippets for surprising new rules

Object-safe async port trait (boxed future)

In a hexagonal codebase where port traits are used as Arc<dyn Trait> for dependency injection, native async fn in traits isn’t object-safe. Use the async-trait crate or write an explicit boxed future:

#![allow(unused)]
fn main() {
// Native async — NOT object-safe
trait MyPort {
    async fn process(&self) -> Result<()>;
}

// Object-safe — explicit boxed future
trait MyPort: Send + Sync {
    fn process(&self) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
}

impl MyPort for ConcreteAdapter {
    fn process(&self) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
        Box::pin(async move {
            // implementation
            Ok(())
        })
    }
}
}

One heap allocation per call is acceptable for HTTP handlers and DB operations; reserve concrete impl Trait for hot paths.

LazyCell vs LazyLock choice (Rust 1.80+)

#![allow(unused)]
fn main() {
// Single-threaded context — use LazyCell
use std::cell::LazyCell;
static CONFIG: LazyCell<Config> = LazyCell::new(|| load_config());

// Multi-threaded initialisation — use LazyLock
use std::sync::LazyLock;
static CONFIG: LazyLock<Config> = LazyLock::new(|| load_config());
}

Reaching for LazyLock thread-locally signals a confused ownership model — single-threaded code should use LazyCell (no atomic reference count, no thread safety overhead).

#[expect(dead_code)] over #[allow(dead_code)] (Rust 1.81+)

#![allow(unused)]
fn main() {
// Old way — silently becomes dead code when the lint no longer fires
#[allow(dead_code)]
fn temporary_scaffolding() { }

// New way — compiler warns when the suppression becomes unnecessary
#[expect(dead_code)]
fn temporary_scaffolding() { }
}

Use #[expect(lint, reason = "...")] for temporary suppressions; reserve #[allow] for permanent, intentional deviations with a documented reason.

Let chains (2024 edition)

#![allow(unused)]
fn main() {
if let Some(x) = opt
    && let Ok(y) = x.parse::<i32>()
    && y > 0
{
    println!("{y}");
}
}

Replaces nested if let Some(...) { if let Ok(...) { if ... {} } } chains. Available on the 2024 edition only — port your crate to 2024 before adopting this pattern.

Match arm if let guards (Rust 1.95+)

#![allow(unused)]
fn main() {
match event {
    Event::Login { user_id } if user_id.is_admin() => handle_admin(),
    Event::Login { user_id } => handle_user(user_id),
    _ => {}
}
}

Cleaner pattern-matching control flow than nesting a match inside an if let.

Example strict-mode rule excerpts by language:

LanguageSample strict rules (see docs/strict-lints.md for the full set)
RustNo .unwrap() / .expect() / panic! in production code; no index slicing that can panic; no #[allow(clippy::...)] suppressions; full pedantic + nursery + perf clippy profile with hard denials
Gogolangci-lint v2 + gofumpt + govulncheck; never discard errors; context.Context everywhere; depguard bans on crypto/md5, crypto/sha1, math/rand
TypeScripttypescript-eslint v8 strictTypeChecked; noUncheckedIndexedAccess; Zod at every input boundary; no any / !; node:crypto for randomness
PythonRuff with the S (bandit) group on; mypy --strict; pip-audit; no pickle.loads / yaml.load; secrets for tokens
JavaError Prone + NullAway + SpotBugs findsecbugs; PreparedStatement only; no ObjectInputStream on untrusted data
C# / .NETRoslyn AnalysisMode=All + Nullable=enable + Security Code Scan; no ! null-forgiving; no BinaryFormatter
Kotlindetekt allRules + explicitApi(); no !!; no GlobalScope; structured concurrency only
SwiftSwift 6 language mode + complete strict concurrency; no @unchecked Sendable; no force-unwrap/try/cast outside tests
RubyRuboCop Security/* + Lint/* as errors; Brakeman with -z; Sorbet # typed: strict
Elixir--warnings-as-errors + mix credo --strict + Dialyzer + Sobelow --exit; never String.to_atom/1 on user input
PHPPHPStan level max + phpstan-strict-rules + Psalm --taint-analysis; declare(strict_types=1); password_hash (Argon2id); random_bytes / random_int

Cross-language baseline

Every language profile’s strict_rules includes the same closing pair drawn from the cross-language baseline in docs/strict-lints.md:

  • Treat warnings as errors — the language’s “warnings as errors” switch stays on; a warning fails the build.
  • No suppression without justification — never blanket-disable a rule; suppress narrowly, inline, with a rule ID and a one-line reason. Prefer fixing.

These hold regardless of language and are injected alongside the language-specific rules.

Where strict rules appear

When strict = true, every generated artifact that contains prompt content gets the strict block:

  • VSCode target — orchestrator.prompt.md, each tasks/T{NN}-{slug}.md, evaluator.prompt.md (when [evaluator] is configured)
  • opencode target — orchestrator.md (the embedded subagent body inherits the rules), evaluator.md (when [evaluator] is configured)
  • Claude target — CLAUDE.md (so Claude Code sees the rules on every session)
  • agent-rules target — .cursorrules, .windsurfrules, .github/copilot-instructions.md (so Cursor / Windsurf / Copilot users see them too)

Tooling version pins

The strict profiles track specific toolchain versions because the rules are written against those tools’ surface area. When you adopt strict mode, pin your project to the version that matches the rules — moving to a newer toolchain without re-pinning the rules can leave gaps.

Current pins (see docs/strict-lints.md for the canonical list):

  • Rust — 1.95+ minimum (match arm if let guards, deny(dead_code_pub_in_binary), LazyCell/LazyLock); 1.97+ recommended for the pin! deref coercion soundness fix and [build] warnings = "deny"; 2024 edition required for let chains.
  • Go — golangci-lint v2 + Go 1.24+ + gofumpt
  • TypeScript — typescript-eslint v8 flat config + projectService: true
  • Python — Ruff (linter + formatter) + mypy --strict + Python 3.12+
  • Java — Error Prone + NullAway + SpotBugs + findsecbugs on JDK 21+
  • C# / .NET — Roslyn AnalysisMode=All + Security Code Scan on .NET 8+
  • Kotlin — detekt allRules = true + ktlint on JDK 21+
  • Swift — Swift 6 language mode + SwiftLint --strict
  • Ruby — RuboCop (with rubocop-performance, rubocop-rspec) + Brakeman + Sorbet on Ruby 3.2+
  • Elixir — Credo --strict + Dialyzer via Dialyxir + Sobelow on Elixir 1.16+ / OTP 26+
  • PHP — PHPStan 2.x at level max + Psalm --taint-analysis on PHP 8.3+

When to enable strict mode

Enable strict = true when:

  • The project is security-sensitive (auth, payments, PII, infra)
  • The team is multi-engineer and you want a single canonical standard instead of per-developer conventions
  • You’re starting a new codebase and you want to prevent AI-generated slop from accumulating
  • The project will outlive any one AI model’s current capabilities — rules survive the model

Leave strict = false (the default) when:

  • You’re prototyping or evaluating wiggum itself
  • The codebase predates the strict toolchain baseline (e.g. a Python 2 codebase)
  • You need fast iteration and are willing to clean up later

Strict mode is additive, not destructive. You can flip strict = true on an existing plan at any time — the next wiggum generate injects the new rules into all subagent prompts without altering task content, hints, or preflight commands.