@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.
- package/.githooks/pre-commit +5 -1
- package/audits/agents-md.md +2 -2
- package/audits/doc-read.sh +14 -12
- package/audits/logging.sh +164 -11
- package/audits/rule-ledger-lib.sh +45 -1
- package/audits/rule-ledger.sh +15 -3
- package/dispatch/edit_rules.sh +41 -0
- package/dispatch/write_any.md +35 -5
- package/dispatch/write_go.md +2 -0
- package/dispatch/write_mdx.md +2 -0
- package/dispatch/write_python.md +2 -0
- package/dispatch/write_rust.md +2 -0
- package/dispatch/write_shell.md +2 -0
- package/docs/EXECUTE_DOC_READS.md +3 -0
- package/docs/LOGGING_STANDARD.md +60 -5
- package/docs/ORLY_ARCHITECTURE.md +19 -0
- package/docs/RULE_ENFORCEMENT.md +3 -3
- package/docs/greptile-learnings/RULES.md +7 -0
- package/package.json +1 -1
- package/packs/language/go/rules.md +2 -0
- package/packs/language/mdx/rules.md +2 -0
- package/packs/language/python/rules.md +2 -0
- package/packs/language/rust/rules.md +130 -0
- package/packs/language/shell/rules.md +2 -0
- package/registry.json +4 -0
- package/skills/orly-babysit-prs/SKILL.md +6 -0
- package/skills/orly-spec-new/SKILL.md +6 -0
- package/skills/orly-write-integration-test/SKILL.md +6 -0
- package/skills/orly-write-unit-test/SKILL.md +6 -0
- package/src/cli.test.ts +83 -2
- package/src/cli.ts +62 -18
- package/src/install.test.ts +3 -2
- package/src/install.ts +2 -2
- package/src/telemetry.test.ts +308 -0
- package/src/telemetry.ts +350 -0
- package/src/telemetry_sync.ts +134 -0
package/.githooks/pre-commit
CHANGED
|
@@ -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
|
-
|
|
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
|
package/audits/agents-md.md
CHANGED
|
@@ -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) |
|
|
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 |
|
package/audits/doc-read.sh
CHANGED
|
@@ -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.
|
|
98
|
-
#
|
|
99
|
-
#
|
|
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"
|
|
102
|
-
while IFS= read -r
|
|
103
|
-
[ -n "$
|
|
104
|
-
|
|
105
|
-
[ -
|
|
106
|
-
|
|
107
|
-
[
|
|
108
|
-
|
|
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
|
|
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:
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
package/audits/rule-ledger.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dispatch/write_any.md
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
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
|
|
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]
|
package/dispatch/write_go.md
CHANGED
package/dispatch/write_mdx.md
CHANGED
package/dispatch/write_python.md
CHANGED
|
@@ -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.
|
package/dispatch/write_rust.md
CHANGED
|
@@ -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.
|
package/dispatch/write_shell.md
CHANGED
|
@@ -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 -->
|