@agentsfleet/orly 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dispatch/lib.sh CHANGED
@@ -80,6 +80,7 @@ declare -A DISPATCH_GLOSS=(
80
80
  [LOG]="LOGging discipline (scoped event, error_code, severity, redaction)"
81
81
  [MSID]="Milestone-ID ban in source (M{N}_{NNN} / §x.y / T{N} / dim)"
82
82
  [ERR]="ERror Registry (UZ-XXX-NNN declared + referenced)"
83
+ [ERR-RS]="Rust ERror discipline (one type per crate, Result alias, no stringified cause)"
83
84
  [GRP]="GREptile rule audit (diff vs greptile-learnings/RULES.md codes)"
84
85
  [LDC]="Legacy-Design Consult (A remove / B patch / C keep)"
85
86
  )
@@ -92,13 +93,35 @@ dispatch_init() {
92
93
  DISPATCH_EXTS=("$@")
93
94
  }
94
95
 
96
+ # Paths this repository's engine wrote, one per line, from the install record.
97
+ #
98
+ # A managed file is orly's, not the repository's: the next `orly update`
99
+ # overwrites it, so a finding against one is a chore nobody can complete. Left
100
+ # unfiltered, staging a fresh install put orly's own leaf helpers in front of
101
+ # orly's own length gate — audits/logging.sh is 444 lines against a 350 cap —
102
+ # and the first commit of an adoption failed on the adoption itself.
103
+ dispatch_managed_paths() {
104
+ local config="$TARGET_ROOT/.oracle/orly.json"
105
+ [ -f "$config" ] || return 0
106
+ sed -n '/"managed"[[:space:]]*:[[:space:]]*\[/,/\]/p' "$config" \
107
+ | grep -oE '"[^"]+"' \
108
+ | sed -e 's/^"//' -e 's/"$//' \
109
+ | grep -vx 'managed' || true
110
+ }
111
+
95
112
  # Resolve targets: explicit file args, or --staged self-discovery via git.
113
+ # Explicit arguments are never filtered — naming a file is asking about it.
96
114
  dispatch_resolve_files() {
97
115
  DISPATCH_FILES=()
98
116
  if [ "${1:-}" = "--staged" ]; then
99
- local pathspecs=() e
117
+ local pathspecs=() e managed
100
118
  for e in "${DISPATCH_EXTS[@]}"; do pathspecs+=("$e"); done
101
- while IFS= read -r f; do [ -n "$f" ] && DISPATCH_FILES+=("$f"); done \
119
+ managed="$(dispatch_managed_paths)"
120
+ while IFS= read -r f; do
121
+ [ -n "$f" ] || continue
122
+ [ -n "$managed" ] && grep -qxF "$f" <<<"$managed" && continue
123
+ DISPATCH_FILES+=("$f")
124
+ done \
102
125
  < <(git -C "$TARGET_ROOT" diff --cached --name-only --diff-filter=ACMRT -- "${pathspecs[@]}" \
103
126
  | grep -vE '(^|/)(vendor|third_party|node_modules|\.zig-cache|dist|build|\.next)/' || true)
104
127
  elif [ "$#" -ge 1 ]; then
@@ -124,9 +147,11 @@ dispatch_resolve_files() {
124
147
  # dispatch must never claim a file its gates cannot parse.
125
148
  dispatch_add_shebang_files() {
126
149
  [ "${1:-}" = "--staged" ] || return 0
127
- local f base path firstline
150
+ local f base path firstline managed
151
+ managed="$(dispatch_managed_paths)"
128
152
  while IFS= read -r f; do
129
153
  [ -n "$f" ] || continue
154
+ [ -n "$managed" ] && grep -qxF "$f" <<<"$managed" && continue
130
155
  base="${f##*/}"
131
156
  case "$base" in *.*) continue ;; esac
132
157
  path="$TARGET_ROOT/$f"; [ -f "$path" ] || path="$f"; [ -f "$path" ] || continue
@@ -10,6 +10,33 @@ that moment. 🤔 judgment-only — no `.sh` pair; CONFORM's deterministic gates
10
10
  Section-scan first (`grep -n "^## " dispatch/lifecycle.md`); read the stage you
11
11
  are entering, not the whole file.
12
12
 
13
+ ## What runs at each stage
14
+
15
+ The mechanical half of the lifecycle, in one table. Everything in the "runs"
16
+ column is a command, not a claim — `orly gate` reads the repository's declared
17
+ `.oracle/orly.json` commands and never invents one.
18
+
19
+ | Stage | Runs | Fired by |
20
+ |---|---|---|
21
+ | CHORE(open) | the declared `verify.unit` once, to record `Test Baseline:` | the agent |
22
+ | PLAN | nothing — no file mutations | — |
23
+ | EXECUTE | the dispatch façade for each edited file type; DOC READ recorded via `audits/doc-read.sh log` | the agent, and the runtime's read hook where it has one |
24
+ | CONFORM | the declared `conform` command | `orly gate work` — the generated pre-commit hook |
25
+ | VERIFY (Section) | `conform` plus the declared lane covering the surface touched | the agent |
26
+ | VERIFY (milestone) | the fast `verify.*` set: lint, unit, version | `orly gate verify` — the generated pre-push hook |
27
+ | REVIEW | gstack `/review` over the diff | the agent |
28
+ | DOCUMENT | nothing mechanical | — |
29
+ | COMMIT | `conform` again, on the staged tree | the pre-commit hook |
30
+ | CHORE(close) | every spec criterion, branch shape, clean tree, pushed branch, docs surface, and the slow `verify.*` suites | `orly gate pr`, by hand, before `gh pr create` |
31
+ | LAND | nothing gated | — |
32
+
33
+ Three properties hold this together. A gate only asks what it can answer
34
+ honestly at that moment: `work` judges no git state, because a commit hook's
35
+ tree is dirty by construction and a new spec is committed on the default branch.
36
+ Each tier runs once per cadence — `conform` at commit, the fast set at push, the
37
+ slow set at the close. And `orly gate pr` can skip the fast set because
38
+ `git.pushed` proves HEAD is the commit pre-push already graded.
39
+
13
40
  ## CHORE (open) — runbook
14
41
 
15
42
  1. Spec `docs/v*/pending/` → `active/`; `Status: IN_PROGRESS`; `Branch:` set.
@@ -31,24 +31,52 @@ Package-scoped runners (`bun run test`, `vitest <file>`, `cargo test -p <crate>`
31
31
  are **not** verification — they skip every other package's lint and tests and the
32
32
  cross-language gates. The declared `make` targets are the canonical gates.
33
33
 
34
- ## Required runs
34
+ ## Two cadences, one boundary
35
+
36
+ "Always" is not a cadence. A Section closing and a milestone closing are
37
+ different claims, and running the whole declared set at both spends the
38
+ wall-clock of the comprehensive tier to answer a question the scoped lane
39
+ already answered.
40
+
41
+ The gates match those cadences: `orly gate work` runs the declared `conform`
42
+ alone (every commit, seconds), `orly gate verify` runs the fast `verify.*` set
43
+ (every push), and `orly gate pr` runs the whole-branch criteria and the slow
44
+ suites (the close). Each tier runs once per cadence.
45
+
46
+ **Section lane (per Section, and per done-claim inside one).** `make
47
+ harness-verify` — the declared `conform`, seconds — plus the lane that covers
48
+ the surface the Section touched (`make test-unit-rustd`, `make test-unit-app`,
49
+ …). This proves a Section. It is not a repository claim and never satisfies a
50
+ VERIFY row on its own.
51
+
52
+ **Milestone boundary (once, at CHORE(close), before the PR).** Every command
53
+ `.oracle/orly.json` declares, in one pass: `conform`, `verify.lint`,
54
+ `verify.unit`, `verify.version`, and the slow tier (`verify.integration`,
55
+ `verify.memory`). This is the repository claim, and it is what the done-message
56
+ below reports.
57
+
58
+ Run `orly gate pr` at the close. It does not re-run the fast tier, and it does
59
+ not need to: its own `git.pushed` criterion proves HEAD is exactly the commit
60
+ the pre-push `orly gate verify` already graded. A bare `orly gate` chains all
61
+ three gates — use it on a branch whose earlier gates never ran.
35
62
 
36
63
  | Target | When |
37
64
  |---|---|
38
- | `make lint-all` | Always. The declared `verify.lint`. |
39
- | `make test-unit-all` | Always. The declared `verify.unit` — the cargo workspace plus every package coverage gate. |
40
- | `make harness-verify` | Always. The declared `conform` — the deterministic gate audit. |
41
- | `make check-version` | Always. The declared `verify.version`. |
65
+ | `make harness-verify` | Section lane and boundary. The declared `conform` — the deterministic gate audit. |
66
+ | `make lint-all` | Boundary. The declared `verify.lint`. |
67
+ | `make test-unit-all` | Boundary. The declared `verify.unit` — the cargo workspace plus every package coverage gate. |
68
+ | `make check-version` | Boundary. The declared `verify.version`. |
69
+ | `make test-integration-rustd` | Boundary. The declared `verify.integration` — live Postgres and Redis; `orly gate pr` skips it on a branch carrying no code. |
42
70
  | `make wire-fixtures` | The `/v1/runners` wire types changed. Regenerate, then review the diff — a changed fixture IS the wire change. |
43
71
  | `make bench` (local) | Diff touches request-path code, allocator wiring, or startup/shutdown sequencing. |
44
72
  | `API_BENCH_URL=https://api-dev.agentsfleet.net/healthz make bench` | After branch deploys to dev. |
45
73
  | `/orly-write-integration-test` (skill) | With `/orly-write-unit-test` at VERIFY when the diff crosses module boundaries with real I/O; otherwise record `N/A — <reason>`. |
46
74
  | Acceptance e2e (live tier) | Diff touches a surface the live/acceptance tier covers — relevant suites green, or their opt-in skip matrix recorded. |
47
75
 
48
- There is no slow tier. The Zig integration and memory-leak lanes retired with the
49
- rest of the Zig gating, so nothing a developer runs needs live Postgres or Redis.
50
- A package-scoped runner (`cargo test -p afd_wire`, `bun run test` inside a
51
- package) proves that package and never the repository.
76
+ The declared set in `.oracle/orly.json` is the source of truth for what exists,
77
+ and a lane listed there is a lane that runs at the boundary — including the slow
78
+ tier. A package-scoped runner (`cargo test -p afd_wire`, `bun run test` inside a
79
+ package) proves that package and never the repository, at either cadence.
52
80
 
53
81
  ## Wire-fixture evidence rule
54
82
 
@@ -70,10 +98,16 @@ generated output; a type change with no fixture diff means the emitter never ran
70
98
 
71
99
  ## Required output (done-message)
72
100
 
73
- **Success:**
101
+ **Success (boundary):**
74
102
 
75
103
  ```
76
- ✅ Verified: 🧪 lint-all ✓ · 🧪 test-unit-all ✓ <N>p/<M>s · 🔆 harness-verify ✓ · 🔆 check-version ✓
104
+ ✅ Verified: 🧪 lint-all ✓ · 🧪 test-unit-all ✓ <N>p/<M>s · 🧩 test-integration-rustd ✓ · 🔆 harness-verify ✓ · 🔆 check-version ✓
105
+ ```
106
+
107
+ **Success (Section lane)** — names the lane it ran, never the repository:
108
+
109
+ ```
110
+ ✅ Section verified: 🔆 harness-verify ✓ · 🧪 test-unit-<surface> ✓ <N>p/<M>s
77
111
  ```
78
112
 
79
113
  **Failure (any required target failed):**
@@ -127,7 +127,7 @@ shebang and is the authoritative check.
127
127
  **Triggers** — every `Edit`/`Write` that adds, removes, or changes a log emit:
128
128
 
129
129
  - `*.zig` outside `vendor/`/`third_party/`/`.zig-cache/`/`*_test.zig` — `std.log.*`, `std.debug.print`, raw stderr writes, calls into the `log` named module (source: `src/logging/mod.zig`).
130
- - `*.rs` under `rustd/` outside `tests/`, `benches/`, and `#[cfg(test)]` items — `tracing::*`, `println!`, `eprintln!`, and `dbg!`.
130
+ - Every tracked or unignored `*.rs` outside `build.rs`, `examples/`, `tests/`, `benches/`, and `#[cfg(test)]` items — `tracing::*`, `println!`, `eprintln!`, and `dbg!`.
131
131
  - `*.ts`/`*.tsx`/`*.js`/`*.jsx` outside `vendor/`/`node_modules/`/`*.test.*`/`*.spec.*` — `console.*`, custom logger calls.
132
132
  - `*.sh` outside generated dirs — `echo`/`printf` to `&2`.
133
133
 
@@ -150,7 +150,8 @@ shebang and is the authoritative check.
150
150
  | New `err`/`warn` log mapping to a domain failure | registry-scheme `error_code=` field required (agentsfleet: `UZ-XXX-NNN`). Registry entry must land in same commit. |
151
151
  | Per-iteration / hot-loop log | Use `debug` (hidden by default), not `info`. |
152
152
  | `info` level | No allow-list — `info` is open (§4 rule 2). Two checks instead: a boundary-crossing operation needs its `_started`/`_completed`\|`_failed` pair on every exit path (rule 1), and a per-iteration path is `debug` (rule 3). Hot-poll boundary pairs remain complete but use `debug`. |
153
- | `console.log`/`std.debug.print`/Rust print macro in non-test source | Forbidden. Convert to the structured logger or delete before commit. |
153
+ | `console.log` in non-test source | Forbidden. Convert to the structured logger or delete before commit. |
154
+ | `std.debug.print` or a Rust print macro in runtime source | Convert to structured logging. When the stream output is the program interface, add `// logging: <reason>` on or immediately above the emit. The reason is mandatory. |
154
155
  | `std.log.scoped` outside `src/logging/` | Migration target. The `log` named module's `scoped` API is the only non-test entry point; flip the file's alias and migrate every call site in the same commit. |
155
156
  | `msg=` field | ≤ 300 chars. Stack traces emit as separate `event=stack_trace` debug record. |
156
157
  | Multi-line values | Newlines must be `\n` literal (two chars), not raw newline byte. |
@@ -190,13 +191,13 @@ LOGGING GATE: <file>
190
191
 
191
192
  > [DETERMINISTIC → LOG]
192
193
 
193
- `logging.sh` walks the **full `src/` + `rustd/` + `agentsfleet/src/` working tree**. The staged mode reads the index, so a fix staged in pre-commit satisfies the check on the same hook run. `--staged` is preserved as an opt-in narrowing mode for iterative dev.
194
+ `logging.sh` walks `src/**/*.zig`, every tracked or unignored `*.rs`, and `agentsfleet/src/**/*.{js,jsx,ts,tsx}`. Rust repositories do not need to use a fixed source-root name. The staged mode reads the index, so a fix staged in pre-commit satisfies the check on the same hook run. `--staged` is preserved as an opt-in narrowing mode for iterative development.
194
195
 
195
196
  #### End-of-turn audit
196
197
 
197
198
  > [DETERMINISTIC → LOG]
198
199
 
199
- `audits/logging.sh` runs as part of `make lint`. It greps `std.log.*`, Rust `tracing` and direct-print macros, `console.*`, and raw stderr writes. It does NOT see calls through the Zig `log` named module's `scoped()`, so a clean run is not evidence that §4/§5 hold everywhere. Mechanical today: direct Zig/Rust/TypeScript diagnostics, Rust's required `event` field and positional-format ban, `std.log.scoped` outside `src/lib/logging/`, and legacy Zig error-code hints. Reviewer-side: severity choice, entry/exit pairing, registry applicability, inline Rust expression hoisting, and Personally Identifiable Information (PII) spot-checks.
200
+ `audits/logging.sh` runs as part of `make lint`. It greps `std.log.*`, Rust `tracing` and direct-print macros, `console.*`, and raw stderr writes. It accepts a direct Zig or Rust stream write only when `// logging: <reason>` appears on or immediately above the emit with a non-empty reason. It does NOT see calls through the Zig `log` named module's `scoped()`, so a clean run is not evidence that §4/§5 hold everywhere. Mechanical today: direct Zig/Rust/TypeScript diagnostics, Rust's required `event` field and positional-format ban, `std.log.scoped` outside `src/lib/logging/`, and legacy Zig error-code hints. Reviewer-side: reason validity, severity choice, entry/exit pairing, registry applicability, inline Rust expression hoisting, and Personally Identifiable Information (PII) spot-checks.
200
201
 
201
202
  #### Family
202
203
 
@@ -22,5 +22,13 @@ a working value.
22
22
  Table-driven tests are the default shape. Concurrency work needs a `-race` run,
23
23
  and a deterministic contention test rather than a sleep.
24
24
 
25
+ A string literal repeated in one file becomes a named constant — `audits/ufs.sh`
26
+ reads `*.go` and enforces it. Two Go shapes are held out, because no rename can
27
+ fix them: members of a `const ( … )` group, which state the binding keyword once
28
+ on the opening line rather than per member, and backtick struct tags, whose
29
+ quoted halves (`json:"id"`) are addressed by reflection and cannot hold a const.
30
+ Both are carve-outs for what the *language* makes unfixable, not permission —
31
+ a flag name or a label spelled inline twice is still the rule.
32
+
25
33
  The repository owns formatting, vet, lint, build, and test commands — it
26
34
  declares them in `.oracle/orly.json`.
@@ -33,6 +33,172 @@ Local convention wins on conflict, but the divergence is named in the review
33
33
  output. Refresh the copy when the user asks or a cited rule looks stale
34
34
  (re-run the fetch; upstream updates continuously).
35
35
 
36
+ ## Error discipline (RULE ERR-RS)
37
+
38
+ > [DETERMINISTIC → ERR-RS]
39
+
40
+ Two of the clauses below are decided by `audits/rust-error.sh`, which
41
+ `dispatch/write_rust.sh` runs over staged Rust: an error type with no `Result`
42
+ alias in its crate, and a `map_err` that stringifies its own cause. The other
43
+ two — whether a `source()` implementation returns its own kind, and whether a
44
+ context addition is warranted — need the call site read beside the diff and stay
45
+ the agent's call at write time.
46
+
47
+ One error type per crate, and a `Result` alias beside it:
48
+
49
+ pub type Result<T, E = Error> = core::result::Result<T, E>;
50
+
51
+ Every fallible function in the crate returns `Result<T>`. The default type
52
+ parameter is what lets the few functions answering with a foreign error keep
53
+ the same spelling — `Result<T, VerifyError>` — instead of reaching for
54
+ `std::result::Result`. A reader must never have to check WHICH error a
55
+ signature returns to know it is this crate's.
56
+
57
+ **Compose with `From`; `?` does the lifting.** This is `M-FROM-ERROR` and is
58
+ read there, carve-out included; what the guideline leaves open is how the
59
+ variant is spelled, which locally is `#[from]`:
60
+
61
+ #[derive(Debug, thiserror::Error)]
62
+ pub enum BootFailure {
63
+ #[error(transparent)]
64
+ Environment(#[from] Refusal),
65
+ #[error("cannot boot: the database would not answer")]
66
+ Database(#[from] db::Error),
67
+ }
68
+
69
+ **`map_err` only to ADD context the call site alone knows** — a role tag, a
70
+ budget, an operation name; the same carve-out `M-FROM-ERROR` names. And never
71
+ this, which no guideline covers:
72
+
73
+ // WRONG: a lossy conversion wearing a conversion's clothes.
74
+ .map_err(|error| Mine::Database(error.to_string()))?
75
+
76
+ `to_string()` on the way INTO an error type destroys the `source()` chain. It
77
+ compiles, it reads fine, and it silently defeats every chain walker downstream.
78
+
79
+ **`source()` returns what caused you, never yourself.** An error whose
80
+ `Display` already renders its kind must not also return that kind as its
81
+ source, or a chain walker prints the same sentence twice before reaching
82
+ anything new:
83
+
84
+ // WRONG — Display is "[code] {kind}", so the kind is already printed.
85
+ fn source(&self) -> Option<&(dyn Error + 'static)> { Some(&self.kind) }
86
+
87
+ // RIGHT — skip ourselves, hand back what the kind wraps.
88
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
89
+ std::error::Error::source(&self.kind)
90
+ }
91
+
92
+ **Not every error has a cause, and that is not a defect.** A variant holding
93
+ another error has one; a variant holding only data (`MissingUrl { knob }`) does
94
+ not — nothing *caused* an unset variable. A test asserting `source().is_some()`
95
+ for every variant is WRONG and forces authors to invent causes. Assert the real
96
+ invariant: where there IS a source, it is not a repeat of our own message.
97
+
98
+ **Divergence from the reference guideline, named as that section requires.**
99
+ `M-ERRORS-CANONICAL-STRUCTS` prescribes a situation-specific `struct` carrying
100
+ a `Backtrace` and lets a complex crate expose several error types;
101
+ `M-APP-ERROR` further lets an application crate skip its own type for
102
+ `anyhow`/`eyre`/`ohno::AppError`. This rule diverges on all three for one
103
+ reason: a `#[from]`-composed enum behind a single per-crate alias is what lets
104
+ `?` lift across a crate boundary with no call-site conversion, and an
105
+ application-level catch-all erases the variant the caller would have matched
106
+ on. A `Backtrace` inside a variant is welcome. A crate with two genuinely
107
+ unrelated fallible surfaces may carry a second type — with its own alias, under
108
+ these same rules. Cite this divergence in review rather than re-deriving it.
109
+
110
+ Prior art, not invention: `core_api` (multi-crate workspace, per-crate
111
+ `error.rs` with `pub type Result<T>` + flat `Error`, in production for years),
112
+ bun (`thiserror`, `#[from]`, `pub type Result<T, E = Error>`), habitat (one
113
+ payload-carrying `enum Error`, one alias).
114
+
115
+ ## Functional design (RULE FN-RS)
116
+
117
+ Design the fallible path as a Result PIPELINE, never as a control-flow tree.
118
+
119
+ **Fallibility lives in the type, never in a sentinel.** A function that can
120
+ fail returns `Result<T>`; a value that can be absent returns `Option<T>`.
121
+ Never a bool-plus-out-parameter, never a magic value (-1, empty string, null
122
+ UUID), never a logged-and-swallowed error. If the caller cannot see the
123
+ failure in the signature, the design is wrong before the body is written.
124
+
125
+ **Compose with combinators; spend `match` only where arms genuinely differ.**
126
+ `?`, `map`, `and_then`, `ok_or_else`, `unwrap_or_else` say "transform on
127
+ success, pass failure through" in one line, where a nested match spends five
128
+ and gives every arm a chance to swallow the `Err`:
129
+
130
+ // WRONG: the failure path is hand-plumbed, twice.
131
+ let config = match load(path) {
132
+ Ok(raw) => match parse(&raw) {
133
+ Ok(c) => c,
134
+ Err(e) => return Err(e.into()),
135
+ },
136
+ Err(e) => return Err(e.into()),
137
+ };
138
+
139
+ // RIGHT: one pipeline; `?` and `From` (RULE ERR-RS) do the lifting.
140
+ let config = parse(&load(path)?)?;
141
+
142
+ A `match` earns its place when the arms carry different LOGIC — not when one
143
+ arm is a hand-written `?`.
144
+
145
+ **Make illegal states unrepresentable; parse, don't validate.** Convert raw
146
+ input into a type that can only hold valid states ONCE, at the boundary, and
147
+ pass that type inward. Two `Option` fields where exactly one is ever set is an
148
+ enum. A `state: String` compared against literals is an enum. A checked-then-
149
+ used raw value is a newtype whose constructor returns `Result` — that
150
+ constructor is `M-STRONG-TYPES-GUARD`, and picking the type family is
151
+ `M-STRONG-TYPES`. What those do not say is the consequence: interior code that
152
+ trusts its types needs no defensive re-checks — delete them.
153
+
154
+ **Expression-orientation: bind once.** `let x = if ... { a } else { b };` and
155
+ `let x = match ...` — never a mutable default that later branches overwrite.
156
+ A `mut` that exists only to simulate an expression is a bug surface with no
157
+ compensating power.
158
+
159
+ **`unwrap()` / `expect()` are for tests and for invariants already proved.**
160
+ `M-PANIC-ON-BUG` settles when a panic is right and `M-PANIC-MESSAGE` that it
161
+ must be helpful; the addition here is WHAT the message carries — the proof
162
+ ("mutex never poisoned: no thread panics while holding it"). In production
163
+ code, an `unwrap()` without a written invariant is a `Result` that was never
164
+ designed.
165
+
166
+ Prior art, not invention: `core_api` — a multi-crate production workspace
167
+ where every crate with a fallible surface carries its own `error.rs` +
168
+ `Result` alias, and the fallible path is written as pipelines throughout
169
+ (573 `.map`, 463 `.and_then`, 164 `.ok_or_else` across the workspace). It has
170
+ run in production for years; its author attributes the absence of error-path
171
+ defects to exactly this design, and the maintenance record bears that out.
172
+
173
+ ## Constant discipline (RULE UFS)
174
+
175
+ `audits/ufs.sh` reads `*.rs`. It did not until this pack said so, and the cost
176
+ was concrete: a crate spelled a wire verb inline twice — `pong == "PONG" ||
177
+ pong.contains("PONG")` — while `write_any.sh` printed a green UFS row over the
178
+ file, because the façade had always declared `*.rs` and the leaf had never read
179
+ it. Bind the repeated literal to a `const` or `static` and use the name.
180
+
181
+ Four Rust shapes are held out of the count, because no rename can fix them:
182
+
183
+ - **`#[cfg(test)]` and `#[test]` blocks.** Rust keeps unit tests inside the file
184
+ they cover, so fixture keys would otherwise read as that file's worst debt.
185
+ - **Feature-gated test seams** — `#[cfg(feature = "test-util")]`. A helper that
186
+ hands a sibling integration test one of each error kind has to compile into
187
+ the crate, which is the only reason it is not behind `cfg(test)`; its contents
188
+ are fixture data all the same. The match is narrow on purpose: `test` as a
189
+ whole word or a `-`/`_` segment, so `#[cfg(feature = "redis")]` still counts.
190
+ - **Attribute literals** — `#[serde(rename = "…")]`, `#[cfg(feature = "…")]`,
191
+ `#[doc = "…"]`. An attribute takes a literal token by language rule; a const
192
+ is not accepted in that position.
193
+ - **`tests/` and `benches/`** at the crate root, which are fixture trees.
194
+
195
+ Everything else counts, including `examples/` — not because it compiles, but
196
+ because what repeats there is coupling, not demonstration. A cache namespace
197
+ declared at the top of a walkthrough and matched ninety lines below it, or a
198
+ fixture id that is both the fake store's match arm and the key five call sites
199
+ pass, breaks silently when one end moves. That is the drift RULE UFS exists to
200
+ catch, and example code is where a reader copies it from.
201
+
36
202
  ## Evolution
37
203
 
38
204
  This pack is young next to the Zig façade and grows the same way: a Rust
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # dispatch/write_rust.sh — the Rust dispatch, DETERMINISTIC façade.
3
+ #
4
+ # Pairs with dispatch/write_rust.md (the LATENT façade — prose the agent reads).
5
+ # This .sh runs the mechanically-checkable subset of the Rust discipline and
6
+ # emits ONE verdict block. The latent trigger ("about to write a *.rs file")
7
+ # dispatches here; everything below is deterministic and re-runnable.
8
+ #
9
+ # dispatch/write_rust.sh <src/file.rs> [...] # explicit targets (EXECUTE)
10
+ # dispatch/write_rust.sh --staged # staged *.rs (CONFORM, pre-commit)
11
+ #
12
+ # Layering: AGENTS.md → write_rust.md (latent) → write_rust.sh (this) → audits/*.sh
13
+ # Signals: 🟢 pass · 🔴 fail (blocks) · 🤔 DECIDE (judgment; blocks the TURN, not the script) · 🟣 delegated
14
+ # Exit: 0 = mechanical gates pass · 1 = ≥1 failed · 2 = usage error.
15
+ #
16
+ # Note: Rust had no deterministic façade at all until this file, which is why
17
+ # the error standard the operating model calls mandatory was enforced by review
18
+ # alone. ERR-RS carries the two clauses a machine can decide from the text — a
19
+ # map_err that stringifies its own cause, and an error type with no Result alias
20
+ # beside it. The rest of write_rust.md stays judgment on purpose: ownership,
21
+ # justified `unsafe`, feature combinations, and whether a source() implementation
22
+ # returns its own kind all need a call site read beside the diff, and a gate that
23
+ # guesses at those is a gate people learn to ignore.
24
+ #
25
+ # UFS over *.rs is NOT re-run here. It is wired once in its home façade
26
+ # (write_any), and the universal-code carve-out makes that row satisfy the tag
27
+ # wherever it appears — running it twice would report one violation as two.
28
+
29
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
30
+
31
+ dispatch_init "RUST" '*.rs'
32
+ dispatch_resolve_files "$@"
33
+ dispatch_header
34
+
35
+ # ── deterministic gates ────────────────────────────────────────────
36
+ dispatch_run_helper "ERR-RS" rust-error.sh --staged
37
+
38
+ dispatch_verdict
@@ -15,7 +15,7 @@ Every triggered document requires one `📖 DOC READ: <path>` proof-line **per t
15
15
  | `*.zig` | `dispatch/write_zig.md`. ZIG GATE per edit. |
16
16
  | `*.ts`/`*.tsx`/`*.js`/`*.jsx` | `dispatch/write_ts_adhere_bun.md` — TS FILE SHAPE DECISION (§1) at PLAN, const/import/Bun-primitive discipline, anti-patterns. |
17
17
  | `*.rs` | `dispatch/write_rust.md` — ownership, error variants, feature combinations, and contention tests. | <!-- oracle-packs:language.rust -->
18
- | Rust error type, `Result` alias, or any fallible signature | `dispatch/write_rust.md` §Error discipline — one error per crate, one `Result` alias, `#[from]` composition, `source()` never returns self. | <!-- oracle-packs:language.rust -->
18
+ | Rust error type, `Result` alias, or any fallible signature | `dispatch/write_rust.md` §Error discipline — one error per crate, one `Result` alias, `#[from]` composition, `source()` never returns self. ERR-RS fires per edit: `audits/rust-error.sh` (via `dispatch/write_rust.sh`) blocks a `map_err` that stringifies its own cause and an error type with no alias in its crate; the remaining clauses stay judgment. | <!-- oracle-packs:language.rust -->
19
19
  | Designing a Rust function, type, or module; writing `match` on a `Result`/`Option`; any production `unwrap()` | `dispatch/write_rust.md` §Functional design — Result pipelines over control-flow trees, illegal states unrepresentable, bind once, `unwrap` needs a proved invariant. | <!-- oracle-packs:language.rust -->
20
20
  | `*.py` | `dispatch/write_python.md` — parsing boundaries, resource ownership, and specific exceptions. |
21
21
  | `*.sh` | `dispatch/write_shell.md` — quoting, array arguments, cleanup, input safety, and shell compatibility. |
@@ -250,7 +250,7 @@ The `comptime if ... return` makes the entire emit path disappear when the scope
250
250
  | Pattern | Why banned | Fix |
251
251
  |---|---|---|
252
252
  | `std.log.info("..."` outside `logging.scoped(.tag)` | Bypasses scope/event/field discipline. | Convert to `logging.scoped(.tag).info("event", .{...})`. |
253
- | `std.debug.print(` in non-test source | No level, no scope, no field structure. Dev-only debugging that escaped to main. | Convert or delete before commit. |
253
+ | `std.debug.print(` in non-test source | No level, no scope, no field structure. Dev-only debugging that escaped to main. | Convert or delete. If stdout or stderr is the program interface, add `// logging: <reason>` on or immediately above the emit. |
254
254
  | `std.log.err` without `error_code=` field | Drops registry traceability. | Add the registry code, or add a registry entry if missing. |
255
255
  | `std.log.scoped(...)` outside `src/lib/logging/` | Free-form printf-style emit. | Switch the file's alias to `const log = logging.scoped(.tag);` and migrate every call site to the event+fields form. |
256
256
  | Positional `{s} {d}` placeholders | Not logfmt; not greppable. | Convert to struct-of-fields. |
@@ -295,7 +295,7 @@ class AgentError extends Error {
295
295
 
296
296
  Throw-style modules `throw new AgentError({...})`. Result-style modules return `{ ok: false, error: new AgentError({...}) }`. Render path picks human or JSON based on the runtime mode.
297
297
 
298
- ## §8A · Per-language binding — Rust (`rustd`)
298
+ ## §8A · Per-language binding — Rust
299
299
 
300
300
  > [DETERMINISTIC → LOG]
301
301
 
@@ -333,10 +333,20 @@ match. Renaming is a separate, intentional observability migration.
333
333
 
334
334
  | Pattern | Why banned | Fix |
335
335
  |---|---|---|
336
- | `println!`, `eprintln!`, or `dbg!` in non-test source | No level, event, or structured fields. | Convert to a `tracing` emit or delete. |
336
+ | `println!`, `eprintln!`, or `dbg!` in runtime source | No level, event, or structured fields. | Convert to a `tracing` emit or delete. If the stream is the program interface, add `// logging: <reason>` on or immediately above the emit. |
337
337
  | `tracing::<level>!` without `event = ...` | Drops the stable §3 event key. | Add the byte-stable snake_case event field. |
338
338
  | Positional formatting in a `tracing` message | Hides values inside text and defeats field queries. | Hoist values and emit named fields. |
339
339
 
340
+ The annotation reason is mandatory and must be non-empty. A bare `// logging:`
341
+ marker still fails. The annotation is for direct program output, such as a
342
+ command result or a fatal renderer that must work before tracing starts. It does
343
+ not excuse an incomplete `tracing` emit.
344
+
345
+ ```rust
346
+ // logging: stdout is this command's answer, not a log record
347
+ println!("{}", rendered_summary);
348
+ ```
349
+
340
350
  ## §9 · Pretty-printer (dev-only render variant)
341
351
 
342
352
  In production: every record on the wire is logfmt. No exceptions.
@@ -379,7 +389,7 @@ Failure modes the audit script and reviewer must close. These are **not aspirati
379
389
 
380
390
  | # | Rationalization | Closure |
381
391
  |---|---|---|
382
- | L1 | "Temporary debug print, I'll remove later" | `logging.sh` greps `std.debug.print`, Rust `println!` / `eprintln!` / `dbg!`, and `console.log` / `console.debug` / `console.info` in non-test source unconditionally. Found in commit → gate fails. No "temporary" carve-out. |
392
+ | L1 | "Temporary debug print, I'll remove later" | `logging.sh` rejects `std.debug.print`, Rust `println!` / `eprintln!` / `dbg!`, and `console.log` / `console.debug` / `console.info` in runtime source. Direct Zig or Rust stream output needs `// logging: <reason>` on or immediately above the emit. The reason must be non-empty and explain why the stream is the program interface. |
383
393
  | L2 | "`std.log.scoped` is fine, `obs.scoped` is just a wrapper" | `std.log.scoped` is **forbidden** in `src/**/*.zig` outside `src/lib/logging/`. Only `obs.scoped` is callable. Audit flags every `std.log.` call site. |
384
394
  | L3 | "I added `error_code=UZ-NEW-001` — registry entry coming next commit" | The registry entry **must land in the same commit** as the first reference. `error-codes.sh` runs against the staged diff; missing entry = blocking. |
385
395
  | L4 | "This per-iteration event matters for debugging — `info`-level" | Per-iteration and per-row paths are `debug`, including mandatory boundary pairs on hot polls. `debug` is the level with a volume switch. This closes the ONLY `info` prohibition — there is no allow-list of event names and none may be reintroduced (§4). The inverse rationalization is closed too: "this operation is obviously fine, no need to log it" does not survive §4 rule 1, which requires a `_started`/`_completed`\|`_failed` pair on every boundary-crossing operation. Reviewer checks the pair and the loop, never the spelling. |
@@ -6,15 +6,15 @@ regenerates this file and fails on any difference. To update it, run
6
6
 
7
7
  | Rule document | enforced by | judged | acknowledged | classified | trigger |
8
8
  |---|---|--:|--:|--:|---|
9
- | `docs/LOGGING_STANDARD.md` | `LOG` → `logging.sh` | 11 | 13 | 28/28 | mechanical |
9
+ | `docs/LOGGING_STANDARD.md` | `LOG` → `logging.sh` | 11 | 13 | 30/30 | mechanical |
10
10
  | `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/74 | mechanical |
11
11
  | `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/16 | mechanical |
12
12
  | `docs/DOCUMENTATION_RULES.md` | — | 0 | 0 | 0/22 | latent |
13
13
  | `docs/LIFECYCLE_PATTERNS.md` | — | 0 | 0 | 0/22 | mechanical |
14
14
  | `docs/CHANGELOG_VOICE.md` | — | 0 | 0 | 0/3 | latent |
15
- | `docs/VERIFY_TIERS.md` | — | 0 | 0 | 0/4 | uncited |
15
+ | `docs/VERIFY_TIERS.md` | — | 0 | 0 | 0/7 | uncited |
16
16
  | `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/114 | mechanical |
17
- | **corpus** | | | | **28/283** | |
17
+ | **corpus** | | | | **30/288** | |
18
18
 
19
19
  ## How to read this
20
20
 
@@ -8,16 +8,33 @@ page carries only what is specific to this repository.
8
8
 
9
9
  | Tier | Command | When |
10
10
  |---|---|---|
11
- | conform | `make harness-verify` | Always, after EXECUTE and before the rest. Any 🔴 returns to EXECUTE. |
12
- | lint | `make lint-all` | Always. Rust rides `lint-rustd` (rustfmt + Clippy, warnings are errors); script self-tests ride `lint-scripts`. |
13
- | unit | `make test-unit-all` | Always. The cargo workspace plus every TypeScript package coverage gate. |
14
- | version | `make check-version` | Always. `VERSION` against `build.zig.zon`, `cli/package.json` and both `rustd/Cargo.toml` sites. |
15
-
16
- These four are exactly what `.oracle/orly.json` declares and exactly what
17
- `orly gate` runs, so the rubric and the mechanical gate grade one boundary.
18
-
19
- There is no slow tier. The Zig integration and memory-leak lanes retired with the
20
- rest of the Zig gating, so no lane a developer runs needs live Postgres or Redis.
11
+ | conform | `make harness-verify` | Section lane and boundary, after EXECUTE and before the rest. Any 🔴 returns to EXECUTE. |
12
+ | lint | `make lint-all` | Boundary. Rust rides `lint-rustd` (rustfmt + Clippy, warnings are errors); script self-tests ride `lint-scripts`. |
13
+ | unit | `make test-unit-all` | Boundary. The cargo workspace plus every TypeScript package coverage gate. |
14
+ | version | `make check-version` | Boundary. `VERSION` against `build.zig.zon`, `cli/package.json` and both `rustd/Cargo.toml` sites. |
15
+ | integration | `make test-integration-rustd` | Boundary. Live Postgres and Redis via docker compose, schemas reset per run. `orly gate pr` skips it on a branch carrying no code. |
16
+
17
+ These five are exactly what `.oracle/orly.json` declares and exactly what
18
+ `orly gate pr` runs, so the rubric and the mechanical gate grade one boundary.
19
+
20
+ **Two cadences.** A Section closing is not a milestone closing. Inside a
21
+ Section, run `make harness-verify` plus the lane covering the surface the
22
+ Section touched (`make test-unit-rustd`, `make test-unit-app`, …) — enough to
23
+ prove the Section, never a repository claim. The full table above runs ONCE, at
24
+ CHORE(close).
25
+
26
+ **Which gate runs which row.** `orly gate work` runs the `conform` row alone and
27
+ is what the pre-commit hook fires, so it costs seconds. `orly gate verify` runs
28
+ the lint, unit, and version rows and is what pre-push fires. `orly gate pr` runs
29
+ the integration row plus the whole-branch and spec criteria, and is what
30
+ CHORE(close) fires — it skips the fast rows because `git.pushed` proves HEAD is
31
+ the commit pre-push already graded.
32
+
33
+ One lane needs live datastores, and only one. `make/test-infra.mk` brings up
34
+ docker compose Postgres and Redis for `test-integration-rustd`; `make
35
+ test-unit-all` stays datastore-free, because every Rust test needing one is
36
+ `#[ignore]`d and runs only in that lane. `KEEP_TEST_STATE=1` skips the reset for
37
+ the inner loop; Continuous Integration (CI) never sets it.
21
38
 
22
39
  ## Test Baseline
23
40
 
@@ -61,7 +78,14 @@ Paste the deciding line, not the exit code. A gate that reports a number — a s
61
78
  against a cap, a count against a budget — is owed that number in the done message.
62
79
 
63
80
  ```
64
- ✅ Verified: 🧪 lint-all ✓ · 🧪 test-unit-all ✓ <N>p/<M>s · 🔆 harness-verify ✓ · 🔆 check-version ✓
81
+ ✅ Verified: 🧪 lint-all ✓ · 🧪 test-unit-all ✓ <N>p/<M>s · 🧩 test-integration-rustd ✓ · 🔆 harness-verify ✓ · 🔆 check-version ✓
82
+ ```
83
+
84
+ A Section lane reports what it ran and says so, never borrowing the repository's
85
+ verdict:
86
+
87
+ ```
88
+ ✅ Section verified: 🔆 harness-verify ✓ · 🧪 test-unit-<surface> ✓ <N>p/<M>s
65
89
  ```
66
90
 
67
91
  A skipped target is surfaced as 🟠 with its reason, never dressed as a pass.
@@ -51,6 +51,7 @@ every code a dispatch `.sh` emits resolves to exactly one row here.
51
51
  | LOG | LOGging discipline (scoped event, error_code, severity, redaction) |
52
52
  | MSID | Milestone-ID ban in source (M{N}_{NNN} / §x.y / T{N} / dim) |
53
53
  | ERR | ERror Registry (UZ-XXX-NNN declared + referenced) |
54
+ | ERR-RS | Rust ERror discipline (one type per crate, Result alias, no stringified cause) |
54
55
  | GRP | GREptile rule audit (diff vs greptile-learnings/RULES.md codes) |
55
56
  | LDC | Legacy-Design Consult (A remove / B patch / C keep) |
56
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary \u2014 for any coding agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,15 @@ output. Refresh the copy when the user asks or a cited rule looks stale
35
35
 
36
36
  ## Error discipline (RULE ERR-RS)
37
37
 
38
+ > [DETERMINISTIC → ERR-RS]
39
+
40
+ Two of the clauses below are decided by `audits/rust-error.sh`, which
41
+ `dispatch/write_rust.sh` runs over staged Rust: an error type with no `Result`
42
+ alias in its crate, and a `map_err` that stringifies its own cause. The other
43
+ two — whether a `source()` implementation returns its own kind, and whether a
44
+ context addition is warranted — need the call site read beside the diff and stay
45
+ the agent's call at write time.
46
+
38
47
  One error type per crate, and a `Result` alias beside it:
39
48
 
40
49
  pub type Result<T, E = Error> = core::result::Result<T, E>;