ai: fold in ramekin outbox proposals
Assisted-by: Claude Opus 5.5 via Claude Code
diff --git a/ai/skills/design-notes/SKILL.md b/ai/skills/design-notes/SKILL.md
index 909a6b1a..9a7f1112 100644
--- a/ai/skills/design-notes/SKILL.md
+++ b/ai/skills/design-notes/SKILL.md
@@ -9,6 +9,10 @@ Supplementary notes on designing software with AI, accumulated from experience a
*This is a self-improving skill — see the `self-improving-skills` skill.*
+## Before proposing: read the existing record
+
+If the task points at a design record, or the repo already keeps them, read it first. Implementation conforms to the record, not the reverse, and a dated record is never rewritten to match behavior that arrived after it.
+
## The implementation trap
AI collapses design and implementation into a single step. Describe a feature, receive hundreds of lines — with every architectural decision made silently. The first time you see the AI's design thinking is when you're reading code, which is the most expensive place to discover a disagreement.
diff --git a/ai/skills/ruby-style/SKILL.md b/ai/skills/ruby-style/SKILL.md
index 35cc74c2..aeec9109 100644
--- a/ai/skills/ruby-style/SKILL.md
+++ b/ai/skills/ruby-style/SKILL.md
@@ -18,6 +18,7 @@ immediately instead of propagating `nil` through the code.
# good
config.fetch(:timeout)
config.fetch(:timeout, 30)
+config.fetch(:timeout) { expensive_default }
# bad — silent nil on typos or missing keys
config[:timeout]
@@ -68,12 +69,16 @@ and composes naturally with any iterator.
```ruby
# good — compose onto the base enumerator
array.each.with_index { |item, i| ... }
+array.map.with_index { |item, i| ... }
+array.flat_map.with_index { |item, i| ... }
array.each.with_object([]) { |item, acc| ... }
hash.each { |key, value| ... }
# bad — specialized methods
array.each_with_index { |item, i| ... }
array.each_with_object([]) { |item, acc| ... }
+hash.each_value { |value| ... }
+hash.each_key { |key| ... }
```
The same goes for the specialized `each_*` readers (`each_value`,
@@ -160,6 +165,10 @@ File.expand_path("../secrets.yml", "/app/config/database.yml")
Use `File` for plain IO when `Pathname` adds nothing.
+`system`, `exec`, and Rake's `sh` reject a `Pathname` with a
+`TypeError` — they want a `String`. Convert at that boundary:
+`sh "ls", path.to_s`.
+
## Building hashes
Prefer `.to_h` with a block over `.each_with_object` or manual
@@ -292,6 +301,11 @@ Literals in files with no magic comment are *chilled*: mutating one
warns instead of raising. Run with `RUBYOPT=-W:deprecated` to find them,
then `.dup` each one.
+Don't fight an existing convention. If a project already relies on the
+magic comment — RuboCop's `Style/FrozenStringLiteralComment` is
+enabled, or the files already carry it — keep adding it so the codebase
+stays consistent.
+
## Requires
Put `require` calls at the top of the file. Defer one inside a method
diff --git a/ai/skills/rust/scaffolding.md b/ai/skills/rust/scaffolding.md
index f8275823..3f057135 100644
--- a/ai/skills/rust/scaffolding.md
+++ b/ai/skills/rust/scaffolding.md
@@ -80,11 +80,12 @@ Notes on this template:
detail survives through tests and library consumers.
- `hegeltest` — property-based testing built on the Hypothesis engine,
with built-in shrinking.
-- `[lints.clippy]` — `self_named_module_files` enforces `foo.rs` over
- `foo/mod.rs` for module files. The rest are panic-discipline lints:
- `unwrap`, `expect`, raw `panic!`, slice indexing, and unchecked
- arithmetic are denial-of-service bugs waiting for untrusted input. All
- are lint levels, so they live here rather than in `.clippy.toml`. See
+- `[lints.clippy]` — `self_named_module_files` enforces `foo/mod.rs`
+ over `foo.rs` for directory modules (P-MOD in `style.md`). The rest
+ are panic-discipline lints: `unwrap`, `expect`, raw `panic!`, slice
+ indexing, and unchecked arithmetic are denial-of-service bugs waiting
+ for untrusted input. All are lint levels, so they live here rather
+ than in `.clippy.toml`. See
`style.md` "Panic discipline" for rationale and how to opt out at a
single call site when an invariant is genuinely local.
- A project that's already a multi-crate workspace hoists the block to
diff --git a/ai/skills/rust/server.md b/ai/skills/rust/server.md
index 6b5e1ada..585a6312 100644
--- a/ai/skills/rust/server.md
+++ b/ai/skills/rust/server.md
@@ -82,23 +82,31 @@ async fn main() -> miette::Result<()> {
async fn shutdown_signal() {
let ctrl_c = async {
- signal::ctrl_c().await.expect("install ctrl-c handler");
+ if let Err(err) = signal::ctrl_c().await {
+ tracing::error!(%err, "failed to install SIGINT handler");
+ std::future::pending::<()>().await;
+ }
};
#[cfg(unix)]
let terminate = async {
- signal::unix::signal(signal::unix::SignalKind::terminate())
- .expect("install SIGTERM handler")
- .recv()
- .await;
+ match signal::unix::signal(signal::unix::SignalKind::terminate()) {
+ Ok(mut sigterm) => {
+ sigterm.recv().await;
+ }
+ Err(err) => {
+ tracing::error!(%err, "failed to install SIGTERM handler");
+ std::future::pending::<()>().await;
+ }
+ }
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
- _ = ctrl_c => tracing::info!("received SIGINT, shutting down"),
- _ = terminate => tracing::info!("received SIGTERM, shutting down"),
+ () = ctrl_c => tracing::info!("received SIGINT, shutting down"),
+ () = terminate => tracing::info!("received SIGTERM, shutting down"),
}
}
```
@@ -106,6 +114,9 @@ async fn shutdown_signal() {
Notes:
- `shutdown_signal()` returns once either SIGINT or SIGTERM fires.
+ A handler that fails to install logs the error and parks forever
+ rather than panicking, so the other signal still works and the
+ template passes the `expect_used` lint under `-D warnings`.
`axum::serve(...).with_graceful_shutdown(...)` stops accepting new
connections and lets in-flight requests finish.
- Distroless containers have no init process. Either rely on this