@agentsfleet/orly 0.7.1 → 0.9.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.
@@ -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. |
@@ -12,9 +12,9 @@ regenerates this file and fails on any difference. To update it, run
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** | | | | **30/285** | |
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.1",
3
+ "version": "0.9.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>;
package/registry.json CHANGED
@@ -36,6 +36,14 @@
36
36
  "source": "dispatch/write_pr_description.md",
37
37
  "target": "dispatch/write_pr_description.md"
38
38
  },
39
+ {
40
+ "source": "audits/doc-read.sh",
41
+ "target": "audits/doc-read.sh"
42
+ },
43
+ {
44
+ "source": "audits/rule-ledger-lib.sh",
45
+ "target": "audits/rule-ledger-lib.sh"
46
+ },
39
47
  {
40
48
  "source": "audits/ufs.sh",
41
49
  "target": "audits/ufs.sh"
@@ -118,6 +126,14 @@
118
126
  ".rs"
119
127
  ],
120
128
  "managed_files": [
129
+ {
130
+ "source": "dispatch/write_rust.sh",
131
+ "target": "dispatch/write_rust.sh"
132
+ },
133
+ {
134
+ "source": "audits/rust-error.sh",
135
+ "target": "audits/rust-error.sh"
136
+ },
121
137
  {
122
138
  "source": "packs/language/rust/rules.md",
123
139
  "target": "dispatch/write_rust.md"
@@ -1,14 +1,10 @@
1
1
  ---
2
2
  name: orly-babysit-prs
3
3
  description: |
4
- Post-push follow-up for PRs/MRs: backoff-cadence polls over CI check runs
5
- (fix every failure your diff caused), every greptile inline thread, and the
6
- PR-level summary; classifies via gstack greptile-triage, suppresses via
7
- greptile-history, stops after two consecutive empty polls with CI green.
8
- Forge-aware (gh PR / glab MR). Use after `gh pr create` / `glab mr create`,
9
- after every push to a branch with an open PR/MR, or on "babysit", "watch
10
- greptile", "poll the PR/MR", "watch reviews", "follow up on review
11
- feedback". Cross-agent: Claude, Codex, OpenCode, Amp.
4
+ Post-push follow-up for a Pull Request or Merge Request: poll Continuous
5
+ Integration (CI) checks, greptile threads, and the summary; fix what your diff
6
+ broke; stop after two empty polls with CI green. Forge-aware (gh, glab). Use
7
+ after creating or pushing to a PR/MR, or on "babysit", "watch reviews".
12
8
  ---
13
9
 
14
10
  # orly-babysit-prs
@@ -1,12 +1,10 @@
1
1
  ---
2
2
  name: orly-spec-new
3
3
  description: |
4
- Author a milestone/workstream spec the executing agent (Orly) ships into a
5
- deterministic, review-clean, reported Pull Request — intent capture and
6
- review-readiness first, file mechanics last. Use on "create a spec", "new
7
- milestone", "start M{N}_{NNN}", "spec out X", or any TODO.md attempt
8
- (forbidden — every non-trivial intent becomes a spec). Cross-agent,
9
- self-contained markdown.
4
+ Author a milestone or workstream spec that ships as a deterministic,
5
+ review-clean Pull Request — intent capture first, file mechanics last. Use on
6
+ "create a spec", "new milestone", "spec out X", "start M{N}_{NNN}", or any
7
+ TODO.md attempt (forbidden — every non-trivial intent becomes a spec).
10
8
  ---
11
9
 
12
10
  # orly-spec-new
@@ -1,12 +1,10 @@
1
1
  ---
2
2
  name: orly-write-integration-test
3
3
  description: >
4
- Service-layer integration tests against real dependencies (Postgres, Redis,
5
- full HTTP router + middleware) with deterministic failure injection,
6
- per-test isolation, drain + leak audits, and 100-or-more-connection parallelism
7
- proofs. Sister of orly-write-unit-test. Use for handlers, repos, services, or
8
- any real-I/O module-boundary crossing. Not for browser E2E (gstack /qa) or
9
- pure logic (orly-write-unit-test).
4
+ Service-layer integration tests against real dependencies — Postgres, Redis,
5
+ full HTTP router and middleware — with failure injection, per-test isolation,
6
+ and leak audits. Use for handlers, repositories, services, or any real
7
+ input/output module boundary; not for browser end-to-end or pure logic.
10
8
  ---
11
9
 
12
10
  # Write Integration Test
@@ -1,12 +1,10 @@
1
1
  ---
2
2
  name: orly-write-unit-test
3
3
  description: >
4
- Risk-weighted, failure-injecting test generation across 8 stacks (Python,
5
- Python SDK, OpenAPI, JS/TS CLI, React/TS, Zig, Rust, Shell) enforcing behaviour +
6
- failure + invariant + integration + regression coverage with a
7
- Definition-of-Done gate; deterministic production-safety proofs (Zig
8
- zero-leak, 100-or-more-connection concurrency, complexity/latency budgets) live
9
- in the skill body. Use during implementation, not after.
4
+ Risk-weighted, failure-injecting unit tests across Python, TypeScript, React,
5
+ Zig, Rust, and shell: behaviour, failure, invariant, and regression coverage
6
+ behind a Definition-of-Done gate. Production-safety proofs live in the body.
7
+ Use during implementation, not after.
10
8
  ---
11
9
 
12
10
  # Write Unit Test
package/src/cli.ts CHANGED
@@ -293,6 +293,12 @@ function printHelp(): void {
293
293
  Gates (read-only; no PR without every criterion green or a recorded override):
294
294
  orly gate [--accept-dirty] run work → verify → pr; stop at first red
295
295
  orly gate <work|verify|pr> run one gate
296
+ work does this commit conform? the declared conform command; no git
297
+ state, so a commit hook's own dirty tree never blocks it
298
+ verify does the work hold up? spec dimensions, docs language, and the
299
+ fast verify.* set
300
+ pr can this ship? branch, tree, pushed, every spec criterion, and
301
+ the slow verify.* suites
296
302
  orly override <CRITERION> --reason <REASON>
297
303
  empty commit with an Orly-Override trailer
298
304
 
@@ -302,7 +308,11 @@ Install (the repository is the unit — no checkout of this package required):
302
308
  seeded .oracle/orly.json. A repository that
303
309
  already has an AGENTS.md keeps it: orly's
304
310
  rules land as AGENTS.orly.md, reached by a
305
- pointer block in the file you own.
311
+ pointer block in the file you own. One
312
+ import line lands per runtime that needs
313
+ one (CLAUDE.md, opencode.json), so the
314
+ rules load rather than wait to be noticed;
315
+ a loader you wrote yourself is left alone.
306
316
  orly update [--force] [--with <PACK>] [--dry-run] [--json]
307
317
  re-materialise at the installed engine version
308
318
 
@@ -24,9 +24,12 @@ describe("criteriaFor", () => {
24
24
  return criteriaFor(gate, context).map((criterion) => criterion.name).sort();
25
25
  }
26
26
 
27
- test("work evaluates the branch, the tree and the profile", async () => {
27
+ // work is the commit-time gate, and a commit hook's tree is dirty by
28
+ // construction. It asks the repository's own conform command and nothing
29
+ // about git state.
30
+ test("work evaluates the profile and the conform tier", async () => {
28
31
  const project = newSpecRepository();
29
- expect(named(WORK, await contextFor(project))).toEqual(["git.branch", "git.tree", REPO_CONFIG]);
32
+ expect(named(WORK, await contextFor(project))).toEqual(["cmd.conform", REPO_CONFIG]);
30
33
  });
31
34
 
32
35
  test("verify pairs the spec dimensions with the fast command tier only", async () => {
@@ -34,7 +37,9 @@ describe("criteriaFor", () => {
34
37
  const names = named(VERIFY, await contextFor(project));
35
38
 
36
39
  expect(names).toContain("spec.dimensions");
37
- expect(names).toContain("cmd.conform");
40
+ expect(names).toContain("cmd.verify.unit");
41
+ // conform is the work gate's tier now, so the chain runs it once.
42
+ expect(names).not.toContain("cmd.conform");
38
43
  // The slow tier belongs to the pr gate; leaking it here would make every
39
44
  // verify run pay for integration and memory suites.
40
45
  expect(names).not.toContain("cmd.verify.integration");
@@ -47,9 +52,15 @@ describe("criteriaFor", () => {
47
52
  for (const required of ["spec.moved", "spec.baseline", "spec.ordering", "spec.deferrals", "docs.updated"]) {
48
53
  expect(names).toContain(required);
49
54
  }
50
- // git.branch is a work-gate concern: by pr time the branch is established,
51
- // and re-asserting it would make an already-merged branch un-gateable.
52
- expect(names).not.toContain("git.branch");
55
+ // git.branch belongs here and only here. It was a work-gate criterion until
56
+ // the generated pre-commit hook was found refusing every commit made on the
57
+ // default branch — including the commit that installs orly, and the
58
+ // spec-on-main commit the operating model itself prescribes. The question
59
+ // "is this the default branch" is only ever decisive where a Pull Request
60
+ // would open from it, so it is asked there. Running `orly gate pr` on a
61
+ // merged branch reds, which is the honest answer: no PR opens from main.
62
+ expect(names).toContain("git.branch");
63
+ expect(names).toContain("git.tree");
53
64
  });
54
65
 
55
66
  test("an unknown gate name yields no criteria rather than throwing", async () => {
package/src/criteria.ts CHANGED
@@ -36,6 +36,12 @@ const DOCS_LANGUAGE = "docs.language";
36
36
  // line is a summary, not a linter dump.
37
37
  const NAMED_FINDINGS = 3;
38
38
 
39
+ // CONFORM is its own lifecycle stage — EXECUTE → CONFORM → VERIFY — and it is
40
+ // its own tier here for the same reason. Lumped into verify, the deterministic
41
+ // rule audit could only run beside the lint and unit suites, so a repository
42
+ // paid minutes to learn something its seconds-long conform command already
43
+ // knew, and `orly gate` ran it twice on the way to `pr`.
44
+ const CONFORM_TIER = "conform";
39
45
  const FAST_TIER = "fast";
40
46
  const SLOW_TIER = "slow";
41
47
  // The slow tier is a fixed name set, not a prefix rule: lint and version
@@ -45,14 +51,31 @@ const SLOW_COMMANDS = ["verify.integration", "verify.memory"];
45
51
  // Every criterion is mechanical: it reads an exit code or a file, never a
46
52
  // judgment. The anchor invariant promises the machine can PROVE the PR
47
53
  // boundary, so anything unprovable stays prose and never lands here.
48
- // work = is this branch workable · verify = does the work hold up ·
49
- // pr = can this ship (whole-branch checks + the slow suites).
54
+ //
55
+ // One gate per cadence, and each question asked at the only moment it can be
56
+ // answered honestly:
57
+ //
58
+ // work — does this commit conform? Config plus the declared `conform`
59
+ // command. Nothing about branch or tree: inside a commit hook the
60
+ // tree is dirty BY CONSTRUCTION (that is what is being committed),
61
+ // and the operating model itself says a new spec is committed on the
62
+ // default branch. Judging either here made the generated pre-commit
63
+ // hook refuse every commit — including the one installing orly —
64
+ // with `--no-verify`, which Hard Safety forbids, as the only way out.
65
+ // verify — does the work hold up? Spec dimensions, documentation language,
66
+ // and the fast `verify.*` set. Runs before the work leaves the
67
+ // machine, not after every edit.
68
+ // pr — can this ship? The whole-branch facts (branch shape, clean tree,
69
+ // pushed) plus every spec criterion and the slow suites. A tree is
70
+ // only required clean where a reviewer is about to read it, and a
71
+ // branch is only wrong for being `main` where a PR would open from
72
+ // it.
50
73
  export function criteriaFor(gate: string, context: CriterionContext): Criterion[] {
51
- if (gate === "work") return [gitBranch(), gitTree(), repositoryConfig()];
74
+ if (gate === "work") return [repositoryConfig(), ...commandCriteria(context, CONFORM_TIER)];
52
75
  if (gate === "verify") return [specDimensions(), docsLanguage(), ...commandCriteria(context, FAST_TIER)];
53
76
  if (gate === "pr") {
54
77
  return [
55
- gitTree(), gitPushed(), specGate(), openQuestions(), productClarity(), specDimensions(),
78
+ gitBranch(), gitTree(), gitPushed(), specGate(), openQuestions(), productClarity(), specDimensions(),
56
79
  specMoved(), specBaseline(), specOrdering(), specDeferrals(),
57
80
  docsUpdated(), ...commandCriteria(context, SLOW_TIER),
58
81
  ];
@@ -107,10 +130,11 @@ function repositoryConfig(): Criterion {
107
130
  }
108
131
 
109
132
  // Model C: the repository's declared command surface. Orly owns policy and
110
- // invokes these; the repository owns what they actually do. Fast tier =
111
- // conform + verify.unit. Slow tier = every other verify.* (integration,
112
- // memleak); those auto-pass with a printed skip when the branch carries no
113
- // code, so a prose-only branch never pays for the slow suites.
133
+ // invokes these; the repository owns what they actually do. Conform tier = the
134
+ // `conform` command alone. Fast tier = every verify.* that is not slow. Slow
135
+ // tier = verify.integration and verify.memory; those auto-pass with a printed
136
+ // skip when the branch carries no code, so a prose-only branch never pays for
137
+ // the slow suites.
114
138
  function commandCriteria(context: CriterionContext, tier: string): Criterion[] {
115
139
  const config = resolvedConfig(context);
116
140
  if (!config) return tier === FAST_TIER ? [criterion(REPO_CONFIG, () => ({ ok: false, detail: UNINSTALLED }))] : [];
@@ -125,8 +149,9 @@ function commandCriteria(context: CriterionContext, tier: string): Criterion[] {
125
149
  }
126
150
 
127
151
  function tierOf(key: string): string {
152
+ if (key === CONFORM_COMMAND) return CONFORM_TIER;
128
153
  if (SLOW_COMMANDS.includes(key)) return SLOW_TIER;
129
- if (key === CONFORM_COMMAND || key.startsWith(VERIFY_PREFIX)) return FAST_TIER;
154
+ if (key.startsWith(VERIFY_PREFIX)) return FAST_TIER;
130
155
  return "";
131
156
  }
132
157