@agentsfleet/orly 0.5.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.
Files changed (112) hide show
  1. package/.githooks/pre-commit +42 -0
  2. package/.githooks/pre-push +67 -0
  3. package/AGENTS.md +348 -0
  4. package/LICENSE +21 -0
  5. package/README.md +249 -0
  6. package/SOUL.md +90 -0
  7. package/audits/agents-md.md +497 -0
  8. package/audits/agents-md.sh +353 -0
  9. package/audits/cross-tier-rates.sh +105 -0
  10. package/audits/data.sh +185 -0
  11. package/audits/deinit-pairs.sh +224 -0
  12. package/audits/design-tokens.sh +143 -0
  13. package/audits/doc-read.sh +183 -0
  14. package/audits/error-codes.sh +221 -0
  15. package/audits/fixtures/clean.diff +15 -0
  16. package/audits/fixtures/dirty.diff +16 -0
  17. package/audits/lifecycle-anchors.sh +22 -0
  18. package/audits/logging.sh +217 -0
  19. package/audits/msid-ui.sh +144 -0
  20. package/audits/parity-dispatch.sh +51 -0
  21. package/audits/rule-ledger-lib.sh +264 -0
  22. package/audits/rule-ledger.sh +225 -0
  23. package/audits/rule-paths.sh +108 -0
  24. package/audits/spec-template.sh +258 -0
  25. package/audits/sql-mod.sh +102 -0
  26. package/audits/ufs.sh +303 -0
  27. package/bin/orly +26 -0
  28. package/core/operating-model.md +256 -0
  29. package/dispatch/edit_rules.md +69 -0
  30. package/dispatch/lib.sh +213 -0
  31. package/dispatch/lifecycle.md +187 -0
  32. package/dispatch/name_architecture.md +93 -0
  33. package/dispatch/verify.md +111 -0
  34. package/dispatch/write_any.md +740 -0
  35. package/dispatch/write_any.sh +49 -0
  36. package/dispatch/write_auth.md +36 -0
  37. package/dispatch/write_changelog.md +64 -0
  38. package/dispatch/write_documentation.md +45 -0
  39. package/dispatch/write_go.md +24 -0
  40. package/dispatch/write_http.md +45 -0
  41. package/dispatch/write_mdx.md +7 -0
  42. package/dispatch/write_python.md +8 -0
  43. package/dispatch/write_rust.md +38 -0
  44. package/dispatch/write_shell.md +8 -0
  45. package/dispatch/write_spec.md +105 -0
  46. package/dispatch/write_sql.md +112 -0
  47. package/dispatch/write_sql.sh +36 -0
  48. package/dispatch/write_ts_adhere_bun.md +493 -0
  49. package/dispatch/write_ts_adhere_bun.sh +37 -0
  50. package/dispatch/write_zig.md +852 -0
  51. package/dispatch/write_zig.sh +40 -0
  52. package/docs/CHANGELOG_VOICE.md +18 -0
  53. package/docs/DISPATCH_ARCHITECTURE.md +664 -0
  54. package/docs/DISPATCH_REVIEW_DISPOSITION.md +65 -0
  55. package/docs/DOCUMENTATION_RULES.md +209 -0
  56. package/docs/EXECUTE_DOC_READS.md +29 -0
  57. package/docs/HARNESS_VERIFY_OUTPUT.md +40 -0
  58. package/docs/LIFECYCLE_PATTERNS.md +316 -0
  59. package/docs/LOGGING_STANDARD.md +397 -0
  60. package/docs/ORLY_ARCHITECTURE.md +129 -0
  61. package/docs/REST_API_DESIGN_GUIDELINES.md +783 -0
  62. package/docs/RULE_ENFORCEMENT.md +41 -0
  63. package/docs/SCHEMA_CONVENTIONS.md +93 -0
  64. package/docs/TEMPLATE.md +411 -0
  65. package/docs/VERIFY_TIERS.md +57 -0
  66. package/docs/greptile-learnings/README.md +51 -0
  67. package/docs/greptile-learnings/RULES.md +1045 -0
  68. package/fixtures/registry-invalid.json +1 -0
  69. package/fixtures/registry-valid.json +18 -0
  70. package/fixtures/unclosed-pack-block.md +8 -0
  71. package/package.json +63 -0
  72. package/packs/language/go/rules.md +24 -0
  73. package/packs/language/mdx/rules.md +7 -0
  74. package/packs/language/python/rules.md +8 -0
  75. package/packs/language/rust/rules.md +38 -0
  76. package/packs/language/shell/rules.md +8 -0
  77. package/registry.json +405 -0
  78. package/schemas/evidence.schema.json +43 -0
  79. package/schemas/profile.schema.json +12 -0
  80. package/schemas/registry.schema.json +12 -0
  81. package/skills/orly-babysit-prs/SKILL.md +326 -0
  82. package/skills/orly-spec-new/SKILL.md +157 -0
  83. package/skills/orly-write-integration-test/SKILL.md +470 -0
  84. package/skills/orly-write-unit-test/SKILL.md +520 -0
  85. package/src/cli.test.ts +70 -0
  86. package/src/cli.ts +287 -0
  87. package/src/config.test.ts +226 -0
  88. package/src/config.ts +288 -0
  89. package/src/criteria.test.ts +137 -0
  90. package/src/criteria.ts +166 -0
  91. package/src/criteria_spec.ts +127 -0
  92. package/src/criteria_support.ts +53 -0
  93. package/src/gates.test.ts +183 -0
  94. package/src/gates.ts +169 -0
  95. package/src/gates_spec.test.ts +98 -0
  96. package/src/gates_test_support.ts +194 -0
  97. package/src/git_env.ts +20 -0
  98. package/src/install.test.ts +514 -0
  99. package/src/install.ts +447 -0
  100. package/src/install_nested.test.ts +32 -0
  101. package/src/model.test.ts +31 -0
  102. package/src/model.ts +245 -0
  103. package/src/references.test.ts +47 -0
  104. package/src/references.ts +147 -0
  105. package/src/render.test.ts +60 -0
  106. package/src/render.ts +94 -0
  107. package/src/surfaces.test.ts +43 -0
  108. package/src/surfaces.ts +89 -0
  109. package/src/validation.test.ts +11 -0
  110. package/src/validation.ts +87 -0
  111. package/src/verify.test.ts +13 -0
  112. package/src/verify.ts +59 -0
@@ -0,0 +1,397 @@
1
+ # LOGGING_STANDARD — wire format and discipline for structured logs
2
+
3
+ The contract between code that emits log records and the systems (humans, collectors, dashboards) that consume them. Cross-language: applies to every Zig, TypeScript, JavaScript, and shell call site that emits to `stderr`/`stdout`.
4
+
5
+ Pre-design rules, decisive defaults, and the anti-patterns each rule exists to prevent. Every rule has a one-line "why" so you can judge edge cases.
6
+
7
+ ## §1 · Scope
8
+
9
+ Triggers on every `Edit`/`Write` that adds, removes, or changes a log emit:
10
+
11
+ - `*.zig` outside `vendor/`/`third_party/`/`.zig-cache/` — `std.log.*`, `std.debug.print`, `std.io.getStdErr().writer().print`, any helper in `src/lib/logging/`.
12
+ - `*.ts`/`*.tsx`/`*.js`/`*.jsx` outside `vendor/`/`node_modules/` — `console.*`, custom logger calls.
13
+ - `*.sh` outside generated directories — `echo`, `printf` to `&2`.
14
+
15
+ Out of scope (explicitly):
16
+
17
+ - Test-only diagnostic prints inside `*_test.zig`, `*.test.ts`, `*.spec.ts`. Tests render to humans, not collectors; gates ignore.
18
+ - Build/release scripts in `~/Projects/dotfiles/`, `audits/release-*` — toolchain output, not application logs.
19
+ - Generated framework noise (Next.js startup banners, Bun runtime warnings) — out of our control.
20
+
21
+ The **LOGGING GATE** (`dispatch/write_any.md`, Logging Gate) sits on top of this file — it fires in addition to the language-level rules in `dispatch/write_zig.md` and `dispatch/write_ts_adhere_bun.md`, not instead of them.
22
+
23
+ ## §2 · Today's de-facto standard (survey-derived)
24
+
25
+ Documented honestly, not aspirationally. Pre-M62 baseline (the fix-pass converges every existing emit toward §3 onward):
26
+
27
+ - Zig calls are mostly `std.log.scoped(.tag).info(comptime fmt, args)` with positional `{s} {d}` placeholders. Severity choice is inconsistent: successful happy-path events sometimes log at `info`, sometimes are silent. Per-call migration to the structured `log.<level>("event", .{ .field = val })` form is in flight.
28
+
29
+ > [DETERMINISTIC → LOG]
30
+
31
+ - A small number of Zig sites use `std.debug.print` directly — bypasses the scope/level system entirely. Always a violation.
32
+
33
+ > [UNENFORCED → umbrella pointer; enforcement lives in the tagged clauses §3 onward, not in this survey sentence itself]
34
+
35
+ - The named module `log` (source: `src/lib/logging/mod.zig`) exposes `scoped(.tag)` returning a logger struct with `.err` / `.warn` / `.info` / `.debug` methods. M62 removed the older free-function helpers (`logErr` / `logErrWithHint` / `logWarnErr`) — there is no compat shim.
36
+ - TypeScript (`cli/src/**`) calls `console.log`/`console.error` directly. No structure. No scope. No severity beyond err vs out.
37
+ - Error-code embedding (`UZ-XXX-NNN`) appears on some `err` lines but not others. The registry (`src/agentsfleetd/errors/error_registry.zig`) is the source of truth, but enforcement is voluntary.
38
+ - No collector-friendly format. Logs are a mix of free-form English and ad-hoc `key={value}` fragments.
39
+
40
+ The proposed standard below is what every new emit must conform to and what the fix-pass converges existing emits toward.
41
+
42
+ ## §3 · Wire format — logfmt
43
+
44
+ Every record is a single newline-terminated line in **logfmt**: space-separated `key=value` pairs, value-quoted only when the value contains whitespace or `=` or `"`.
45
+
46
+ ```
47
+ ts_ms=1715004901234 level=warn scope=executor event=tool_call_failed correlation_id=abc-123 error_code=UZ-EXEC-012 tool=bash duration_ms=1240 msg="tool returned non-zero"
48
+ ```
49
+
50
+ > [UNENFORCED → record shape is a runtime-rendered property; no linter parses emitted logfmt for key presence/order, and none of this section is checked by audits/logging.sh]
51
+
52
+ **Required keys per record (in this order):**
53
+
54
+ | Key | Type | Required | Notes |
55
+ |---|---|---|---|
56
+ | `ts_ms` | u64 | always | Unix epoch milliseconds. Single source of truth for ordering. |
57
+ | `level` | enum | always | One of `err`, `warn`, `info`, `debug`, `trace`. See §4. |
58
+ | `scope` | snake_case ident | always | Subsystem tag (e.g. `executor`, `http`, `auth`, `redis`). See §7 for the binding. |
59
+ | `event` | snake_case `verb_noun` | always | What happened. Examples: `request_started`, `tool_call_failed`, `worker_drained`. |
60
+ | `error_code` | `UZ-XXX-NNN` | required on `err`/`warn` mapping to a registry code | See §5. |
61
+ | `correlation_id` | string | optional | Trace context where present. |
62
+ | `msg` | string | optional | Human-readable elaboration. **Last** field by convention so multiline-ish msgs don't break grep. |
63
+ | `<arbitrary>` | any | optional | Domain fields (`tool=bash`, `duration_ms=1240`, `worker_id=42`). Snake_case keys. |
64
+
65
+ **Encoding rules:**
66
+
67
+ - Values with no whitespace, `=`, or `"`: bare (`duration_ms=1240`, `tool=bash`).
68
+ - Values with whitespace or `=` or `"`: double-quoted, with `"` and `\` backslash-escaped (`msg="tool returned: \"exit 1\""`).
69
+ - Newlines in values: replaced with `\n` literal (two chars). Logfmt is one-line-per-record; embedded newlines break tooling.
70
+ - Booleans: `true` / `false`.
71
+ - Numbers: bare integers and decimals. No thousands separators.
72
+ - Slices/arrays: omit. If you need to log a collection, log it as count + sample (`count=42 sample=abc,def,ghi`) or emit one record per item.
73
+ - Null/absent: omit the key entirely. **Don't emit `key=null` or `key=`** — empty/null fields confuse parsers.
74
+
75
+ **Why logfmt and not JSON:** logfmt is grep-friendly for `tail -f`, parses cleanly into Loki / Vector / Promtail / Mezmo / Datadog with zero transform, and is readable without tooling. JSON would also work but is uglier on a dev box. logfmt is the production-server sweet spot. See §9 for the optional pretty-printer.
76
+
77
+ ## §4 · Severity contract
78
+
79
+ > [UNENFORCED → the closed method set on LoggerFor (§7) blocks a sixth level at compile time; audits/logging.sh does not check level count]
80
+
81
+ Five levels. Use them as defined; do not invent intermediate levels.
82
+
83
+ | Level | When | Examples | Default visibility |
84
+ |---|---|---|---|
85
+ | `err` | Operation failed; user/operator action required; data may be inconsistent. | DB connection lost, auth failure, panic recovery, syscall failure that breaks a request. | always emitted |
86
+ | `warn` | Operation succeeded but with degraded behaviour, or an expected failure on a recoverable path. | Retry succeeded after one failure; cache miss to slow path; deprecated endpoint hit. | always emitted |
87
+ | `info` | Operation boundaries and operationally significant state changes. | Server start/stop, an operation's `_started`/`_completed` pair, migration applied, batch boundary, a subsystem coming up. | emitted in production |
88
+ | `debug` | Exhaustive diagnostic detail — the level you switch **on** to debug. | Per-iteration tool calls, full request/response shapes, every branch of a multi-step pipeline, cache lookup outcomes. | hidden by default; opt-in via env var (see §7) |
89
+ | `trace` | Per-byte / per-event firehose. | Bytes-on-the-wire, parser tokens, allocator events. | hidden by default; opt-in only; rarely written |
90
+
91
+ `info` has three rules, in this order. The first is a duty to log, the second a
92
+ licence, the third the only prohibition.
93
+
94
+ > [JUDGMENT → BOUNDARY]
95
+
96
+ **1. REQUIRED — bracket every boundary-crossing operation.** An operation that
97
+ crosses a process, network, datastore, or subprocess boundary, or that bookends a
98
+ lifecycle phase, MUST emit `<thing>_started` on entry and exactly one of
99
+ `<thing>_completed` / `<thing>_failed` on **every** exit path, carrying an id
100
+ that correlates the pair. Boot phases, migrations, lease lifecycle, outbound
101
+ vendor calls, subprocess execution, subsystem startup and shutdown all qualify.
102
+
103
+ This one is a requirement rather than advice because the failure it prevents is
104
+ invisible until an incident: an entry with no exit means a hang reads exactly
105
+ like work that was never reached, and that is the state an operator cannot
106
+ debug. An exit with no entry is the same defect from the other side — you cannot
107
+ tell how long it took or whether it ran twice. Adding the second half of a pair
108
+ is never scope creep.
109
+
110
+ It is narrow on purpose. It binds operations that can *hang*, not functions. Do
111
+ not manufacture a `_started` for a pure computation, an accessor, or anything
112
+ that cannot fail slowly.
113
+
114
+ **2. OPEN — everything else is the engineer's judgement.** There is no allow-list
115
+ of permitted event names, and none may be introduced. An engineer who judges a
116
+ state change worth an operator's attention logs it at `info` and owes nobody a
117
+ justification; a reviewer asking for its removal owes a concrete reason. The cost
118
+ of a missing `info` is an incident nobody can bisect. The cost of an extra one is
119
+ a line in a log. Those are not symmetric, so the default is to log it.
120
+
121
+ **3. PROHIBITED — per-iteration paths are `debug`.** A loop body, a per-row step,
122
+ a per-chunk callback: these flood collectors and drown the records that matter.
123
+ `debug` is not a lesser level, it is the level with a volume switch. A sweep that
124
+ reaps 4,000 workspaces logs each one at `debug` and its outcome at `info`.
125
+
126
+ ### `err` and `warn` are unconditional
127
+
128
+ > [UNENFORCED → "always emitted" is a runtime filter-configuration property; nothing greps for a suppressed err/warn path]
129
+
130
+ Both are **always emitted**, in every build and every environment, and there is
131
+ no volume argument against either. If a code path can fail, its failure is
132
+ logged — a silent `catch {}` on a path that can genuinely fail is the defect,
133
+ not the log line that would have named it. §5 governs what the record carries;
134
+ this rule governs that the record exists at all.
135
+
136
+ There is no `fatal` level (§11). A condition that ends the process is `err`
137
+ immediately before the exit, so the last line an operator reads is the reason.
138
+
139
+ ### Detail and redaction
140
+
141
+ > [UNENFORCED → broad principle; the M42_002 harness redacts executor-child stdout, not every log call site in the codebase]
142
+
143
+ `info` carries the fields that make a significant event actionable — ids,
144
+ counts, outcomes, durations, the resolved configuration source. It is not a bare
145
+ event name. `debug` carries whatever it takes to diagnose: full payload shapes,
146
+ every branch taken, intermediate state.
147
+
148
+ **Redaction is level-independent.** §6 applies identically at `debug` and at
149
+ `trace`. "Exhaustive" describes *volume and breadth*, never permission to log a
150
+ credential: a secret that only appears when debugging is on is still a secret in
151
+ a log file, and debugging is on precisely when something is already going wrong.
152
+ > [JUDGMENT → REDACT]
153
+
154
+ Log the source (`source=env:OPENAI_API_KEY`), never the value, at every level.
155
+
156
+ > [UNENFORCED → same closed-method-set reasoning as §4 — compile-time, not audits/logging.sh]
157
+
158
+ `note` is **NOT** a level. Bun has it; we do not. Five levels only.
159
+
160
+ ## §5 · Error-code embedding
161
+
162
+ > [DETERMINISTIC → LOG]
163
+
164
+ Every `err` and `warn` record that maps to a domain error MUST carry `error_code=UZ-XXX-NNN` where `UZ-XXX-NNN` is declared in `src/agentsfleetd/errors/error_registry.zig`.
165
+
166
+ - **Used-but-undeclared** (`UZ-FAKE-999` appearing in code, no entry in registry): **blocking** in `make lint`.
167
+ - **Declared-but-unreferenced** (registry has `UZ-LEGACY-007`, no code references it): **informational**. Deletion may be deferred to a sweep milestone.
168
+
169
+ Embed the code as a struct field on the logger call: `log.err("event_name", .{ .error_code = error_codes.ERR_X, .err = @errorName(err) })`. The encoding helpers in `src/lib/logging/mod.zig` serialize it as `error_code=UZ-XXX-NNN` per §3. CLI (`agentsfleet`) renders the code in human format and as `code: "UZ-XXX-NNN"` in `--json` output (see §8).
170
+
171
+ System-level failures with no domain meaning (e.g. raw `EACCES` from a syscall before we attribute it to a tenant operation) emit without `error_code`. The follow-up rule: if a syscall failure surfaces to a user, it gets attributed to a registry code at the boundary.
172
+
173
+ ## §6 · PII / secret discipline
174
+
175
+ > [UNENFORCED → broad principle; no scan covers every log call site in the codebase for credential-shaped values]
176
+
177
+ Inherits the redaction list from M42_002 (`src/runner/engine/runner_progress.zig`). Same secret values must not appear anywhere in log records.
178
+
179
+ > [JUDGMENT → REDACT]
180
+
181
+ - **Allocator outputs** (Postgres connection strings, Redis URLs, OAuth tokens, OpenAI keys) — never log. If a record needs to identify a config source, log the *source* (`source=env:OPENAI_API_KEY`), not the *value*.
182
+ - **Tenant-supplied secrets** (workspace API keys, BYOK credentials) — redaction harness covers stdout/stderr from executor children. Log emit sites in this codebase MUST NOT bypass the harness.
183
+ - **Stderr coverage gap** — today's redactor covers child stdout only. Closing this gap (extending to stderr) is part of M62_001's fix-pass, not a separate milestone.
184
+
185
+ > [JUDGMENT → MSG-REVIEW]
186
+
187
+ - **`msg=` fields** — values copy-pasted into `msg=` are the most common leak source. Audit script flags long `msg=` values; reviewer must verify they don't carry credentials.
188
+
189
+ When in doubt, omit. A missing field is recoverable; a leaked secret is not.
190
+
191
+ ## §7 · Per-language binding — Zig
192
+
193
+ The wire format above is produced by helpers in `src/lib/logging/mod.zig`, exposed as the named module `log` so any layer-isolated tree (auth/, executor/) can import it without violating layer rules. Call sites use:
194
+
195
+ ```zig
196
+ const logging = @import("log"); // named module, see build.zig
197
+ const log = logging.scoped(.executor); // compile-time enum-literal tag
198
+
199
+ log.info("tool_call_started", .{
200
+ .correlation_id = req.id,
201
+ .tool = "bash",
202
+ });
203
+
204
+ log.warn("tool_call_failed", .{
205
+ .error_code = "UZ-EXEC-012",
206
+ .tool = "bash",
207
+ .duration_ms = elapsed,
208
+ .msg = "tool returned non-zero",
209
+ });
210
+ ```
211
+
212
+ **Mechanics:**
213
+
214
+ > [UNENFORCED → would need semantic call-site analysis; regex-based audits/logging.sh cannot distinguish manual string assembly from a helper call, and cannot see runtime env-var re-check behaviour]
215
+
216
+ - `logging.scoped(.tag)` returns a type bound to that compile-time scope (the `.tag` is an enum literal). The returned type exposes `.err`, `.warn`, `.info`, `.debug` methods.
217
+ - Each method takes a comptime `event: []const u8` (snake_case `verb_noun`) and an anonymous-struct of fields.
218
+ - Field encoding to logfmt happens in the helper, not at the call site. Call sites never assemble strings.
219
+ - Optional fields with null payload are omitted (no `key=null` / `key=`) per §3.
220
+
221
+ **Compile-time visibility short-circuit** — pattern adapted from Bun's `Output.scoped` (`bun:src/output.zig:869`). Hidden scopes compile to **zero instructions**:
222
+
223
+ ```zig
224
+ pub fn LoggerFor(comptime scope: Scope) type {
225
+ return struct {
226
+ pub inline fn debug(comptime event: Event, fields: anytype) void {
227
+ if (comptime scope_visibility(scope, .debug) == .hidden) return;
228
+ emit(.debug, scope, event, fields);
229
+ }
230
+ // ... err / warn / info / trace
231
+ };
232
+ }
233
+ ```
234
+
235
+ The `comptime if ... return` makes the entire emit path disappear when the scope is gated off. A `debug`-tagged log call in a tight loop, when its scope is gated off, costs zero. Faster than any runtime-flag library.
236
+
237
+ **Runtime visibility override** — env var `AGENTSFLEET_LOG_<SCOPE>=debug` (e.g. `AGENTSFLEET_LOG_EXECUTOR=debug`) flips a scope to debug-visible without recompiling. Read once at startup, cached. Production never re-checks env per call.
238
+
239
+ **Field encoding is allocation-free on the hot path** — the helper writes directly to a thread-local 4 KiB buffer and flushes on `\n`. Spillover to heap only on records exceeding 4 KiB, which is rare and warns at debug.
240
+
241
+ **Anti-patterns flagged by `logging.sh`:**
242
+
243
+ | Pattern | Why banned | Fix |
244
+ |---|---|---|
245
+ | `std.log.info("..."` outside `logging.scoped(.tag)` | Bypasses scope/event/field discipline. | Convert to `logging.scoped(.tag).info("event", .{...})`. |
246
+ | `std.debug.print(` in non-test source | No level, no scope, no field structure. Dev-only debugging that escaped to main. | Convert or delete before commit. |
247
+ | `std.log.err` without `error_code=` field | Drops registry traceability. | Add the registry code, or add a registry entry if missing. |
248
+ | `std.log.scoped(...)` outside `src/lib/logging/` | Free-form printf-style emit. | Switch the file's alias to `const log = logging.scoped(.tag);` and migrate every call site to the event+fields form. |
249
+ | Positional `{s} {d}` placeholders | Not logfmt; not greppable. | Convert to struct-of-fields. |
250
+
251
+ ## §8 · Per-language binding — TypeScript / JavaScript (`agentsfleet`)
252
+
253
+ Wire format identical to §3. CLI obeys two render modes:
254
+
255
+ **Human mode** (default — TTY attached, no `--json` flag):
256
+
257
+ ```
258
+ error UZ-EXEC-012: tool returned non-zero (executor) tool=bash duration=1.24s
259
+ hint: check the tool's stderr above for details
260
+ see: https://docs.agentsfleet.net/errors/UZ-EXEC-012
261
+ ```
262
+
263
+ Colors: red `error`, dim parens. Rendering inspired by Bun's `SystemError.format()` (`bun:src/bun.js/bindings/SystemError.zig:85`) — same shape, our error registry as the source.
264
+
265
+ **JSON mode** (`--json` flag, or stdout is a pipe and `AGENTSFLEET_FORMAT=json` is set):
266
+
267
+ ```json
268
+ { "code": "UZ-EXEC-012", "message": "tool returned non-zero", "scope": "executor", "fields": { "tool": "bash", "duration_ms": 1240 }, "hint": "check the tool's stderr above for details", "docs": "https://docs.agentsfleet.net/errors/UZ-EXEC-012" }
269
+ ```
270
+
271
+ Schema mirrors Bun's `SystemError` extern struct (`bun:src/bun.js/bindings/SystemError.zig:1`) — `{ code, message, ...context, hint?, docs? }`. Our `code` is `UZ-XXX-NNN` (registry-defined) rather than errno; the *shape* is what we mirror.
272
+
273
+ > [DETERMINISTIC → LOG]
274
+
275
+ **Logging emit sites** in `cli/src/**` use a thin Bun-runtime logger that produces logfmt records to `stderr`. `console.log` / `console.error` are forbidden in source per `dispatch/write_ts_adhere_bun.md` §10 — `logging.sh` enforces this for TS/JS.
276
+
277
+ > [JUDGMENT → TS-STYLE]
278
+
279
+ **Module-level error style** is governed by `dispatch/write_ts_adhere_bun.md` §9 (one style per module — throw OR Result, never both). The error type itself is the same:
280
+
281
+ ```ts
282
+ class AgentError extends Error {
283
+ readonly code: string; // UZ-XXX-NNN
284
+ readonly fields: Record<string, unknown>;
285
+ readonly hint?: string;
286
+ }
287
+ ```
288
+
289
+ Throw-style modules `throw new AgentError({...})`. Result-style modules return `{ ok: false, error: new AgentError({...}) }`. Render path picks human or JSON based on the runtime mode.
290
+
291
+ ## §9 · Pretty-printer (dev-only render variant)
292
+
293
+ In production: every record on the wire is logfmt. No exceptions.
294
+
295
+ In development (TTY attached, optional `AGENTSFLEET_LOG_PRETTY=1` env var): the same record can render as a colored printf-style line for human readability:
296
+
297
+ ```
298
+ 14:32:18.234 WARN executor tool_call_failed tool=bash duration=1.24s — tool returned non-zero [UZ-EXEC-012]
299
+ ```
300
+
301
+ Mechanics:
302
+
303
+ > [UNENFORCED → render-path architecture; a static scan cannot see how many times a record renders, and §9 is explicitly off the audit path by its own third bullet]
304
+
305
+ - TTY check happens **once at process startup** (`isatty(stderr) && env.AGENTSFLEET_LOG_PRETTY != "0"`). Cached as a compile-once boolean.
306
+ - A single sink picks the formatter at startup; from then on every record renders through the chosen formatter exactly once. **No strip step. No re-render.** The wire path never sees colors.
307
+ - LOGGING GATE audits the **wire** format only. Pretty mode is post-hoc render and not on the audit path.
308
+
309
+ Performance: zero measurable production impact. The pretty-vs-logfmt selector is one branch the predictor handles in a single cycle; in production (no TTY) it always takes the logfmt path. See `§7` for hot-path details (compile-time visibility short-circuit, thread-local 4 KiB buffer).
310
+
311
+ ## §10 · Reference: Bun's conventions (citations)
312
+
313
+ We borrowed pattern, not code. Concrete file:line references for traceability:
314
+
315
+ | Concept | Bun reference | What we took |
316
+ |---|---|---|
317
+ | Scoped logger with comptime visibility | `~/Projects/oss/bun/src/output.zig:869-902` | `Scope` enum + `LoggerFor(scope)` returning a struct of inline methods. Compile-time short-circuit via `comptime if (visibility == .hidden) return;`. |
318
+ | Severity ladder | `~/Projects/oss/bun/src/logger.zig:1-31` | Five levels (we drop Bun's `note`). |
319
+ | `Output.err` rendering errno + label + syscall | `~/Projects/oss/bun/src/output.zig:1155-1170` | Inspiration for `agentsfleet` human render: `code: message (context)`. |
320
+ | `SystemError` JSON-facing shape | `~/Projects/oss/bun/src/bun.js/bindings/SystemError.zig:1-12` | `{ code, message, path?, syscall?, ... }` mirrored as our `--json` output schema. |
321
+ | `SystemError.format()` ANSI-colored render | `~/Projects/oss/bun/src/bun.js/bindings/SystemError.zig:85-116` | Pattern reused for `agentsfleet` human mode (red code, dim context). |
322
+
323
+ Zero lines copied verbatim. The patterns above each compile to ~10–30 lines of our own Zig/TypeScript in idiomatic form.
324
+
325
+ ## §10A · Tightening clauses (closures of common skip rationalizations)
326
+
327
+ > [UNENFORCED → section pointer; each row below names its own enforcement — logging.sh, error-codes.sh, or the façade]
328
+
329
+ Failure modes the audit script and reviewer must close. These are **not aspirational** — each closes a specific way an agent could otherwise dodge the rule.
330
+
331
+ | # | Rationalization | Closure |
332
+ |---|---|---|
333
+ | L1 | "Temporary debug print, I'll remove later" | `logging.sh` greps `std.debug.print` and `console.log` / `console.debug` / `console.info` in non-test source unconditionally. Found in commit → gate fails. No "temporary" carve-out. |
334
+ | L2 | "`std.log.scoped` is fine, `obs.scoped` is just a wrapper" | `std.log.scoped` is **forbidden** in `src/**/*.zig` outside `src/lib/logging/`. Only `obs.scoped` is callable. Audit flags every `std.log.` call site. |
335
+ | L3 | "I added `error_code=UZ-NEW-001` — registry entry coming next commit" | The registry entry **must land in the same commit** as the first reference. `error-codes.sh` runs against the staged diff; missing entry = blocking. |
336
+ | L4 | "This per-iteration event matters for debugging — `info`-level" | Per-iteration and per-row paths are `debug`, no exception: a loop body is not a boundary, and `debug` is the level with a volume switch. This closes the ONLY `info` prohibition — there is no allow-list of event names and none may be reintroduced (§4). The inverse rationalization is closed too: "this operation is obviously fine, no need to log it" does not survive §4 rule 1, which requires a `_started`/`_completed`\|`_failed` pair on every boundary-crossing operation. Reviewer checks the pair and the loop, never the spelling. |
337
+ | L5 | "Operator needs the full stack trace in `msg=`" | `msg=` capped at 300 chars; total fields per record capped at 15. Stack traces emit as a separate `event=stack_trace` record at `debug` level, correlated by `correlation_id`, not stuffed into `msg`. |
338
+ | L6 | "Embedded newlines because I copy-pasted output" | Audit greps for raw newline byte inside quoted logfmt values. Must be `\n` literal (two chars). |
339
+ | L7 | "Auto-mode is on, the gate block is ceremony" | **Auto-mode does NOT cover gate skips.** Skip without an explicit user-given override = automatic violation. No size threshold lets an edit bypass the gate. |
340
+ | L8 | "I read this doc at session start; subsequent edits don't need re-print" | Gate fires **per-edit**. The printed `🔴 LOGGING GATE` block is required before every triggered Edit/Write, not once per session. |
341
+ | L9 | "Fix-pass touches every line; printing per-line is noise" | Fix-pass produces **one combined gate block per file**, not per-line. The block lists all violations addressed in that file. Still required, just consolidated. |
342
+
343
+ These are enforced by `logging.sh` (mechanical) and the dispatch façade (`dispatch/write_any.md`, Logging Gate — output discipline). When in conflict, the façade wins — it is the enforcement layer.
344
+
345
+ ## §11 · Anti-patterns (named, banned)
346
+
347
+ | Pattern | Why banned |
348
+ |---|---|
349
+ | Free-form English log lines | Not greppable, not parseable, no event tag. |
350
+ | `std.debug.print` outside tests | No level, no scope, no fields — dev debugging that escaped to main. |
351
+ | `console.log` in `agentsfleet` source | Bypasses logger, breaks `--json` mode, violates `dispatch/write_ts_adhere_bun.md` §10. |
352
+ | Logging on hot per-iteration paths at `info` | Floods collectors. Use `debug` (gated off by default). |
353
+ | `error_code=` missing on `err`/`warn` mapping to registry codes | Breaks traceability; future operator can't link log to docs. |
354
+ | Logging credentials, tokens, or BYOK keys | Severe leak surface. Redaction harness mandatory. |
355
+ | Multi-line records (embedded `\n` in values) | Breaks `tail -f`, breaks Loki, breaks every parser. |
356
+ | `key=null` or `key=` (empty value) | Confuses parsers; some treat as missing, others as the literal string `null`. Omit the key. |
357
+ | Inventing severity levels (`notice`, `severe`, `fatal`) | Five levels only. New names diverge from collector dashboards. |
358
+
359
+ ## §12 · Override syntax
360
+
361
+ Per-record override (rare, user-only):
362
+
363
+ ```
364
+ LOGGING RULE §<N>: SKIPPED per user override (reason: ...)
365
+ ```
366
+
367
+ immediately preceding the edit. Generic "scope creep" is not a valid reason — name a concrete external constraint (third-party library log shape, vendored code, pre-launch debugging that won't ship). Auto-mode does NOT cover this override.
368
+
369
+ ## §13 · Family
370
+
371
+ - `dispatch/write_ts_adhere_bun.md` §10 — banned `console.log` in TS/JS source. Cross-referenced by `logging.sh`.
372
+ - `dispatch/write_ts_adhere_bun.md` §9 — module-level error style (throw vs Result) for `agentsfleet`.
373
+ - `dispatch/write_zig.md` — Zig discipline umbrella; this doc's §7 is the logging-specific layer.
374
+ - `LIFECYCLE_PATTERNS.md` — orthogonal: ownership/cleanup of structs, including allocator wiring for the thread-local log buffer.
375
+ - M42_002 redaction harness (`src/runner/engine/runner_progress.zig`) — secret-redaction precondition; this doc's §6 inherits.
376
+ - Universal rules (RULE UFS, RULE TGU, RULE PRI, RULE FLL, RULE ORP, RULE TST-NAM) live in `docs/greptile-learnings/RULES.md`.
377
+ - Length caps in `dispatch/write_any.md` (File & Function Length).
378
+
379
+ > [JUDGMENT → SECTION-SCAN]
380
+
381
+ - This file is the **wire-format and discipline layer** that those universal rules cannot express. Read it once at session start; on sub-task shape change, section-scan (`grep -n "^## "`) and re-read only the touched sections — never the full file again.
382
+
383
+ ## §14 · Enforcement tag glossary
384
+
385
+ Every clause above carries a class tag. `docs/RULE_ENFORCEMENT.md` counts them;
386
+ `audits/rule-ledger.sh --census` is the command that reads them. A tag alone on
387
+ its line covers the clauses under it until the next heading.
388
+
389
+ | Tag | Meaning | Where the verdict comes from |
390
+ |---|---|---|
391
+ | `[DETERMINISTIC → LOG]` | a script decides; no talking past it | `audits/logging.sh` — `std.debug.print` and `console.*` in non-test source block; missing `error_code=` on `std.log.{err,warn}` reports as INFO |
392
+ | `[JUDGMENT → BOUNDARY]` | is this operation boundary-crossing, and is its `_started`/`_completed`\|`_failed` pair complete | the agent at write time; the reviewer at `/review` |
393
+ | `[JUDGMENT → REDACT]` | is this value a secret, or the label of one | the agent at write time; the M42_002 harness covers only executor-child output |
394
+ | `[JUDGMENT → MSG-REVIEW]` | does this `msg=` carry a credential | the reviewer; no length or content check runs today |
395
+ | `[JUDGMENT → TS-STYLE]` | throw-style or Result-style for this module | `dispatch/write_ts_adhere_bun.md` §9, agent-decided |
396
+ | `[JUDGMENT → SECTION-SCAN]` | which sections of this file the current sub-task needs | the agent; `audits/doc-read.sh` records façade reads, not delegated-doc reads |
397
+ | `[UNENFORCED → reason]` | acknowledged prose; the reason states why no check exists | nothing — that is the point of the class |
@@ -0,0 +1,129 @@
1
+ # Orly architecture
2
+
3
+ Orly does two jobs. It renders one rules file, and it proves the boundary
4
+ before a Pull Request (PR) opens. What it stores where is the one thing that
5
+ has changed shape since this document was first written — see "Why it's
6
+ materialised" below; the gates section after it is unaffected.
7
+
8
+ ## Topology
9
+
10
+ ```text
11
+ core/operating-model.md packs/** registry.json
12
+ │
13
+ bin/orly
14
+ │
15
+ ┌───────────────────┴───────────────────┐
16
+ │ │
17
+ orly update orly gate
18
+ │ │
19
+ AGENTS.md (repo root) reads git + the tree,
20
+ │ runs the declared commands,
21
+ ~/.claude/CLAUDE.md ─┐ prints green or red
22
+ ~/.codex/AGENTS.md ─┼─ symlinks │
23
+ opencode, amp ─┘ exit 0 or 1
24
+ ```
25
+
26
+ One generated artifact on this machine: the root `AGENTS.md`. Every agent home
27
+ here links to it, so a rule edit is one commit and reaches every session in
28
+ this checkout at once — that property is local to Kishore's own machine and
29
+ unaffected by anything below.
30
+
31
+ Every other repository gets its rules a different way: `orly init` materialises
32
+ the packs its own sources select — rendered `AGENTS.md`, the rule docs, the gate
33
+ scripts, the hooks that run them — into that repository, and writes
34
+ `.oracle/orly.json` recording the engine version and every file it wrote,
35
+ alongside the repository's own packs, commands, and surfaces. `orly update` re-materialises against the currently installed engine
36
+ version; `orly doctor` reports drift between the lock and disk instead of
37
+ silently tolerating it. A materialised repository needs no checkout of this
38
+ one, on any machine, to read its own rules or run its own gates.
39
+
40
+ ## Why it's materialised
41
+
42
+ An earlier model (M01) rejected storing per-repository copies: it had cost a
43
+ re-baseline commit on every edit, a SHA-256 manifest that invalidated whenever
44
+ the tool itself changed, and drift nobody caught — three of four consumer
45
+ repositories were stale anyway. The fix at the time was to derive instead of
46
+ store: one rendered `AGENTS.md`, symlinked into every agent home, with gate
47
+ scripts resolved live from this checkout via `$ORLY_ROOT`.
48
+
49
+ That traded the storage cost for a distribution cost undiscovered until a
50
+ second engineer tried to install the harness without this checkout present —
51
+ the symlink and `$ORLY_ROOT` both require a copy of `~/Projects/dotfiles` at a
52
+ known path, which is exactly what a fresh machine, a Continuous Integration
53
+ (CI) runner, or a remote fleet container does not have. `orly init` (M03)
54
+ restores storage, but not the failure mode that got it removed: `orly update`
55
+ turns a rule change into one command per repository instead of the manual
56
+ sync M01 rejected, and the lock makes staleness a reported condition —
57
+ `orly doctor` — instead of a silent one. cache-kit.rs is the evidence for both
58
+ failure modes in the same repository: its `.oracle/` snapshot from the
59
+ pre-thin model sat frozen for four weeks with no update path, and its
60
+ generated `AGENTS.md` told a Rust crate its project name was `agentsfleet` —
61
+ a persona/product leak M03 also closes, by fencing both behind opt-in packs a
62
+ a repository without those sources never selects.
63
+
64
+ `orly verify --all` still re-renders each pack set twice and compares, then
65
+ compares the committed root `AGENTS.md` against a fresh render — that
66
+ determinism proof is unchanged by any of this.
67
+
68
+ ## Gates
69
+
70
+ `orly gate` runs three groups in order and stops at the first red one.
71
+
72
+ | Gate | Proves |
73
+ |---|---|
74
+ | `work` | branch is not the default; tree is clean; the repository declares its commands in `.oracle/orly.json` |
75
+ | `verify` | Dimensions marked DONE (when a spec exists); `conform`; the fast `verify.*` commands |
76
+ | `pr` | tree clean; branch pushed; spec gate + closed-spec follow-through (`spec.moved` / `spec.baseline` / `spec.ordering` / `spec.deferrals`); docs updated for user-surface changes; the slow suites |
77
+
78
+ Every criterion is mechanical — it reads an exit code or a file. Claims that
79
+ cannot be proven that way stay prose and are graded by the spec's rubric; they
80
+ never become fake criteria.
81
+
82
+ Four behaviours worth knowing:
83
+
84
+ - **No spec, no problem — but closing is not escaping.** Spec criteria skip
85
+ with a printed reason, so an ad-hoc bug fix meets the quality gates without
86
+ being told to write a spec. A spec moved to `done/` on the branch is still
87
+ discovered through its `Branch:` header and gates the PR — CHORE(close)
88
+ never skip-passes the criteria it exists to satisfy. Two active specs is an
89
+ error — one stream per worktree.
90
+ - **A worktree is its repository.** `repositories.json` registers primary
91
+ checkouts only; a linked worktree resolves through the set of checkouts git
92
+ reports for the shared object store. Streams stay ephemeral and unregistered,
93
+ and the declared commands still run in the worktree, never in the checkout
94
+ that carries the registry entry.
95
+ - **Slow suites are conditional.** `verify.integration` and `verify.memory` run
96
+ only when the branch diff carries code files.
97
+ - **The docs gate is diff-shaped.** A change under the repository's `surfaces.user`
98
+ prefixes with no matching `surfaces.docs` change is red. Test files and the
99
+ spec tree never count.
100
+
101
+ ## Overrides
102
+
103
+ `orly override <criterion> --reason <REASON>` writes an empty commit carrying an
104
+ `Orly-Override: <criterion> (<reason>)` trailer. It is immutable once pushed,
105
+ visible in the PR, scoped to the branch by merge-base, and dead after the merge.
106
+ A red criterion with a matching trailer reports `overridden`, never green. A
107
+ malformed trailer is not an override; the gate stays red.
108
+
109
+ ## Profiles
110
+
111
+ `.oracle/orly.json` names any opt-in packs, the command surface, and optionally the diff
112
+ surfaces:
113
+
114
+ ```json
115
+ {
116
+ "commands": { "conform": [["make", "harness-verify"]],
117
+ "verify.unit": [["make", "test-unit-all"]] },
118
+ "surfaces": { "user": ["src/agentsfleetd/http/", "cli/src/"],
119
+ "docs": ["docs/"] }
120
+ }
121
+ ```
122
+
123
+ Orly owns policy and invokes these commands. The repository owns what they do.
124
+
125
+ ## Evidence
126
+
127
+ `orly verify --all --write-evidence` records the source commit and every check
128
+ into `.oracle/evidence.json` (git-ignored). Pre-push writes it when the pushed
129
+ range touches governance paths.