@agentsfleet/orly 0.6.8 → 0.7.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.
@@ -32,7 +32,11 @@ fi
32
32
  # that did or did not read the façade is still the session committing.
33
33
  SOURCE_PATTERN='\.(zig|ts|tsx|js|jsx|py|rs|go|sh|sql)$'
34
34
 
35
- if git diff --cached --name-only | grep -qE "$SOURCE_PATTERN"; then
35
+ # Governance paths join the source list here: dispatch/edit_rules.sh declares
36
+ # the corpus scope, so a staged rule edit now has a façade to have read. The
37
+ # check itself stays the arbiter — a path outside every leaf's globs still
38
+ # reports "nothing staged triggers a façade" and costs one grep.
39
+ if git diff --cached --name-only | grep -qE "$SOURCE_PATTERN|$GOVERNANCE_PATTERN"; then
36
40
  if ! bash audits/doc-read.sh check; then
37
41
  echo "pre-commit: a triggered façade went unread — read it, or record why" >&2
38
42
  exit 1
@@ -81,8 +81,8 @@ The questionnaire is organised by scenario. Each scenario corresponds to a momen
81
81
  | 4.9 (Shell) | Must `gitleaks` pass before any commit/push? | YES |
82
82
  | 4.10 (CI) | Are CI/CD edits (`.github/workflows/**`, deploy configs) **forbidden without explicit user approval** even in auto mode? | YES |
83
83
  | 4.11 (Bun) | For `*.ts`/`*.tsx`/`*.js`/`*.jsx` edits, does AGENTS.md route to `dispatch/write_ts_adhere_bun.md` (TS FILE SHAPE DECISION at PLAN, const/import/Bun-primitive discipline)? | YES |
84
- | 4.12 (Logging) | For every Edit/Write that adds/removes/changes a log emit (Zig `std.log.*`/`std.debug.print`/`obs.scoped`, TS/JS `console.*`/custom logger, shell `echo` to `&2`), does LOGGING GATE require reading `docs/LOGGING_STANDARD.md` and printing the per-edit gate block citing §3 (wire format) / §4 (severity) / §5 (error codes) / §6 (PII) / §10A (tightenings)? | YES |
85
- | 4.13 (Logging) | Is `std.debug.print` in non-test source AND `console.log` in `agentsfleet/src/**` an automatic blocking violation, with no "temporary" carve-out? | YES |
84
+ | 4.12 (Logging) | For every Edit/Write that adds/removes/changes a log emit (Zig `std.log.*`/`std.debug.print`/`obs.scoped`, Rust `tracing::*`/print macros under `rustd/`, TS/JS `console.*`/custom logger, shell `echo` to `&2`), does LOGGING GATE require reading `docs/LOGGING_STANDARD.md` and printing the per-edit gate block citing §3 (wire format) / §4 (severity) / §5 (error codes) / §6 (PII) / §7, §8, or §8A (language binding) / §10A (tightenings)? | YES |
85
+ | 4.13 (Logging) | Are `std.debug.print`, Rust `println!`/`eprintln!`/`dbg!`, Rust tracing without `event` or with positional formatting, and `console.log` in their non-test production scopes automatic blocking violations, with no "temporary" carve-out? | YES |
86
86
  | 4.14 (Logging) | Must a new registry-scheme `error_code` reference (agentsfleet: `UZ-XXX-NNN`) land in the same commit as its registry entry (used-but-undeclared = blocking)? | YES |
87
87
  | 4.15 (Lifecycle) | For every Edit/Write that adds/reshapes a lifecycle method in `*.zig` (`pub fn init|deinit|close|release|destroy|shutdown|dispose|free`) or an `errdefer`/`defer` adjacent to allocation, does LIFECYCLE GATE require reading `docs/LIFECYCLE_PATTERNS.md` and printing the per-edit gate block? | YES |
88
88
  | 4.16 (Lifecycle) | Is `defer X.free(Y)` + `errdefer X.free(Y)` on the same allocation in the same scope a blocking violation? | YES |
@@ -94,19 +94,21 @@ content_hash() {
94
94
  git -C "$LEDGER_ROOT" hash-object "$LEDGER_ROOT/$1" 2>/dev/null || printf ''
95
95
  }
96
96
 
97
- # Façade pages the staged files trigger, one per line. Same glob map the
98
- # reachability report uses: a façade's scope is its dispatch_init line, so this
99
- # check and the ledger can never disagree about what fires.
97
+ # Façade pages the staged files trigger, one per line. Scope comes from
98
+ # ledger_facade_scope — the page's own oracle-scope tag, else its leaf's
99
+ # dispatch_init — which is the SAME resolver the ledger's reachability report
100
+ # uses, so this check and the ledger can never disagree about what fires.
101
+ # Iterating pages rather than leaves is the point: a façade without a
102
+ # deterministic leaf still has a trigger surface, and used to be unaskable.
100
103
  expected_facade_pages() {
101
- local staged="$1" script stem globs
102
- while IFS= read -r script; do
103
- [ -n "$script" ] || continue
104
- stem="$(basename "$script" .sh)"
105
- [ -f "$LEDGER_ROOT/dispatch/$stem.md" ] || continue
106
- globs="$(ledger_facade_globs "$script")"
107
- [ -n "$globs" ] || continue
108
- [ "$(ledger_match_count "$globs" < "$staged")" -gt 0 ] && printf 'dispatch/%s.md\n' "$stem"
109
- done < <(ledger_facade_scripts)
104
+ local staged="$1" page stem scope
105
+ while IFS= read -r page; do
106
+ [ -n "$page" ] || continue
107
+ scope="$(ledger_facade_scope "$page")"
108
+ [ -n "$scope" ] || continue
109
+ stem="$(basename "$page" .md)"
110
+ [ "$(ledger_match_count "$scope" < "$staged")" -gt 0 ] && printf 'dispatch/%s.md\n' "$stem"
111
+ done < <(ledger_facade_pages)
110
112
  }
111
113
 
112
114
  json_unescape() {
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/` + `agentsfleet/src/` as the scan roots
12
+ # - `src/` + `rustd/` + `agentsfleet/src/` as the 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
@@ -18,6 +18,8 @@
18
18
  # Two severity tiers:
19
19
  # BLOCK — exits 1, must fix:
20
20
  # - `std.debug.print(` in non-test source under src/.
21
+ # - `println!` / `eprintln!` / `dbg!`, missing `event`, or positional
22
+ # tracing formatting in non-test Rust source under rustd/.
21
23
  # - `console.log/debug/info/warn/error` in agentsfleet/src outside tests.
22
24
  # INFO — surfaced for reviewer/agent attention, doesn't block:
23
25
  # - `std.log.scoped(...)` outside `src/logging/` (LOGGING_STANDARD §7
@@ -29,7 +31,7 @@
29
31
  #
30
32
  # Modes:
31
33
  # --staged diff-scope: only files in `git diff --cached`
32
- # --all (default) full src/ + agentsfleet/src/ scan
34
+ # --all (default) full src/ + rustd/ + agentsfleet/src/ scan
33
35
  # --strict promote every INFO finding to BLOCK (post-migration use)
34
36
  #
35
37
  # Exits 0 clean, 1 on BLOCK findings.
@@ -79,6 +81,13 @@ is_test_zig() {
79
81
  return 1
80
82
  }
81
83
 
84
+ is_test_rust_path() {
85
+ case "$1" in
86
+ */tests/*|*/benches/*) return 0 ;;
87
+ esac
88
+ return 1
89
+ }
90
+
82
91
  # ---------------------------------------------------------------------------
83
92
  # 1. Gather files in scope.
84
93
  # ---------------------------------------------------------------------------
@@ -86,11 +95,12 @@ gather_paths() {
86
95
  case "$MODE" in
87
96
  --staged)
88
97
  git diff --cached --name-only --diff-filter=ACMRT \
89
- | grep -E '^(src/.*\.zig$|agentsfleet/src/.*\.(js|jsx|ts|tsx)$)' || true
98
+ | grep -E '^(src/.*\.zig$|rustd/.*\.rs$|agentsfleet/src/.*\.(js|jsx|ts|tsx)$)' || true
90
99
  ;;
91
100
  --all)
92
- find src -type f -name '*.zig' 2>/dev/null
93
- find agentsfleet/src -type f \( -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' \) 2>/dev/null
101
+ find src -type f -name '*.zig' 2>/dev/null || true
102
+ find rustd -type f -name '*.rs' 2>/dev/null || true
103
+ find agentsfleet/src -type f \( -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' \) 2>/dev/null || true
94
104
  ;;
95
105
  esac
96
106
  }
@@ -105,11 +115,15 @@ fi
105
115
  # previously ran 3–4 forks × N files; M70 perf pass batches them into
106
116
  # single awk/grep passes.
107
117
  zig_nontest=()
118
+ rust_nontest=()
108
119
  js_nontest=()
109
120
  for f in "${FILES[@]}"; do
110
121
  if [[ "$f" == *.zig ]] && ! is_test_zig "$f"; then
111
122
  zig_nontest+=("$f")
112
123
  fi
124
+ if [[ "$f" == *.rs ]] && ! is_test_rust_path "$f"; then
125
+ rust_nontest+=("$f")
126
+ fi
113
127
  case "$f" in
114
128
  agentsfleet/src/*.test.*|agentsfleet/src/*.spec.*|agentsfleet/src/tests/*) ;;
115
129
  agentsfleet/src/*.js|agentsfleet/src/*.jsx|agentsfleet/src/*.ts|agentsfleet/src/*.tsx)
@@ -140,7 +154,146 @@ if [[ ${#zig_nontest[@]} -gt 0 ]]; then
140
154
  fi
141
155
 
142
156
  # ---------------------------------------------------------------------------
143
- # 3. BLOCKING: console.log/debug/info/warn/error in agentsfleet/src non-test.
157
+ # 3. BLOCKING: unstructured or event-less tracing in non-test Rust source.
158
+ # The awk tracker excludes complete items annotated with #[cfg(test)].
159
+ # ---------------------------------------------------------------------------
160
+ rust_direct_hits=0
161
+ rust_missing_event_hits=0
162
+ rust_positional_hits=0
163
+ if [[ ${#rust_nontest[@]} -gt 0 ]]; then
164
+ while IFS='|' read -r kind f ln; do
165
+ [[ -z "$kind" ]] && continue
166
+ case "$kind" in
167
+ direct)
168
+ fail "$f:$ln — direct Rust diagnostic macro in non-test source (LOGGING_STANDARD §8A)"
169
+ rust_direct_hits=$((rust_direct_hits + 1))
170
+ ;;
171
+ event)
172
+ fail "$f:$ln — \`tracing\` emit without \`event = ...\` (LOGGING_STANDARD §8A)"
173
+ rust_missing_event_hits=$((rust_missing_event_hits + 1))
174
+ ;;
175
+ positional)
176
+ fail "$f:$ln — positional formatting in \`tracing\` emit (LOGGING_STANDARD §8A)"
177
+ rust_positional_hits=$((rust_positional_hits + 1))
178
+ ;;
179
+ esac
180
+ done < <(awk '
181
+ function count_char(text, char, copy, count) {
182
+ copy = text
183
+ count = gsub(char, "", copy)
184
+ return count
185
+ }
186
+ function code_only(text, output, cursor, char, escaped, quoted) {
187
+ output = ""
188
+ escaped = 0
189
+ quoted = 0
190
+ for (cursor = 1; cursor <= length(text); cursor++) {
191
+ char = substr(text, cursor, 1)
192
+ if (quoted) {
193
+ if (escaped) escaped = 0
194
+ else if (char == "\\") escaped = 1
195
+ else if (char == "\"") quoted = 0
196
+ continue
197
+ }
198
+ if (char == "\"") {
199
+ quoted = 1
200
+ continue
201
+ }
202
+ if (char == "/" && substr(text, cursor + 1, 1) == "/") break
203
+ output = output char
204
+ }
205
+ return output
206
+ }
207
+ function without_comment(text, output, cursor, char, escaped, quoted) {
208
+ output = ""
209
+ escaped = 0
210
+ quoted = 0
211
+ for (cursor = 1; cursor <= length(text); cursor++) {
212
+ char = substr(text, cursor, 1)
213
+ if (quoted) {
214
+ output = output char
215
+ if (escaped) escaped = 0
216
+ else if (char == "\\") escaped = 1
217
+ else if (char == "\"") quoted = 0
218
+ continue
219
+ }
220
+ if (char == "\"") {
221
+ quoted = 1
222
+ output = output char
223
+ continue
224
+ }
225
+ if (char == "/" && substr(text, cursor + 1, 1) == "/") break
226
+ output = output char
227
+ }
228
+ return output
229
+ }
230
+ function check_emit() {
231
+ if (emit_code !~ /event[[:space:]]*=/ && emit_code !~ /[(,][[:space:]]*event[[:space:]]*[,)]/)
232
+ printf "event|%s|%d\n", FILENAME, emit_line
233
+ if (emit_source ~ /"[^"\n]*\{[^"\n]*\}[^"\n]*"/)
234
+ printf "positional|%s|%d\n", FILENAME, emit_line
235
+ emit_code = ""
236
+ emit_source = ""
237
+ in_emit = 0
238
+ }
239
+ FNR == 1 {
240
+ depth = 0
241
+ test_depth = 0
242
+ cfg_test = 0
243
+ in_emit = 0
244
+ emit_parens = 0
245
+ emit_code = ""
246
+ emit_source = ""
247
+ }
248
+ {
249
+ code = code_only($0)
250
+ if (!in_emit && code ~ /^[[:space:]]*$/) next
251
+ opens = count_char(code, "\\{")
252
+ closes = count_char(code, "\\}")
253
+ if (test_depth > 0) {
254
+ depth += opens - closes
255
+ if (depth < test_depth) test_depth = 0
256
+ next
257
+ }
258
+ if (code ~ /^[[:space:]]*#\[cfg\(test\)\]/) {
259
+ if (opens > 0) {
260
+ test_depth = depth + 1
261
+ depth += opens - closes
262
+ if (depth < test_depth) test_depth = 0
263
+ } else cfg_test = 1
264
+ next
265
+ }
266
+ if (cfg_test) {
267
+ if (opens > 0) {
268
+ test_depth = depth + 1
269
+ depth += opens - closes
270
+ if (depth < test_depth) test_depth = 0
271
+ cfg_test = 0
272
+ } else if (code ~ /;/) cfg_test = 0
273
+ next
274
+ }
275
+ if (code ~ /(^|[^[:alnum:]_])(println|eprintln|dbg)!/)
276
+ printf "direct|%s|%d\n", FILENAME, FNR
277
+ if (!in_emit && match(code, /tracing::(error|warn|info|debug|trace)!/)) {
278
+ in_emit = 1
279
+ emit_line = FNR
280
+ emit_code = code
281
+ emit_source = without_comment($0)
282
+ macro = substr(code, RSTART)
283
+ emit_parens = count_char(macro, "\\(") - count_char(macro, "\\)")
284
+ } else if (in_emit) {
285
+ emit_code = emit_code " " code
286
+ emit_source = emit_source " " without_comment($0)
287
+ emit_parens += count_char(code, "\\(") - count_char(code, "\\)")
288
+ }
289
+ if (in_emit && emit_parens <= 0) check_emit()
290
+ depth += opens - closes
291
+ }
292
+ ' "${rust_nontest[@]}")
293
+ fi
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # 4. BLOCKING: console.log/debug/info/warn/error in agentsfleet/src non-test.
144
297
  # ---------------------------------------------------------------------------
145
298
  console_hits=0
146
299
  if [[ ${#js_nontest[@]} -gt 0 ]]; then
@@ -155,7 +308,7 @@ if [[ ${#js_nontest[@]} -gt 0 ]]; then
155
308
  fi
156
309
 
157
310
  # ---------------------------------------------------------------------------
158
- # 4. INFO: std.log.scoped outside src/logging/ (pre-migration to the named
311
+ # 5. INFO: std.log.scoped outside src/logging/ (pre-migration to the named
159
312
  # `log` module's scoped API). The audit no longer carves out src/auth/ —
160
313
  # the named module is import-able from layer-isolated trees, so the
161
314
  # portability exception is gone.
@@ -180,7 +333,7 @@ if [[ ${#scoped_eligible[@]} -gt 0 ]]; then
180
333
  fi
181
334
 
182
335
  # ---------------------------------------------------------------------------
183
- # 5. INFO: err/warn logs without `error_code=` substring on the same line.
336
+ # 6. INFO: err/warn logs without `error_code=` substring on the same line.
184
337
  # Heuristic — captures the common case where an err/warn line should
185
338
  # embed UZ-XXX-NNN per LOGGING_STANDARD §5.
186
339
  # ---------------------------------------------------------------------------
@@ -198,16 +351,16 @@ if [[ ${#zig_nontest[@]} -gt 0 ]]; then
198
351
  fi
199
352
 
200
353
  # ---------------------------------------------------------------------------
201
- # 6. Promote INFO to BLOCK in --strict mode.
354
+ # 7. Promote INFO to BLOCK in --strict mode.
202
355
  # ---------------------------------------------------------------------------
203
356
  if [[ $STRICT -eq 1 && $INFO_COUNT -gt 0 ]]; then
204
357
  fail "--strict: $INFO_COUNT informational findings promoted to blocking"
205
358
  fi
206
359
 
207
360
  # ---------------------------------------------------------------------------
208
- # 7. Verdict.
361
+ # 8. Verdict.
209
362
  # ---------------------------------------------------------------------------
210
- ok "scanned ${#FILES[@]} files; std.debug.print=$debug_print_hits console.*=$console_hits std.log.scoped=$scoped_hits missing-error_code=$missing_code_hits"
363
+ 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"
211
364
  if [[ $FAIL -ne 0 ]]; then
212
365
  printf "\n🔴 LOGGING GATE: blocking violations. See dispatch/write_any.md (Logging Gate).\n" >&2
213
366
  exit 1
@@ -187,6 +187,40 @@ ledger_has_prefix() {
187
187
  DISPATCH_LIB_BASENAME="lib.sh"
188
188
 
189
189
  # The executable façades: dispatch/*.sh minus the shared library.
190
+ # A façade's trigger surface, declared where it belongs: in the façade. The tag
191
+ # is one HTML comment, invisible in rendered markdown, e.g.
192
+ # <!-- oracle-scope: *.rs -->
193
+ # Scope used to be readable ONLY from a leaf's dispatch_init line, so the five
194
+ # façades carrying deterministic checks had a voice and the other fourteen had
195
+ # none — a Rust author was never asked to read the Rust rules, because
196
+ # write_rust.md has no .sh. The page wins; the leaf remains the fallback so the
197
+ # mechanised façades keep the scope they already declare, in one place.
198
+ FACADE_SCOPE_TAG='^[[:space:]]*<!--[[:space:]]*oracle-scope:[^>]*-->[[:space:]]*$'
199
+
200
+ ledger_facade_pages() {
201
+ local file
202
+ for file in "$LEDGER_ROOT"/dispatch/*.md; do
203
+ [ -f "$file" ] || continue
204
+ printf '%s\n' "$file"
205
+ done
206
+ }
207
+
208
+ # Space-separated, trailing space — same shape ledger_facade_globs returns, so
209
+ # ledger_match_count reads either without knowing which declared it.
210
+ ledger_facade_scope() {
211
+ local page="$1" tag stem leaf
212
+ tag="$(grep -hE "$FACADE_SCOPE_TAG" "$page" 2>/dev/null | head -1)"
213
+ if [ -n "$tag" ]; then
214
+ printf '%s' "$tag" \
215
+ | sed -e 's/.*oracle-scope:[[:space:]]*//' -e 's/[[:space:]]*-->.*//' \
216
+ -e 's/[[:space:]][[:space:]]*/ /g' -e 's/[[:space:]]*$/ /'
217
+ return 0
218
+ fi
219
+ stem="$(basename "$page" .md)"
220
+ leaf="$(dirname "$page")/$stem.sh"
221
+ [ -f "$leaf" ] && ledger_facade_globs "$leaf"
222
+ }
223
+
190
224
  ledger_facade_scripts() {
191
225
  local file
192
226
  for file in "$LEDGER_ROOT"/dispatch/*.sh; do
@@ -208,7 +242,16 @@ ledger_facade_globs() {
208
242
  # is the match itself — the same expansion dispatch_resolve_files applies to
209
243
  # explicit file arguments, so a fire count means what the dispatcher means.
210
244
  ledger_match_count() {
211
- local globs="$1" path glob matches=0
245
+ local globs="$1" path glob matches=0 reglob=""
246
+ # `for glob in $globs` needs WORD SPLITTING and must not get PATHNAME
247
+ # EXPANSION: unguarded, bash expands each pattern against the real tree
248
+ # first, so a façade declaring 'src/*' iterates the 33 files that happen to
249
+ # exist there and then compares them as literals — src/foo.rs matches
250
+ # nothing and core/* "matches" only because it expanded to the one file in
251
+ # core/. Latent while every leaf declared extension globs ('*.zig') that
252
+ # match nothing at the root and so survive expansion unchanged; the first
253
+ # path-glob façade turns it into a scope that silently reads wrong.
254
+ case "$-" in *f*) ;; *) reglob="yes"; set -f ;; esac
212
255
  while IFS= read -r path; do
213
256
  [ -n "$path" ] || continue
214
257
  for glob in $globs; do
@@ -216,6 +259,7 @@ ledger_match_count() {
216
259
  case "$path" in $glob) matches=$((matches + 1)); break ;; esac
217
260
  done
218
261
  done
262
+ [ -n "$reglob" ] && set +f
219
263
  printf '%d' "$matches"
220
264
  }
221
265
 
@@ -101,12 +101,24 @@ check_registry_parity() {
101
101
  # is answerable from the tree — unlike "has this fired lately", which depends on
102
102
  # what happened to get committed recently and gates nothing.
103
103
  check_facade_scopes() {
104
- local script stem
104
+ local script stem page
105
+ # Iterates LEAVES, not pages: a leaf is a promise that something mechanical
106
+ # fires, so one declaring no scope is dead weight whether or not a page sits
107
+ # beside it. A façade with no leaf and no tag is situational — a lifecycle
108
+ # stage transition, a "tests pass" claim, writing a PR body — and has no
109
+ # staged-diff trigger to declare, so it is not a finding.
105
110
  while IFS= read -r script; do
106
111
  [ -n "$script" ] || continue
107
- [ -n "$(ledger_facade_globs "$script")" ] && continue
108
112
  stem="$(basename "$script" .sh)"
109
- fail "facade=$stem declares no dispatch_init scope — dispatch/$stem.sh (nothing it carries can ever fire)"
113
+ page="$LEDGER_ROOT/dispatch/$stem.md"
114
+ # Either surface may carry the scope; ledger_facade_scope prefers the page
115
+ # and falls back to this leaf, so both spellings satisfy the same check.
116
+ if [ -f "$page" ]; then
117
+ [ -n "$(ledger_facade_scope "$page")" ] && continue
118
+ else
119
+ [ -n "$(ledger_facade_globs "$script")" ] && continue
120
+ fi
121
+ fail "facade=$stem declares no scope — no oracle-scope tag in dispatch/$stem.md and no dispatch_init in dispatch/$stem.sh (nothing it carries can ever fire)"
110
122
  done < <(ledger_facade_scripts)
111
123
  }
112
124
 
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # dispatch/edit_rules.sh — the rule-corpus dispatch, DETERMINISTIC façade.
3
+ #
4
+ # Pairs with dispatch/edit_rules.md (the LATENT façade — prose the agent reads).
5
+ #
6
+ # This leaf exists for the reason RULE UFS exists in the Rust pack: a façade
7
+ # that declares its scope in PROSE declares it to nobody. edit_rules.md has
8
+ # always named its trigger surface — core/**, packs/**, schemas/**, src/**,
9
+ # registry.json, governance hooks (edit_rules.md:15, and the same row in
10
+ # docs/EXECUTE_DOC_READS.md) — but audits/doc-read.sh derives what fires from
11
+ # `dispatch_init` globs in a leaf, and this façade had no leaf. So every
12
+ # governance edit reported "nothing staged triggers a façade" and the DOC READ
13
+ # GATE could not fire on the one surface whose edits change the rules
14
+ # themselves. The globs below ARE that scope, now in a form a gate can read.
15
+ #
16
+ # dispatch/edit_rules.sh <file> [...] # explicit targets (EXECUTE)
17
+ # dispatch/edit_rules.sh --staged # staged corpus files (CONFORM, pre-commit)
18
+ #
19
+ # Layering: AGENTS.md → edit_rules.md (latent) → edit_rules.sh (this) → audits/*.sh
20
+ # Signals: 🟢 pass · 🔴 fail (blocks) · 🤔 DECIDE (judgment; blocks the TURN, not the script)
21
+ # Exit: 0 = mechanical gates pass · 1 = ≥1 failed · 2 = usage error.
22
+
23
+ source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
24
+
25
+ # Scope mirrors docs/EXECUTE_DOC_READS.md's governance row and edit_rules.md:15.
26
+ # Kept in one order with them: a glob here that the row does not name is a leaf
27
+ # reading more than the façade declares, which is the same drift inverted.
28
+ # ONE LINE, deliberately: ledger_facade_globs reads `head -1` of the
29
+ # dispatch_init line, so a backslash continuation declares an EMPTY scope and
30
+ # the façade silently stops firing — the exact failure this leaf exists to end.
31
+ dispatch_init "RULES" 'core/*' 'packs/*' 'schemas/*' 'src/*' 'registry.json' 'dispatch/*' 'audits/*' '.githooks/*' 'AGENTS.md' 'AGENTS.orly.md'
32
+ dispatch_resolve_files "$@"
33
+ dispatch_header
34
+
35
+ # ── deterministic gates ────────────────────────────────────────────
36
+ # Rule prose carries no milestone ids — the packs say so themselves ("never a
37
+ # memory file, never a milestone cite"), and until now nothing read the corpus
38
+ # for them. Same leaf write_any already wires, so no new code enters the gloss.
39
+ dispatch_run_helper "MSID" "msid-ui.sh" "--staged"
40
+
41
+ dispatch_verdict
@@ -127,6 +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
131
  - `*.ts`/`*.tsx`/`*.js`/`*.jsx` outside `vendor/`/`node_modules/`/`*.test.*`/`*.spec.*` — `console.*`, custom logger calls.
131
132
  - `*.sh` outside generated dirs — `echo`/`printf` to `&2`.
132
133
 
@@ -145,10 +146,11 @@ shebang and is the authoritative check.
145
146
  | Pattern | Rule |
146
147
  |---|---|
147
148
  | New `logging.scoped(.tag)` call | Scope is a Zig enum literal — adding a new tag is freeform. `event` must be snake_case `verb_noun`. |
149
+ | New Rust `tracing::<level>!` call | `event = "verb_noun"` or the equivalent `event` field shorthand is mandatory; values are structured fields hoisted into locals. |
148
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. |
149
151
  | Per-iteration / hot-loop log | Use `debug` (hidden by default), not `info`. |
150
- | `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). |
151
- | `console.log`/`std.debug.print` in non-test source | Forbidden. Convert to logger or delete before commit. |
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. |
152
154
  | `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. |
153
155
  | `msg=` field | ≤ 300 chars. Stack traces emit as separate `event=stack_trace` debug record. |
154
156
  | Multi-line values | Newlines must be `\n` literal (two chars), not raw newline byte. |
@@ -173,7 +175,7 @@ Full multi-line block fires when a sub-rule reports a violation:
173
175
 
174
176
  ```
175
177
  LOGGING GATE: <file>
176
- docs/LOGGING_STANDARD.md sections consulted: §3 (wire format), §4 (severity), §5 (error codes), §6 (PII), §7 (zig binding) | §8 (TS binding), §10A (tightenings)
178
+ docs/LOGGING_STANDARD.md sections consulted: §3 (wire format), §4 (severity), §5 (error codes), §6 (PII), §7 (Zig binding) | §8 (TS binding) | §8A (Rust binding), §10A (tightenings)
177
179
  Wire format: <logfmt ✓ | violation: <where>>
178
180
  Required keys: <ts_ms,level,scope,event present ✓ | violation: <missing>>
179
181
  Severity choice: <within rules ✓ | violation: <e.g. info on per-iteration path>>
@@ -188,13 +190,13 @@ LOGGING GATE: <file>
188
190
 
189
191
  > [DETERMINISTIC → LOG]
190
192
 
191
- `logging.sh` walks the **full `src/` + `agentsfleet/src/` working tree** via `git ls-files`. The index includes staged-but-not-yet-committed content, 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.
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.
192
194
 
193
195
  #### End-of-turn audit
194
196
 
195
197
  > [DETERMINISTIC → LOG]
196
198
 
197
- `audits/logging.sh` runs as part of `make lint`. It greps `std.log.*`, `console.*` and raw stderr writes — it does NOT see calls through the `log` named module's `scoped()`, which is where every structured emit in `agentsfleet` goes, so a clean run is not evidence that §4/§5 hold. Mechanical today: `std.debug.print`/`console.log` presence, `std.log.scoped` outside `src/lib/logging/`, msg-length. Reviewer-side: severity choice, entry/exit pairing, `error_code` on scoped-logger calls, and PII spot-checks.
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.
198
200
 
199
201
  #### Family
200
202
 
@@ -736,6 +738,34 @@ Every triggered consult is logged in the active spec's **Discovery** section, or
736
738
 
737
739
  NLR is the cleanup-on-touch arm; NLG bans new legacy framing pre-v2.0.0; this guard covers the harder judgment calls ("should this whole subsystem exist") that need the user's input.
738
740
 
741
+ ## Porting a codebase between languages (RULE PORT)
742
+
743
+ > [JUDGMENT → NEW:PORT]
744
+
745
+ A port is a REWRITE with a conformance test, never a transliteration.
746
+
747
+ Before porting a construct, ask what it was working AROUND, not what it was.
748
+ A thread with a stop flag is usually the absence of async. A registration map
749
+ with generation counters is usually the absence of structured cancellation. A
750
+ defer chain is usually the absence of ownership. Port the GUARANTEE; delete the
751
+ workaround, and write down which guarantee replaced it.
752
+
753
+ Every construct carried over needs one sentence: "kept because <the target
754
+ language has no better mechanism>". If that sentence cannot be written
755
+ honestly, it is transliteration.
756
+
757
+ Conformance is on OBSERVABLE behaviour — wire bytes, rows, status codes, money,
758
+ exit codes. Never on internal shape. A test that pins internal shape freezes
759
+ the source language's debt into the target.
760
+
761
+ Read the reference implementations the spec names before designing. Take the
762
+ SHAPE and reject the specifics.
763
+
764
+ Name the tech debt you are NOT carrying over, and where it went.
765
+
766
+ A milestone titled "parity" means behavioural parity. It never licenses a
767
+ one-to-one file, type, or thread mapping.
768
+
739
769
  ## Comment Voice — New to the Codebase, Familiar with the Goal
740
770
 
741
771
  > [JUDGMENT → NEW:CMT]
@@ -1,5 +1,7 @@
1
1
  # Go authoring
2
2
 
3
+ <!-- oracle-scope: *.go -->
4
+
3
5
  Return errors, do not panic across a package boundary. Wrap with `%w` so the
4
6
  caller can still match the cause, and add context the caller does not already
5
7
  have — never re-wrap the same fact twice.
@@ -1,5 +1,7 @@
1
1
  # Markdown JSX authoring
2
2
 
3
+ <!-- oracle-scope: *.mdx -->
4
+
3
5
  Treat Markdown JSX (MDX) as a language, not generic prose. Validate front matter,
4
6
  component nesting, links, code fences, and accessible image descriptions.
5
7
 
@@ -1,5 +1,7 @@
1
1
  # Python authoring
2
2
 
3
+ <!-- oracle-scope: *.py -->
4
+
3
5
  Use standard-library parsers for structured input. Keep resource ownership in
4
6
  context managers. Validate data at parse boundaries and preserve specific
5
7
  exception classes until the caller can decide how to respond.
@@ -1,5 +1,7 @@
1
1
  # Rust authoring
2
2
 
3
+ <!-- oracle-scope: *.rs -->
4
+
3
5
  Use ownership and borrowing to make resource lifetime visible. Keep `unsafe`
4
6
  blocks small and state the invariant they rely on. Preserve error variants until
5
7
  the caller has enough information to decide whether to retry or stop.
@@ -1,5 +1,7 @@
1
1
  # Shell authoring
2
2
 
3
+ <!-- oracle-scope: *.sh -->
4
+
3
5
  Quote expansions, use arrays for argument lists, and clean temporary files on
4
6
  every exit path. Avoid `eval` for external input. Match the repository's required
5
7
  shell and macOS Bash compatibility level.
@@ -10,10 +10,13 @@ Every triggered document requires one `📖 DOC READ: <path>` proof-line **per t
10
10
  | `core/**`, `packs/**`, `schemas/**`, `src/**`, `registry.json`, agent-home instruction links, repository rules synchronization, or governance hooks | `dispatch/edit_rules.md` + `docs/ORLY_ARCHITECTURE.md` — canonical sources, profiles, snapshots, refusal boundaries, and evidence. | <!-- oracle-packs:workflow.governance -->
11
11
  | Lifecycle stage transition (CHORE(open)/PLAN/CHORE(close)/LAND, worktree setup, milestone bootstrap) | `dispatch/lifecycle.md` — the entered stage's runbook section only (section-scan; fires on the transition itself, not on an Edit/Write). |
12
12
  | Any source file (cross-cutting authoring) | `dispatch/write_any.md` — length, logging, milestone-id, error-registry, UFS, greptile read, legacy-workaround family. |
13
+ | Porting a codebase, module, or subsystem from another language | `dispatch/write_any.md` §Porting a codebase between languages — a port is a rewrite with a conformance test: port the guarantee, delete the workaround, conform on observable behaviour, name the debt not carried over. |
13
14
  | Spec's "Applicable Rules" | Each rule (canonical). Missing → the cross-cutting codes of `dispatch/write_any.md` are the floor; surface omission. |
14
15
  | `*.zig` | `dispatch/write_zig.md`. ZIG GATE per edit. |
15
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. |
16
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 -->
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 -->
17
20
  | `*.py` | `dispatch/write_python.md` — parsing boundaries, resource ownership, and specific exceptions. |
18
21
  | `*.sh` | `dispatch/write_shell.md` — quoting, array arguments, cleanup, input safety, and shell compatibility. |
19
22
  | `*.mdx` | `dispatch/write_mdx.md` — structure, front matter, links, code fences, accessibility, and Mintlify isolation. | <!-- oracle-packs:language.mdx -->