@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/AGENTS.md CHANGED
@@ -61,7 +61,7 @@ Auto-memory is **disabled** (`autoMemoryEnabled: false` + `CLAUDE_CODE_DISABLE_A
61
61
  - **Skip hooks/signing.** Never `--no-verify`/`--no-gpg-sign`/`-c commit.gpgsign=false`. Hook fails → fix cause.
62
62
  - **Plaintext secrets in entity tables.** Store vault `key_name` ref, resolve via `crypto_store.load()`.
63
63
  - **Static strings in SQL schema.** No `DEFAULT 'value'`/`CHECK (col IN ('a','b'))`. Enforce in app via named constants.
64
- - **Resolving/printing credentials.** `op read 'op://...'` at runtime — never paste/log.
64
+ - **Resolving/printing credentials.** `op read 'op://...'` at runtime — never paste/log. Vault names come from the environment, never from a rules file.
65
65
  - **Force-push default branch** (`main`/`master`).
66
66
  - **Install-process launches in core paths.** Native SDKs. Exception: personal dev tools (`op`/`gh`/`glab`/`oracle`).
67
67
 
@@ -83,14 +83,14 @@ Auto-memory is **disabled** (`autoMemoryEnabled: false` + `CLAUDE_CODE_DISABLE_A
83
83
  - **Symlinked edits land in the repository that owns the file.** A file whose `readlink` resolves outside this checkout — into a dotfiles or config repository — is an edit to *that* repository, not this one. Detect via `readlink` BEFORE editing. After: `cd` to the resolved repository, `git add <files> && git commit && git push` on its default branch. Never leave uncommitted.
84
84
  - **Docs-repo edits on own branch.** `~/Projects/docs/` is shared across milestones: check `git status` first; from `main` (checkout or worktree off `main`) commit on `chore/m{N}-{slug}-changelog`; recovery = stash, re-apply on a fresh branch.
85
85
  - Other dotfiles (`.zshrc`/`.gitconfig`/etc.): timestamped backup; minimal edits.
86
+ - **Check a sibling repository for an existing pattern in the same language before inventing one.** A shape shipping next door is prior art; a new one needs a reason.
87
+ - **Read the reference implementation before designing.** Where the language's dispatch page names a canonical external source, that read is mandatory in review and its guideline identifiers are cited — applied or diverged from. "Broken for us" means the delta was missed: diff our call site against theirs before blaming the principle.
86
88
  - Before commit/push: `gitleaks` must pass.
87
89
  - No new `make` targets without a distinct caller (CI job, spec-mandated gate, or a workflow existing targets can't express) — check `make/*.mk` first; extend over near-duplicate wrappers.
88
90
  - `*.zig` → read `dispatch/write_zig.md`; ZIG GATE fires.
89
91
  - Auth-flow (token-minting handlers, credential-typed spec dimensions, the repository's auth directories) → read the repository's `docs/AUTH.md` first, where it exists.
90
92
  - `conn.query()` requires `.drain()` in same fn before `deinit()`. Verify `make lint-governance`. Use `conn.exec()` for no-rows.
91
- - **Vault (1Password `op`).** Resolve secrets via the `op` CLI, never hand-paste/log. Vault names come from the environment, never from a rules file.
92
- - Check a sibling repository under `$HOME/Projects/` for an existing pattern in the same language before inventing one.
93
- - **Reference canon (read before designing):** TypeScript → `oss/supabase/apps/studio` + `oss/supabase/packages/{ui,ui-patterns}` + `oss/cli`; Zig → `oss/ghostty/src/`; Rust → `oss/bun/src/` + `oss/exonum` + `oss/core_api-develop` + Microsoft's Rust guidelines (`oss/rust-guidelines/all.txt`, fetch if absent; mandatory in review). Missing checkout → ask, then clone into `~/Projects/oss/`. "Broken for us" → call-site diff first.
93
+ - **Indy's reference checkouts** (the machine-local half of the rule above): TypeScript → `oss/supabase/apps/studio` + `oss/supabase/packages/{ui,ui-patterns}` + `oss/cli`; Zig → `oss/ghostty/src/`; Rust → `oss/bun/src/` + `oss/exonum` + `oss/core_api-develop`. Missing checkout → ask, then clone into `~/Projects/oss/`.
94
94
 
95
95
  **Forge detection:** `github.com` → `gh`; `gitlab.com` → `glab`. Check `git remote -v`.
96
96
 
@@ -180,7 +180,9 @@ Non-trivial (full lifecycle) if it: touches >1 file · new abstraction · data m
180
180
 
181
181
  **With spec:** `CHORE(open) → PLAN → EXECUTE → CONFORM → VERIFY → REVIEW → DOCUMENT → COMMIT → CHORE(close)`. **Without spec** (bug fix/config/refactor): `PLAN → EXECUTE → CONFORM → VERIFY → REVIEW → DOCUMENT → COMMIT`. CHORE bookends iff work creates/continues a spec under `docs/v*/{active,pending}/`. Stage runbooks (checklists, recipes, formats): `dispatch/lifecycle.md`.
182
182
 
183
- **Anchor invariant — `orly gate` proves the boundary.** No PR opens unless every `orly gate pr` criterion is green or carries an `Orly-Override` trailer the user recorded with a reason. `orly gate` runs `work → verify → pr`, stops at the first red group, and only reads; green unlocks CHORE(close) but never performs it. No spec → spec criteria skip, quality gates still run. Slow suites (`verify.integration`, `verify.memory`) run only when the branch carries code. A user-surface change without a docs change is red until docs land or an override says why not.
183
+ **Anchor invariant — `orly gate` proves the boundary.** No PR opens unless every `orly gate pr` criterion is green or carries an `Orly-Override` trailer the user recorded with a reason.
184
+
185
+ **One gate per cadence, each tier run once.** `work` = the declared `conform` command, no git state (a commit hook's tree is dirty by construction). `verify` = spec dimensions, docs language, the fast `verify.*` set. `pr` = branch shape, clean tree, pushed branch, every spec criterion, the slow suites. `orly gate pr` is the command CHORE(close) runs; `git.pushed` proves HEAD is exactly what the pre-push `orly gate verify` already graded. A bare `orly gate` chains all three, for a branch whose earlier gates never ran. Stage map: `dispatch/lifecycle.md`. Both stop at the first red group and only read; green unlocks CHORE(close) but never performs it. No spec → spec criteria skip, quality gates still run. Slow suites (`verify.integration`, `verify.memory`) run only when the branch carries code. A user-surface change without a docs change is red until docs land or an override says why not.
184
186
 
185
187
  **LAND (after merge, or when the user confirms it merged):** pull the default branch, prune the merged worktree + branch, `make down` where defined.
186
188
 
@@ -203,13 +205,13 @@ Edit only approved scope; no opportunistic refactors. Stay in active worktree. C
203
205
 
204
206
  ### CONFORM
205
207
 
206
- Runs after EXECUTE, before VERIFY. Invokes the `conform` commands the repository declares in `.oracle/orly.json` and aggregates every gate verdict. Any 🔴 returns to EXECUTE; the lifecycle does not advance.
208
+ Runs after EXECUTE, before VERIFY. Invokes the `conform` commands the repository declares in `.oracle/orly.json` and aggregates every gate verdict. Any 🔴 returns to EXECUTE; the lifecycle does not advance. This is the `work` gate's tier and the pre-commit hook runs it at every commit: declare a `conform` costing seconds, not minutes.
207
209
  In `agentsfleet` this stage is `make harness-verify`; its output block and end-of-turn audit detail live in `docs/HARNESS_VERIFY_OUTPUT.md`. Required rows: FILE SHAPE, PUB GATE, LENGTH GATE, MILESTONE-ID GATE, ZIG GATE, UI GATE, DESIGN TOKEN GATE, UFS GATE, SCHEMA GUARD, GREPTILE GATE, Architecture consult, Coverage, and `/orly-write-unit-test`.
208
210
 
209
211
  ### VERIFY
210
212
 
211
- Run the repository's declared `verify.*` commands from `.oracle/orly.json`. A package-scoped command never replaces a listed one.
212
- The `agentsfleet` output block and exact tiers live in `docs/VERIFY_TIERS.md`. **FIRST: `/orly-write-unit-test`** audits diff coverage. **LAST: the Test Delta row** compares against the CHORE(open) baseline; zero or negative unit growth on a code-adding diff needs justification or a return to EXECUTE. Paste memory-leak evidence into PR Session Notes or cite its Continuous Integration (CI) URL. After refactors, list newly dead code before removing it.
213
+ **Two cadences, one boundary.** Inside a Section, run CONFORM plus the declared lane covering the surface the Section touched — enough to prove a Section, reported as a Section claim. The repository's full declared `verify.*` set runs ONCE, at the milestone boundary, before the PR. A package-scoped command never replaces a listed one at either cadence, and a Section lane never satisfies a "tests pass" claim about the repository.
214
+ The `agentsfleet` output block and both cadences live in `docs/VERIFY_TIERS.md`. **FIRST: `/orly-write-unit-test`** audits diff coverage. **LAST: the Test Delta row** compares against the CHORE(open) baseline; zero or negative unit growth on a code-adding diff needs justification or a return to EXECUTE. Paste memory-leak evidence into PR Session Notes or cite its Continuous Integration (CI) URL. After refactors, list newly dead code before removing it.
213
215
 
214
216
  ### REVIEW
215
217
 
@@ -235,7 +237,7 @@ Required when spec involved — after last COMMIT, before PR. Also runs when par
235
237
 
236
238
  | # | When | Skill |
237
239
  |---|---|---|
238
- | 1 | VERIFY | `/orly-write-unit-test`, then `/orly-write-integration-test` — both before the PR, never skipped, never deferred |
240
+ | 1 | VERIFY | `/orly-write-unit-test` once per Section over that Section's diff, and again at the boundary — never skipped, never deferred. Then `/orly-write-integration-test` at the boundary **when the diff crosses a module boundary with real input/output**; otherwise record `N/A — <reason>` in Session Notes. |
239
241
  | 2 | REVIEW | gstack `/review` — every runtime, same route; address findings or record a user-acked deferral; reviewer unavailable → Session Notes: *"skipped — <reason> <ts>; rerun before merge"* |
240
242
  | 3 | After every push | `orly-babysit-prs` — CI check runs + greptile inline threads + PR-level summary; stops on two consecutive empty polls with CI green; never `gh pr checks --watch` for greptile |
241
243
 
@@ -294,7 +296,10 @@ Optimise for one thing: he never has to ask twice.
294
296
  justification clauses, importance puffery, em-dash rhythm crutches, and
295
297
  fake-profound kickers. End on the clearest concrete sentence. The banned-word
296
298
  list lives in `docs/DOCUMENTATION_RULES.md` §DOC-05, §DOC-07, §DOC-14b, and
297
- `orly gate verify` enforces it.
299
+ `orly gate verify` REPORTS it — `docs.language` prints its findings and stays
300
+ green, because the corpus predates the rule and a red on day one gets switched
301
+ off by the end of the week. Nobody fails a build over this, so the prose is
302
+ mine to get right.
298
303
 
299
304
  ## Reading Indy
300
305
 
@@ -370,5 +375,5 @@ Optimise for one thing: he never has to ask twice.
370
375
 
371
376
  | Responsibility | Commands |
372
377
  |---|---|
373
- | `conform` | `make audit` |
378
+ | `conform` | `make conform` |
374
379
  | `verify.unit` | `bun test src` |
package/SOUL.md CHANGED
@@ -44,7 +44,10 @@ Optimise for one thing: he never has to ask twice.
44
44
  justification clauses, importance puffery, em-dash rhythm crutches, and
45
45
  fake-profound kickers. End on the clearest concrete sentence. The banned-word
46
46
  list lives in `docs/DOCUMENTATION_RULES.md` §DOC-05, §DOC-07, §DOC-14b, and
47
- `orly gate verify` enforces it.
47
+ `orly gate verify` REPORTS it — `docs.language` prints its findings and stays
48
+ green, because the corpus predates the rule and a red on day one gets switched
49
+ off by the end of the week. Nobody fails a build over this, so the prose is
50
+ mine to get right.
48
51
 
49
52
  ## Reading Indy
50
53
 
@@ -95,6 +95,7 @@ The questionnaire is organised by scenario. Each scenario corresponds to a momen
95
95
  | 4.23 (Documentation) | Before published Markdown JSX (MDX), reusable snippets, customer readme, or public OpenAPI prose, must the agent read `dispatch/write_documentation.md` and `docs/DOCUMENTATION_RULES.md` before any narrower guide? | YES |
96
96
  | 4.24 (Documentation) | Are page, snippet, generated API, and changelog scopes distinct, with repository-owned pre-commit checks enforcing the mechanical rules? | YES |
97
97
  | 4.25 (Rust) | Do `*.rs` edits route to `dispatch/write_rust.md` — ownership, error variants, feature combinations, deterministic contention tests — with Microsoft's Pragmatic Rust Guidelines (`~/Projects/oss/rust-guidelines/all.txt`, fetched when absent) read sectioned and its `M-…` IDs cited in every Rust review? | YES | <!-- oracle-packs:language.rust -->
98
+ | 4.25b (Rust errors) | Is RULE ERR-RS deterministic where a machine can decide it — `dispatch/write_rust.sh` running `audits/rust-error.sh` over staged `*.rs` to block a `map_err` that stringifies its own cause and an error type with no `pub type Result<…>` in its crate — while `source()` shape and whether a context addition is warranted stay the agent's call? | YES | <!-- oracle-packs:language.rust -->
98
99
  | 4.25a (Go) | Do `*.go` edits route to `dispatch/write_go.md`, with wrapped errors, goroutine exit paths, context-first signatures, defer-at-acquire, and `-race` verification? | YES | <!-- oracle-packs:language.go -->
99
100
  | 4.26 (Python) | Do `*.py` edits route to `dispatch/write_python.md`, with parse-boundary validation and context-managed resources? | YES |
100
101
  | 4.27 (Shell) | Do `*.sh` edits route to `dispatch/write_shell.md`, with quoted expansions, array arguments, cleanup, and compatibility requirements? | YES |
package/audits/logging.sh CHANGED
@@ -9,7 +9,7 @@
9
9
  # - agentsfleet scope-prefix format from LOGGING_STANDARD §7 (the
10
10
  # `log.scoped(...)` API path under `src/logging/`)
11
11
  # - `UZ-XXX-NNN` as the `error_code=` substring per LOGGING_STANDARD §5
12
- # - `src/` + `rustd/` + `agentsfleet/src/` as the scan roots
12
+ # - `src/` + `agentsfleet/src/` as the Zig and TypeScript scan roots
13
13
  # A parameterised version that reads prefix + scope-api path + scan
14
14
  # roots from a per-project config is the right long-term shape; not
15
15
  # done yet. Today, this script lives in dotfiles so the gate body and
@@ -19,7 +19,9 @@
19
19
  # BLOCK — exits 1, must fix:
20
20
  # - `std.debug.print(` in non-test source under src/.
21
21
  # - `println!` / `eprintln!` / `dbg!`, missing `event`, or positional
22
- # tracing formatting in non-test Rust source under rustd/.
22
+ # tracing formatting in runtime Rust source. Direct stream writes
23
+ # may carry `// logging: <reason>` on or immediately above the
24
+ # emit when stdout or stderr is the intended program interface.
23
25
  # - `console.log/debug/info/warn/error` in agentsfleet/src outside tests.
24
26
  # INFO — surfaced for reviewer/agent attention, doesn't block:
25
27
  # - `std.log.scoped(...)` outside `src/logging/` (LOGGING_STANDARD §7
@@ -31,7 +33,8 @@
31
33
  #
32
34
  # Modes:
33
35
  # --staged diff-scope: only files in `git diff --cached`
34
- # --all (default) full src/ + rustd/ + agentsfleet/src/ scan
36
+ # --all (default) full src/ + every tracked/unignored *.rs +
37
+ # agentsfleet/src/ scan
35
38
  # --strict promote every INFO finding to BLOCK (post-migration use)
36
39
  #
37
40
  # Exits 0 clean, 1 on BLOCK findings.
@@ -81,9 +84,9 @@ is_test_zig() {
81
84
  return 1
82
85
  }
83
86
 
84
- is_test_rust_path() {
87
+ is_non_runtime_rust_path() {
85
88
  case "$1" in
86
- */tests/*|*/benches/*) return 0 ;;
89
+ build.rs|*/build.rs|tests/*|*/tests/*|benches/*|*/benches/*|examples/*|*/examples/*) return 0 ;;
87
90
  esac
88
91
  return 1
89
92
  }
@@ -95,11 +98,13 @@ gather_paths() {
95
98
  case "$MODE" in
96
99
  --staged)
97
100
  git diff --cached --name-only --diff-filter=ACMRT \
98
- | grep -E '^(src/.*\.zig$|rustd/.*\.rs$|agentsfleet/src/.*\.(js|jsx|ts|tsx)$)' || true
101
+ | grep -E '(^src/.*\.zig$|\.rs$|^agentsfleet/src/.*\.(js|jsx|ts|tsx)$)' || true
99
102
  ;;
100
103
  --all)
101
104
  find src -type f -name '*.zig' 2>/dev/null || true
102
- find rustd -type f -name '*.rs' 2>/dev/null || true
105
+ while IFS= read -r path; do
106
+ [[ -f "$path" ]] && printf '%s\n' "$path"
107
+ done < <(git ls-files --cached --others --exclude-standard -- '*.rs')
103
108
  find agentsfleet/src -type f \( -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' \) 2>/dev/null || true
104
109
  ;;
105
110
  esac
@@ -116,12 +121,13 @@ fi
116
121
  # single awk/grep passes.
117
122
  zig_nontest=()
118
123
  rust_nontest=()
124
+ rust_candidates=()
119
125
  js_nontest=()
120
126
  for f in "${FILES[@]}"; do
121
127
  if [[ "$f" == *.zig ]] && ! is_test_zig "$f"; then
122
128
  zig_nontest+=("$f")
123
129
  fi
124
- if [[ "$f" == *.rs ]] && ! is_test_rust_path "$f"; then
130
+ if [[ "$f" == *.rs ]] && ! is_non_runtime_rust_path "$f"; then
125
131
  rust_nontest+=("$f")
126
132
  fi
127
133
  case "$f" in
@@ -132,6 +138,10 @@ for f in "${FILES[@]}"; do
132
138
  esac
133
139
  done
134
140
 
141
+ if [[ ${#rust_nontest[@]} -gt 0 ]]; then
142
+ mapfile -t rust_candidates < <(grep -lE '(^|[^[:alnum:]_])(println|eprintln|dbg)!|tracing::(error|warn|info|debug|trace)!' "${rust_nontest[@]}" 2>/dev/null || true)
143
+ fi
144
+
135
145
  # ---------------------------------------------------------------------------
136
146
  # 2. BLOCKING: std.debug.print in non-test Zig source.
137
147
  # ---------------------------------------------------------------------------
@@ -146,10 +156,40 @@ if [[ ${#zig_nontest[@]} -gt 0 ]]; then
146
156
  fail "$f:$ln — \`std.debug.print\` in non-test source (LOGGING_STANDARD §10A.L1)"
147
157
  debug_print_hits=$((debug_print_hits + 1))
148
158
  done < <(awk '
149
- FNR == 1 { in_test = 0 }
150
- /^test "/ { in_test = 1; next }
151
- /^}/ { in_test = 0; next }
152
- /(^|[^A-Za-z0-9_])std\.debug\.print\(/ { if (!in_test) printf "%s:%d\n", FILENAME, FNR }
159
+ function comment_text(text, output, cursor, char, escaped, quoted) {
160
+ escaped = 0
161
+ quoted = 0
162
+ for (cursor = 1; cursor <= length(text); cursor++) {
163
+ char = substr(text, cursor, 1)
164
+ if (quoted) {
165
+ if (escaped) escaped = 0
166
+ else if (char == "\\") escaped = 1
167
+ else if (char == "\"") quoted = 0
168
+ continue
169
+ }
170
+ if (char == "\"") {
171
+ quoted = 1
172
+ continue
173
+ }
174
+ if (char == "/" && substr(text, cursor + 1, 1) == "/")
175
+ return substr(text, cursor + 2)
176
+ }
177
+ return ""
178
+ }
179
+ function has_logging_reason(text, comment) {
180
+ if (text ~ /^[[:space:]]*\\\\/) return 0
181
+ comment = comment_text(text)
182
+ return comment ~ /^[[:space:]]*logging:[[:space:]]*[^[:space:]]/
183
+ }
184
+ FNR == 1 { in_test = 0; previous_annotation = 0 }
185
+ {
186
+ annotated = has_logging_reason($0) || previous_annotation
187
+ if ($0 ~ /^test "/) in_test = 1
188
+ else if ($0 ~ /^}/) in_test = 0
189
+ else if ($0 ~ /(^|[^A-Za-z0-9_])std\.debug\.print\(/ && !in_test && !annotated)
190
+ printf "%s:%d\n", FILENAME, FNR
191
+ previous_annotation = has_logging_reason($0)
192
+ }
153
193
  ' "${zig_nontest[@]}")
154
194
  fi
155
195
 
@@ -160,7 +200,7 @@ fi
160
200
  rust_direct_hits=0
161
201
  rust_missing_event_hits=0
162
202
  rust_positional_hits=0
163
- if [[ ${#rust_nontest[@]} -gt 0 ]]; then
203
+ if [[ ${#rust_candidates[@]} -gt 0 ]]; then
164
204
  while IFS='|' read -r kind f ln; do
165
205
  [[ -z "$kind" ]] && continue
166
206
  case "$kind" in
@@ -227,6 +267,30 @@ if [[ ${#rust_nontest[@]} -gt 0 ]]; then
227
267
  }
228
268
  return output
229
269
  }
270
+ function comment_text(text, cursor, char, escaped, quoted) {
271
+ escaped = 0
272
+ quoted = 0
273
+ for (cursor = 1; cursor <= length(text); cursor++) {
274
+ char = substr(text, cursor, 1)
275
+ if (quoted) {
276
+ if (escaped) escaped = 0
277
+ else if (char == "\\") escaped = 1
278
+ else if (char == "\"") quoted = 0
279
+ continue
280
+ }
281
+ if (char == "\"") {
282
+ quoted = 1
283
+ continue
284
+ }
285
+ if (char == "/" && substr(text, cursor + 1, 1) == "/")
286
+ return substr(text, cursor + 2)
287
+ }
288
+ return ""
289
+ }
290
+ function has_logging_reason(text, comment) {
291
+ comment = comment_text(text)
292
+ return comment ~ /^[[:space:]]*logging:[[:space:]]*[^[:space:]]/
293
+ }
230
294
  function check_emit() {
231
295
  if (emit_code !~ /event[[:space:]]*=/ && emit_code !~ /[(,][[:space:]]*event[[:space:]]*[,)]/)
232
296
  printf "event|%s|%d\n", FILENAME, emit_line
@@ -244,15 +308,21 @@ if [[ ${#rust_nontest[@]} -gt 0 ]]; then
244
308
  emit_parens = 0
245
309
  emit_code = ""
246
310
  emit_source = ""
311
+ previous_annotation = 0
247
312
  }
248
313
  {
314
+ current_annotation = has_logging_reason($0)
249
315
  code = code_only($0)
250
- if (!in_emit && code ~ /^[[:space:]]*$/) next
316
+ if (!in_emit && code ~ /^[[:space:]]*$/) {
317
+ previous_annotation = current_annotation
318
+ next
319
+ }
251
320
  opens = count_char(code, "\\{")
252
321
  closes = count_char(code, "\\}")
253
322
  if (test_depth > 0) {
254
323
  depth += opens - closes
255
324
  if (depth < test_depth) test_depth = 0
325
+ previous_annotation = current_annotation
256
326
  next
257
327
  }
258
328
  if (code ~ /^[[:space:]]*#\[cfg\(test\)\]/) {
@@ -261,6 +331,7 @@ if [[ ${#rust_nontest[@]} -gt 0 ]]; then
261
331
  depth += opens - closes
262
332
  if (depth < test_depth) test_depth = 0
263
333
  } else cfg_test = 1
334
+ previous_annotation = current_annotation
264
335
  next
265
336
  }
266
337
  if (cfg_test) {
@@ -270,9 +341,10 @@ if [[ ${#rust_nontest[@]} -gt 0 ]]; then
270
341
  if (depth < test_depth) test_depth = 0
271
342
  cfg_test = 0
272
343
  } else if (code ~ /;/) cfg_test = 0
344
+ previous_annotation = current_annotation
273
345
  next
274
346
  }
275
- if (code ~ /(^|[^[:alnum:]_])(println|eprintln|dbg)!/)
347
+ if (code ~ /(^|[^[:alnum:]_])(println|eprintln|dbg)!/ && !current_annotation && !previous_annotation)
276
348
  printf "direct|%s|%d\n", FILENAME, FNR
277
349
  if (!in_emit && match(code, /tracing::(error|warn|info|debug|trace)!/)) {
278
350
  in_emit = 1
@@ -288,8 +360,9 @@ if [[ ${#rust_nontest[@]} -gt 0 ]]; then
288
360
  }
289
361
  if (in_emit && emit_parens <= 0) check_emit()
290
362
  depth += opens - closes
363
+ previous_annotation = current_annotation
291
364
  }
292
- ' "${rust_nontest[@]}")
365
+ ' "${rust_candidates[@]}")
293
366
  fi
294
367
 
295
368
  # ---------------------------------------------------------------------------
@@ -363,6 +436,7 @@ fi
363
436
  ok "scanned ${#FILES[@]} files; std.debug.print=$debug_print_hits rust-direct=$rust_direct_hits rust-missing-event=$rust_missing_event_hits rust-positional=$rust_positional_hits console.*=$console_hits std.log.scoped=$scoped_hits missing-error_code=$missing_code_hits"
364
437
  if [[ $FAIL -ne 0 ]]; then
365
438
  printf "\n🔴 LOGGING GATE: blocking violations. See dispatch/write_any.md (Logging Gate).\n" >&2
439
+ printf "Intentional direct Zig or Rust stream output requires '// logging: <reason>' on or immediately above the emit; the reason must be non-empty.\n" >&2
366
440
  exit 1
367
441
  fi
368
442
  [[ $INFO_COUNT -gt 0 ]] && note "$INFO_COUNT informational findings; not blocking. Use --strict to enforce."
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env bash
2
+ # rust-error.sh — the mechanical half of RULE ERR-RS (Rust error discipline).
3
+ #
4
+ # Dispatch façade: dispatch/write_rust.md (Error discipline). Pairs with
5
+ # dispatch/write_rust.sh, which is what runs it.
6
+ #
7
+ # The rule has four clauses. Two of them a machine can decide from the text
8
+ # alone, and only those two live here:
9
+ #
10
+ # 1. LOSSY MAP_ERR — `map_err` whose closure converts its own cause through
11
+ # `to_string()` or `format!`. The rule allows `map_err` to ADD context the
12
+ # call site alone knows; it forbids stringifying the cause on the way into
13
+ # an error type, because `to_string()` destroys the `source()` chain. The
14
+ # closure's BINDING is what makes this decidable: `.map_err(|e| Mine::Db(
15
+ # e.to_string()))` stringifies the cause, while `.map_err(|e| Mine::Read {
16
+ # source: e, path: path.to_string() })` stringifies a path and is correct.
17
+ # Only a `to_string()` or `format!` applied to the closure's own parameter
18
+ # is a finding.
19
+ #
20
+ # 2. MISSING RESULT ALIAS — a crate declaring `pub enum Error` or
21
+ # `pub struct Error` with no `pub type Result<` anywhere beside it. The
22
+ # rule is "one error type per crate, and a `Result` alias beside it": a
23
+ # reader must never have to check WHICH error a signature returns.
24
+ #
25
+ # The other two clauses stay JUDGMENT and are not attempted here. Whether a
26
+ # `source()` implementation returns its own kind needs the Display body read
27
+ # beside it, and whether a context addition is warranted is a call-site
28
+ # question. A gate that guesses at those gets switched off within a week, and a
29
+ # switched-off gate protects nothing — partial mechanisation stated honestly is
30
+ # the whole design (the same call audits/doc-read.sh makes about its record).
31
+ #
32
+ # Carve-outs match audits/logging.sh exactly, so one Rust scope answers to both
33
+ # leaves: build scripts, `tests/`, `benches/`, `examples/`, and `#[cfg(test)]`
34
+ # modules are fixture code, where a stringified error in a test assertion is
35
+ # not a production error path.
36
+ #
37
+ # Modes:
38
+ # --staged diff-scope: only *.rs in `git diff --cached`
39
+ # --all (default) every tracked/unignored *.rs
40
+ #
41
+ # Exits 0 clean, 1 on findings.
42
+
43
+ set -euo pipefail
44
+
45
+ MODE_STAGED="--staged"
46
+ MODE_ALL="--all"
47
+ RULE="ERR-RS"
48
+ FACADE="dispatch/write_rust.md"
49
+ # The closure body window. A `map_err` closure that has not closed its
50
+ # parentheses within this many lines is a block long enough that its conversion
51
+ # is no longer a one-liner, and the rule's subject is the one-liner.
52
+ MAX_CLOSURE_LINES=12
53
+ CARGO_MANIFEST="Cargo.toml"
54
+ RESULT_ALIAS_PATTERN='^[[:space:]]*pub[[:space:]]+type[[:space:]]+Result[[:space:]]*<'
55
+ ERROR_TYPE_PATTERN='^[[:space:]]*pub[[:space:]]+(enum|struct)[[:space:]]+Error[[:space:]]*[<{(;]'
56
+
57
+ MODE="$MODE_ALL"
58
+ for arg in "$@"; do
59
+ case "$arg" in
60
+ --staged|staged) MODE="$MODE_STAGED" ;;
61
+ --all|all) MODE="$MODE_ALL" ;;
62
+ -h|--help)
63
+ printf 'usage: %s [--staged|--all]\n' "$0"
64
+ exit 0
65
+ ;;
66
+ *) printf 'unknown arg: %s\n' "$arg" >&2; exit 64 ;;
67
+ esac
68
+ done
69
+
70
+ ROOT="$(git rev-parse --show-toplevel)"
71
+ cd "$ROOT"
72
+
73
+ FAIL=0
74
+ fail() { printf 'FAIL: %s\n' "$*" >&2; FAIL=1; }
75
+ ok() { printf 'OK: %s\n' "$*"; }
76
+
77
+ # Non-runtime Rust paths, identical to audits/logging.sh's carve-out list.
78
+ is_non_runtime_rust_path() {
79
+ case "$1" in
80
+ build.rs|*/build.rs|tests/*|*/tests/*|benches/*|*/benches/*|examples/*|*/examples/*) return 0 ;;
81
+ esac
82
+ return 1
83
+ }
84
+
85
+ gather_paths() {
86
+ case "$MODE" in
87
+ "$MODE_STAGED")
88
+ git diff --cached --name-only --diff-filter=ACMRT -- '*.rs' || true
89
+ ;;
90
+ "$MODE_ALL")
91
+ git ls-files --cached --others --exclude-standard -- '*.rs' || true
92
+ ;;
93
+ esac
94
+ }
95
+
96
+ FILES=()
97
+ while IFS= read -r path; do
98
+ [[ -n "$path" && -f "$path" ]] || continue
99
+ is_non_runtime_rust_path "$path" && continue
100
+ FILES+=("$path")
101
+ done < <(gather_paths)
102
+
103
+ if [[ ${#FILES[@]} -eq 0 ]]; then
104
+ ok "$RULE: no runtime Rust files in scope ($MODE)"
105
+ exit 0
106
+ fi
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # 1. Scan every in-scope file once, emitting both signals.
110
+ # ---------------------------------------------------------------------------
111
+ # One pass, one carve-out. Both halves need the same `#[cfg(test)]` skip, and
112
+ # implementing it twice is how the two halves would come to disagree about what
113
+ # a test module is — so the scanner emits `LOSSY:` and `ERRTYPE:` records from a
114
+ # single walk and the shell decides what to do with each.
115
+ #
116
+ # The lossy scanner accumulates a map_err closure from its opening parenthesis
117
+ # until the parentheses balance, so a conversion split over several lines reads
118
+ # the same as a one-liner.
119
+ signals="$(awk -v maxlines="$MAX_CLOSURE_LINES" -v errtype="$ERROR_TYPE_PATTERN" '
120
+ FNR == 1 { intest = 0; testdepth = 0; pending = 0; collecting = 0 }
121
+
122
+ # A #[cfg(test)] module is fixture code. The attribute precedes the item, so
123
+ # arm on the attribute and start counting braces at the item that follows.
124
+ !collecting && /#\[cfg\(test\)\]/ { pending = 1; next }
125
+ pending {
126
+ if (index($0, "{") > 0) { pending = 0; intest = 1; testdepth = 0 }
127
+ else next
128
+ }
129
+ intest {
130
+ n = gsub(/\{/, "{"); m = gsub(/\}/, "}")
131
+ testdepth += n - m
132
+ if (testdepth <= 0) intest = 0
133
+ next
134
+ }
135
+
136
+ {
137
+ line = $0
138
+ # Strip a trailing line comment so a commented example is not a finding.
139
+ sub(/\/\/.*$/, "", line)
140
+
141
+ # A public error type outside any test module. Emitted per file; the shell
142
+ # resolves it to a crate and asks that crate for its alias once.
143
+ if (!collecting && line ~ errtype) printf "ERRTYPE:%s\n", FILENAME
144
+
145
+ if (!collecting) {
146
+ where = match(line, /map_err[[:space:]]*\([[:space:]]*\|[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\|/)
147
+ if (where == 0) next
148
+ rest = substr(line, where)
149
+ # The closure binding: the identifier between the pipes.
150
+ bind = rest
151
+ sub(/^map_err[[:space:]]*\([[:space:]]*\|[[:space:]]*/, "", bind)
152
+ sub(/[[:space:]]*\|.*$/, "", bind)
153
+ if (bind == "_") next
154
+ collecting = 1; buffer = rest; startline = FNR; spanned = 1
155
+ depth = gsub(/\(/, "(", rest) - gsub(/\)/, ")", rest)
156
+ } else {
157
+ buffer = buffer " " line
158
+ spanned++
159
+ depth += gsub(/\(/, "(", line) - gsub(/\)/, ")", line)
160
+ }
161
+
162
+ if (depth > 0 && spanned < maxlines) next
163
+
164
+ # `bind.to_string()` — the cause converted to text on the way in.
165
+ stringified = 0
166
+ if (buffer ~ ("(^|[^A-Za-z0-9_])" bind "[[:space:]]*\\.[[:space:]]*to_string[[:space:]]*\\(")) stringified = 1
167
+ # `format!(... bind ...)` — the same loss wearing a formatter.
168
+ if (buffer ~ /format!/ && buffer ~ ("(^|[^A-Za-z0-9_])" bind "([^A-Za-z0-9_]|$)")) {
169
+ after = buffer; sub(/^.*format!/, "", after)
170
+ if (after ~ ("(^|[^A-Za-z0-9_])" bind "([^A-Za-z0-9_]|$)")) stringified = 1
171
+ }
172
+ if (stringified) printf "LOSSY:%s:%d\n", FILENAME, startline
173
+ collecting = 0
174
+ }
175
+ ' "${FILES[@]}" || true)"
176
+
177
+ lossy_count=0
178
+ error_type_files=()
179
+ while IFS= read -r signal; do
180
+ case "$signal" in
181
+ LOSSY:*)
182
+ lossy_count=$((lossy_count + 1))
183
+ fail "${signal#LOSSY:} $RULE: map_err stringifies its own cause — to_string()/format! on the closure binding destroys the source() chain. Carry the cause with #[from] or a source field; map_err only ADDS context."
184
+ ;;
185
+ ERRTYPE:*)
186
+ error_type_files+=("${signal#ERRTYPE:}")
187
+ ;;
188
+ esac
189
+ done <<< "$signals"
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # 2. An error type with no Result alias beside it.
193
+ # ---------------------------------------------------------------------------
194
+ # Crate-scoped: the alias may live in any file of the crate that declares the
195
+ # error, so the question is asked of the crate, once, not of the file.
196
+ crate_root_of() {
197
+ local dir; dir="$(dirname "$1")"
198
+ while [[ "$dir" != "." && "$dir" != "/" ]]; do
199
+ [[ -f "$dir/$CARGO_MANIFEST" ]] && { printf '%s' "$dir"; return 0; }
200
+ dir="$(dirname "$dir")"
201
+ done
202
+ [[ -f "$CARGO_MANIFEST" ]] && { printf '.'; return 0; }
203
+ return 1
204
+ }
205
+
206
+ alias_count=0
207
+ checked_crates=""
208
+ for path in ${error_type_files[@]+"${error_type_files[@]}"}; do
209
+ crate=""
210
+ crate_root_of "$path" > /dev/null 2>&1 && crate="$(crate_root_of "$path")"
211
+ # A .rs file outside any Cargo crate has no crate to carry the alias; the
212
+ # lossy-map_err half still judged it.
213
+ [[ -n "$crate" ]] || continue
214
+ case " $checked_crates " in *" $crate "*) continue ;; esac
215
+ checked_crates="$checked_crates $crate"
216
+ if ! git ls-files --cached --others --exclude-standard -- "$crate/*.rs" \
217
+ | xargs grep -lE "$RESULT_ALIAS_PATTERN" 2>/dev/null | grep -q .; then
218
+ alias_count=$((alias_count + 1))
219
+ fail "$path $RULE: crate '$crate' declares a public Error type with no \`pub type Result<T, E = Error>\` beside it — a reader must not have to check WHICH error a signature returns."
220
+ fi
221
+ done
222
+
223
+ ok "$RULE: scanned ${#FILES[@]} runtime Rust file(s); lossy-map_err=$lossy_count missing-result-alias=$alias_count"
224
+ if [[ $FAIL -ne 0 ]]; then
225
+ printf '\n🔴 %s: blocking violations. See %s (Error discipline).\n' "$RULE" "$FACADE" >&2
226
+ exit 1
227
+ fi
228
+ exit 0
@@ -61,7 +61,7 @@ Auto-memory is **disabled** (`autoMemoryEnabled: false` + `CLAUDE_CODE_DISABLE_A
61
61
  - **Skip hooks/signing.** Never `--no-verify`/`--no-gpg-sign`/`-c commit.gpgsign=false`. Hook fails → fix cause.
62
62
  - **Plaintext secrets in entity tables.** Store vault `key_name` ref, resolve via `crypto_store.load()`.
63
63
  - **Static strings in SQL schema.** No `DEFAULT 'value'`/`CHECK (col IN ('a','b'))`. Enforce in app via named constants.
64
- - **Resolving/printing credentials.** `op read 'op://...'` at runtime — never paste/log.
64
+ - **Resolving/printing credentials.** `op read 'op://...'` at runtime — never paste/log. Vault names come from the environment, never from a rules file.
65
65
  - **Force-push default branch** (`main`/`master`).
66
66
  - **Install-process launches in core paths.** Native SDKs. Exception: personal dev tools (`op`/`gh`/`glab`/`oracle`).
67
67
 
@@ -80,18 +80,18 @@ Auto-memory is **disabled** (`autoMemoryEnabled: false` + `CLAUDE_CODE_DISABLE_A
80
80
  ### Operational defaults
81
81
 
82
82
  - Workspace `~/Projects`. `gh`/`glab` not browsers. `trash` not `rm`. Conventional Commits. Process decisions → repo docs, not chat. "Make a note" → update `AGENTS.md`/repo docs.
83
- - **Symlinked edits land in the repository that owns the file.** A file whose `readlink` resolves outside this checkout — into a dotfiles or config repository — is an edit to *that* repository, not this one. Detect via `readlink` BEFORE editing. After: `cd` to the resolved repository, `git add <files> && git commit && git push` on its default branch. Never leave uncommitted. <!-- oracle-packs:persona.indy -->
83
+ - **Symlinked edits land in the repository that owns the file.** A file whose `readlink` resolves outside this checkout — into a dotfiles or config repository — is an edit to *that* repository, not this one. Detect via `readlink` BEFORE editing. After: `cd` to the resolved repository, `git add <files> && git commit && git push` on its default branch. Never leave uncommitted.
84
84
  - **Docs-repo edits on own branch.** `~/Projects/docs/` is shared across milestones: check `git status` first; from `main` (checkout or worktree off `main`) commit on `chore/m{N}-{slug}-changelog`; recovery = stash, re-apply on a fresh branch.
85
- - Other dotfiles (`.zshrc`/`.gitconfig`/etc.): timestamped backup; minimal edits. <!-- oracle-packs:persona.indy -->
85
+ - Other dotfiles (`.zshrc`/`.gitconfig`/etc.): timestamped backup; minimal edits.
86
+ - **Check a sibling repository for an existing pattern in the same language before inventing one.** A shape shipping next door is prior art; a new one needs a reason.
87
+ - **Read the reference implementation before designing.** Where the language's dispatch page names a canonical external source, that read is mandatory in review and its guideline identifiers are cited — applied or diverged from. "Broken for us" means the delta was missed: diff our call site against theirs before blaming the principle.
86
88
  - Before commit/push: `gitleaks` must pass.
87
89
  - No new `make` targets without a distinct caller (CI job, spec-mandated gate, or a workflow existing targets can't express) — check `make/*.mk` first; extend over near-duplicate wrappers.
88
90
  - `*.zig` → read `dispatch/write_zig.md`; ZIG GATE fires. <!-- oracle-packs:language.zig -->
89
91
  - Auth-flow (token-minting handlers, credential-typed spec dimensions, the repository's auth directories) → read the repository's `docs/AUTH.md` first, where it exists. <!-- oracle-packs:domain.auth -->
90
92
  - `conn.query()` requires `.drain()` in same fn before `deinit()`. Verify `make lint-governance`. Use `conn.exec()` for no-rows. <!-- oracle-packs:product.agentsfleet -->
91
93
  <!-- oracle-packs:start persona.indy -->
92
- - **Vault (1Password `op`).** Resolve secrets via the `op` CLI, never hand-paste/log. Vault names come from the environment, never from a rules file.
93
- - Check a sibling repository under `$HOME/Projects/` for an existing pattern in the same language before inventing one.
94
- - **Reference canon (read before designing):** TypeScript → `oss/supabase/apps/studio` + `oss/supabase/packages/{ui,ui-patterns}` + `oss/cli`; Zig → `oss/ghostty/src/`; Rust → `oss/bun/src/` + `oss/exonum` + `oss/core_api-develop` + Microsoft's Rust guidelines (`oss/rust-guidelines/all.txt`, fetch if absent; mandatory in review). Missing checkout → ask, then clone into `~/Projects/oss/`. "Broken for us" → call-site diff first.
94
+ - **Indy's reference checkouts** (the machine-local half of the rule above): TypeScript → `oss/supabase/apps/studio` + `oss/supabase/packages/{ui,ui-patterns}` + `oss/cli`; Zig → `oss/ghostty/src/`; Rust → `oss/bun/src/` + `oss/exonum` + `oss/core_api-develop`. Missing checkout → ask, then clone into `~/Projects/oss/`.
95
95
  <!-- oracle-packs:end -->
96
96
 
97
97
  **Forge detection:** `github.com` → `gh`; `gitlab.com` → `glab`. Check `git remote -v`.
@@ -186,7 +186,9 @@ Non-trivial (full lifecycle) if it: touches >1 file · new abstraction · data m
186
186
 
187
187
  **With spec:** `CHORE(open) → PLAN → EXECUTE → CONFORM → VERIFY → REVIEW → DOCUMENT → COMMIT → CHORE(close)`. **Without spec** (bug fix/config/refactor): `PLAN → EXECUTE → CONFORM → VERIFY → REVIEW → DOCUMENT → COMMIT`. CHORE bookends iff work creates/continues a spec under `docs/v*/{active,pending}/`. Stage runbooks (checklists, recipes, formats): `dispatch/lifecycle.md`.
188
188
 
189
- **Anchor invariant — `orly gate` proves the boundary.** No PR opens unless every `orly gate pr` criterion is green or carries an `Orly-Override` trailer the user recorded with a reason. `orly gate` runs `work → verify → pr`, stops at the first red group, and only reads; green unlocks CHORE(close) but never performs it. No spec → spec criteria skip, quality gates still run. Slow suites (`verify.integration`, `verify.memory`) run only when the branch carries code. A user-surface change without a docs change is red until docs land or an override says why not.
189
+ **Anchor invariant — `orly gate` proves the boundary.** No PR opens unless every `orly gate pr` criterion is green or carries an `Orly-Override` trailer the user recorded with a reason.
190
+
191
+ **One gate per cadence, each tier run once.** `work` = the declared `conform` command, no git state (a commit hook's tree is dirty by construction). `verify` = spec dimensions, docs language, the fast `verify.*` set. `pr` = branch shape, clean tree, pushed branch, every spec criterion, the slow suites. `orly gate pr` is the command CHORE(close) runs; `git.pushed` proves HEAD is exactly what the pre-push `orly gate verify` already graded. A bare `orly gate` chains all three, for a branch whose earlier gates never ran. Stage map: `dispatch/lifecycle.md`. Both stop at the first red group and only read; green unlocks CHORE(close) but never performs it. No spec → spec criteria skip, quality gates still run. Slow suites (`verify.integration`, `verify.memory`) run only when the branch carries code. A user-surface change without a docs change is red until docs land or an override says why not.
190
192
 
191
193
  **LAND (after merge, or when the user confirms it merged):** pull the default branch, prune the merged worktree + branch, `make down` where defined.
192
194
 
@@ -213,16 +215,16 @@ Edit only approved scope; no opportunistic refactors. Stay in active worktree. C
213
215
 
214
216
  ### CONFORM
215
217
 
216
- Runs after EXECUTE, before VERIFY. Invokes the `conform` commands the repository declares in `.oracle/orly.json` and aggregates every gate verdict. Any 🔴 returns to EXECUTE; the lifecycle does not advance.
218
+ Runs after EXECUTE, before VERIFY. Invokes the `conform` commands the repository declares in `.oracle/orly.json` and aggregates every gate verdict. Any 🔴 returns to EXECUTE; the lifecycle does not advance. This is the `work` gate's tier and the pre-commit hook runs it at every commit: declare a `conform` costing seconds, not minutes.
217
219
  <!-- oracle-packs:start product.agentsfleet -->
218
220
  In `agentsfleet` this stage is `make harness-verify`; its output block and end-of-turn audit detail live in `docs/HARNESS_VERIFY_OUTPUT.md`. Required rows: FILE SHAPE, PUB GATE, LENGTH GATE, MILESTONE-ID GATE, ZIG GATE, UI GATE, DESIGN TOKEN GATE, UFS GATE, SCHEMA GUARD, GREPTILE GATE, Architecture consult, Coverage, and `/orly-write-unit-test`.
219
221
  <!-- oracle-packs:end -->
220
222
 
221
223
  ### VERIFY
222
224
 
223
- Run the repository's declared `verify.*` commands from `.oracle/orly.json`. A package-scoped command never replaces a listed one.
225
+ **Two cadences, one boundary.** Inside a Section, run CONFORM plus the declared lane covering the surface the Section touched — enough to prove a Section, reported as a Section claim. The repository's full declared `verify.*` set runs ONCE, at the milestone boundary, before the PR. A package-scoped command never replaces a listed one at either cadence, and a Section lane never satisfies a "tests pass" claim about the repository.
224
226
  <!-- oracle-packs:start product.agentsfleet -->
225
- The `agentsfleet` output block and exact tiers live in `docs/VERIFY_TIERS.md`. **FIRST: `/orly-write-unit-test`** audits diff coverage. **LAST: the Test Delta row** compares against the CHORE(open) baseline; zero or negative unit growth on a code-adding diff needs justification or a return to EXECUTE. Paste memory-leak evidence into PR Session Notes or cite its Continuous Integration (CI) URL. After refactors, list newly dead code before removing it.
227
+ The `agentsfleet` output block and both cadences live in `docs/VERIFY_TIERS.md`. **FIRST: `/orly-write-unit-test`** audits diff coverage. **LAST: the Test Delta row** compares against the CHORE(open) baseline; zero or negative unit growth on a code-adding diff needs justification or a return to EXECUTE. Paste memory-leak evidence into PR Session Notes or cite its Continuous Integration (CI) URL. After refactors, list newly dead code before removing it.
226
228
  <!-- oracle-packs:end -->
227
229
 
228
230
  ### REVIEW
@@ -250,7 +252,7 @@ Required when spec involved — after last COMMIT, before PR. Also runs when par
250
252
 
251
253
  | # | When | Skill |
252
254
  |---|---|---|
253
- | 1 | VERIFY | `/orly-write-unit-test`, then `/orly-write-integration-test` — both before the PR, never skipped, never deferred |
255
+ | 1 | VERIFY | `/orly-write-unit-test` once per Section over that Section's diff, and again at the boundary — never skipped, never deferred. Then `/orly-write-integration-test` at the boundary **when the diff crosses a module boundary with real input/output**; otherwise record `N/A — <reason>` in Session Notes. |
254
256
  | 2 | REVIEW | gstack `/review` — every runtime, same route; address findings or record a user-acked deferral; reviewer unavailable → Session Notes: *"skipped — <reason> <ts>; rerun before merge"* |
255
257
  | 3 | After every push | `orly-babysit-prs` — CI check runs + greptile inline threads + PR-level summary; stops on two consecutive empty polls with CI green; never `gh pr checks --watch` for greptile |
256
258