@agentsfleet/orly 0.14.3 → 0.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -16,7 +16,7 @@ You are this repository's coding agent: deterministic, autonomous, command-line-
16
16
 
17
17
  **The human is Kishore** — casual handle **Indy**; either name, any case, addresses him. Ambiguous "the user" / "they" here = Kishore. The agent is **Oracle**, casual handle **Aiwa**. `orly` names the agentsfleet CLI only (`orly gate`, `orly init`, `orly update`, and the `Orly-Override` trailer it writes).
18
18
 
19
- **Address tags.** Kishore: **🤠 Indy**; agent: **🦉 Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains → `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps → flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `core.fleet_*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
19
+ **Address tags.** Kishore: **🤠 Indy**; agent: **🦉 Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains → `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps → flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `fleet.*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
20
20
 
21
21
  MacBook. Languages: Python, Go, Rust, TypeScript, Zig. Tooling: `mise` first, `brew` fallback. Forges: `gh`/`glab`. Commit identity from `git config`, never here.
22
22
 
package/README.md CHANGED
@@ -78,7 +78,7 @@ TypeSafe's Jev model supplies runtime advice through explicit `orly judge` reque
78
78
 
79
79
  See [releases](https://github.com/agentsfleet/orly/releases) for published versions.
80
80
  The install commands below fetch the published package.
81
- The release workflow publishes 0.14.3 after its version change merges to `main`.
81
+ The release workflow publishes 0.14.5 after its version change merges to `main`.
82
82
 
83
83
  ---
84
84
 
@@ -179,7 +179,7 @@ The questionnaire is organised by scenario. Each scenario corresponds to a momen
179
179
 
180
180
  | # | Question | Expected |
181
181
  |---|---|---|
182
- | 11.1 | Only under the explicit `product.agentsfleet` pack, is its pre-v0.30.0 removal flow rm-file + rm-embed + rm-migration-array with no markers, while independent repositories retain their own live-data policy and destructive-change approval? | YES |
182
+ | 11.1 | Only under the explicit `product.agentsfleet` pack, is its pre-v0.30.0 removal flow rm-file + rm-migration-array with no markers, while independent repositories retain their own live-data policy and destructive-change approval? | YES |
183
183
  | 11.2 | Are static strings in SQL schema a **no-override** ban across every literal that names a value or identifier the application also names — `DEFAULT`, `CHECK (col IN (…))`, trigger-body comparisons against a column, `current_setting()` parameter names, and `RAISE EXCEPTION` text a caller matches on — not just `DEFAULT`/`CHECK`? | YES |
184
184
 
185
185
  ### Scenario 12 — Auto-mode boundary (added)
package/audits/msid-ui.sh CHANGED
@@ -99,7 +99,23 @@ case "$MODE" in
99
99
  EXPLICIT=("$@"); POST_REF=""; LABEL="explicit files" ;;
100
100
  esac
101
101
 
102
- # Single awk pass; reads stdin (the unified diff).
102
+ # --staged/--diff: one git diff piped into one awk — the scan itself costs
103
+ # two processes however large the staged set, plus at most one `git show`
104
+ # per file holding a candidate hit line (zero for a clean diff). Explicit
105
+ # mode keeps a per-file scan so the reported path is the caller's own argv
106
+ # string (git's --no-index headers strip the leading slash of absolute
107
+ # paths, which would misreport the hit and misread the post-image).
108
+ # This gate runs inside a 30-minute pre-commit budget that the previous
109
+ # per-file fork loop blew through at 1,628 staged files.
110
+ #
111
+ # The awk learns the current file from the diff's own `+++ b/<path>` header
112
+ # instead of a shell loop variable. That parse is made safe on purpose:
113
+ # * `--src-prefix=a/ --dst-prefix=b/` pins the prefixes, because user config
114
+ # (diff.noprefix, diff.mnemonicPrefix) may rename them.
115
+ # * git C-quotes paths containing control characters, `"` or `\` (and with
116
+ # the default core.quotePath, non-ASCII bytes — hence `-c
117
+ # core.quotePath=false` on the git calls), so the header is unquoted in
118
+ # awk. git terminates the header path with one TAB, quoted or not.
103
119
  #
104
120
  # Carve-out recognition reads the preceding line from the SOURCE, not from the
105
121
  # diff. Every carve-out here — the two override markers and the design-system
@@ -112,29 +128,21 @@ esac
112
128
  # override comment stopped covering the line it sits above the moment that line
113
129
  # was next touched.
114
130
  #
115
- # The hunk header carries the post-image line number, so `line_above` can fetch
116
- # the neighbouring line out of the post-image the mode named. Same shape as
117
- # audits/design-tokens.sh, which has always read its override out of the file.
131
+ # The hunk header carries the post-image line number, so `line_above` answers
132
+ # from the post-image the mode named. The post-image is slurped once per file —
133
+ # on the file's first *candidate* hit line, since only candidates (a line
134
+ # matching the MS-ID or UI regexes in a scoped file) ever read the neighbour —
135
+ # and dropped when the next file's `diff --git` section opens. One `git show`
136
+ # per candidate file, zero for a clean diff, and memory bounded by one file.
118
137
  scan_diff() {
119
- ORLY_AUDIT_PATH="$1" awk -v post_ref="$POST_REF" '
138
+ awk -v post_ref="$POST_REF" '
120
139
  # BWK awk (macOS) has no gensub and no --posix quoting help; APOSTROPHE is
121
140
  # built rather than written, because this program is inside single quotes.
122
- BEGIN { APOSTROPHE = sprintf("%c", 39); f = ENVIRON["ORLY_AUDIT_PATH"] }
123
-
124
- # The source line above line `n` of `path`, from the post-image, or "" when
125
- # there is none. Cached: a hunk may ask about the same neighbour repeatedly,
126
- # and each miss costs a fork.
127
- function line_above(path, n, key, cmd, out) {
128
- if (n <= 1) return ""
129
- key = path SUBSEP n
130
- if (key in above) return above[key]
131
- above[key] = ""
132
- if (post_ref == "") cmd = "sed -n " (n - 1) "p " shell_quote(path)
133
- else cmd = "git show " shell_quote(post_ref path) " 2>/dev/null | sed -n " (n - 1) "p"
134
- if ((cmd | getline out) > 0) above[key] = out
135
- close(cmd)
136
- return above[key]
137
- }
141
+ # Explicit mode names its file through the environment (see the driver) and
142
+ # the header parse below then never overrides it; the diff modes pass an
143
+ # empty value and every `+++` header re-attributes f.
144
+ BEGIN { APOSTROPHE = sprintf("%c", 39); in_hunk = 0; loaded = 0; imgn = 0
145
+ f = ENVIRON["ORLY_AUDIT_PATH"]; fixed_f = (f != "") }
138
146
 
139
147
  function shell_quote(s, i,c,out) {
140
148
  out = APOSTROPHE
@@ -145,23 +153,113 @@ ORLY_AUDIT_PATH="$1" awk -v post_ref="$POST_REF" '
145
153
  }
146
154
  return out APOSTROPHE
147
155
  }
156
+
157
+ # The post-image path a `+++ ` header names: C-quote resolved (git wraps
158
+ # the whole "b/<path>" in quotes), the pinned b/ prefix removed. /dev/null
159
+ # maps to the empty path, which matches no scope below.
160
+ function dst_path(header, rest,out,i,c,nxt,q,oct,v,j) {
161
+ rest = substr(header, 5)
162
+ if (rest == "/dev/null") return ""
163
+ if (substr(rest, 1, 1) == "\"") {
164
+ q = 0
165
+ for (i = length(rest); i >= 2; i--) if (substr(rest, i, 1) == "\"") { q = i; break }
166
+ rest = substr(rest, 2, q - 2)
167
+ out = ""
168
+ i = 1
169
+ while (i <= length(rest)) {
170
+ c = substr(rest, i, 1)
171
+ if (c == "\\") {
172
+ nxt = substr(rest, i + 1, 1)
173
+ if (nxt ~ /[0-7]/) {
174
+ # \ooo octal escape, one to three digits (only control bytes reach
175
+ # here: core.quotePath=false passes non-ASCII through raw).
176
+ oct = nxt
177
+ while (length(oct) < 3 && substr(rest, i + 1 + length(oct), 1) ~ /[0-7]/)
178
+ oct = oct substr(rest, i + 1 + length(oct), 1)
179
+ v = 0
180
+ for (j = 1; j <= length(oct); j++) v = v * 8 + substr(oct, j, 1)
181
+ out = out sprintf("%c", v)
182
+ i += 1 + length(oct)
183
+ } else {
184
+ if (nxt == "a") out = out sprintf("%c", 7)
185
+ else if (nxt == "b") out = out sprintf("%c", 8)
186
+ else if (nxt == "f") out = out "\f"
187
+ else if (nxt == "n") out = out "\n"
188
+ else if (nxt == "r") out = out "\r"
189
+ else if (nxt == "t") out = out "\t"
190
+ else if (nxt == "v") out = out "\v"
191
+ else out = out nxt
192
+ i += 2
193
+ }
194
+ } else { out = out c; i++ }
195
+ }
196
+ rest = out
197
+ } else {
198
+ # Unquoted: drop the single trailing TAB git appends to the header
199
+ # path. A literal tab in a filename forces C-quoting, so a trailing
200
+ # tab here is never name content; a trailing SPACE is, and is kept.
201
+ sub(/\t$/, "", rest)
202
+ }
203
+ sub(/^b\//, "", rest)
204
+ return rest
205
+ }
206
+
207
+ function load_postimage( cmd,l) {
208
+ imgn = 0
209
+ if (post_ref == "") {
210
+ while ((getline l < f) > 0) { imgn++; img[imgn] = l }
211
+ close(f)
212
+ } else {
213
+ cmd = "git show " shell_quote(post_ref f) " 2>/dev/null"
214
+ while ((cmd | getline l) > 0) { imgn++; img[imgn] = l }
215
+ close(cmd)
216
+ }
217
+ loaded = 1
218
+ }
219
+
220
+ # The source line above post-image line `n` of the current file, or "" when
221
+ # there is none. Reads the slurped post-image; the slurp happens at most
222
+ # once per file section.
223
+ function line_above(n) {
224
+ if (n <= 1) return ""
225
+ if (!loaded) load_postimage()
226
+ if (n - 1 > imgn) return ""
227
+ return img[n - 1]
228
+ }
229
+
230
+ # A new file section: reset hunk state and drop the previous post-image —
231
+ # lookups only ever concern the file of the open section.
232
+ /^diff --git / {
233
+ in_hunk = 0
234
+ if (loaded) { for (i = 1; i <= imgn; i++) delete img[i]; imgn = 0; loaded = 0 }
235
+ next
236
+ }
237
+ !in_hunk && /^\+\+\+ / { if (!fixed_f) f = dst_path($0); next }
148
238
  /^\+\+\+/ { next }
149
239
  # `@@ -a,b +c,d @@` — $3 is the post-image start line for this hunk.
150
- /^@@/ { start=$3; sub(/^\+/,"",start); split(start,parts,","); nl=parts[1]+0; next }
240
+ /^@@/ { in_hunk = 1; start=$3; sub(/^\+/,"",start); split(start,parts,","); nl=parts[1]+0; next }
151
241
  /^[^+]/ { next }
152
242
  /^\+/ {
153
243
  line=$0
154
244
  sub(/^\+/,"",line)
155
- prev = line_above(f, nl)
245
+ # The line above is fetched only for candidate hits: every reader of
246
+ # `prev` sits behind one of these two guards, so a line that matches
247
+ # neither regex never pays for the post-image.
248
+ msid_cand = f ~ /\.(zig|sql|ts|tsx|js|jsx|py|rs|go|sh|toml|yaml|json)$/ &&
249
+ f !~ /^(docs|node_modules|vendor|third_party)\// &&
250
+ match(line, /M[0-9]+_[0-9]+|§[0-9]+(\.[0-9]+)+|(^|[^A-Za-z0-9_])T[0-9]+([^A-Za-z0-9_]|$)|(^|[^A-Za-z0-9_])dim [0-9]+\.[0-9]+([^A-Za-z0-9_]|$)/)
251
+ ui_cand = f ~ /^ui\/packages\/app\/.*\.(tsx|jsx)$/ &&
252
+ line ~ /<(section|button|input|dialog|article|nav|header|form)[ \t>\/]/
253
+ if (msid_cand || ui_cand) {
254
+ prev = line_above(nl)
255
+ ms_id_override = (prev ~ /MILESTONE ID ALLOWED per user override/)
256
+ ui_override = (prev ~ /UI GATE: SKIPPED per user override/)
257
+ } else {
258
+ prev = ""; ms_id_override = 0; ui_override = 0
259
+ }
156
260
  nl++
157
- ms_id_override = (prev ~ /MILESTONE ID ALLOWED per user override/)
158
- ui_override = (prev ~ /UI GATE: SKIPPED per user override/)
159
- if (!ms_id_override &&
160
- f ~ /\.(zig|sql|ts|tsx|js|jsx|py|rs|go|sh|toml|yaml|json)$/ &&
161
- f !~ /^(docs|node_modules|vendor|third_party)\//) {
162
- if (match(line, /M[0-9]+_[0-9]+|§[0-9]+(\.[0-9]+)+|(^|[^A-Za-z0-9_])T[0-9]+([^A-Za-z0-9_]|$)|(^|[^A-Za-z0-9_])dim [0-9]+\.[0-9]+([^A-Za-z0-9_]|$)/)) {
163
- print "MS-ID " f ": " line
164
- }
261
+ if (msid_cand && !ms_id_override) {
262
+ print "MS-ID " f ": " line
165
263
  }
166
264
  # react-hook-form caveat: its prescribed shape is a raw
167
265
  # <form onSubmit={form.handleSubmit(...)}> inside the design-system <Form>
@@ -179,9 +277,7 @@ ORLY_AUDIT_PATH="$1" awk -v post_ref="$POST_REF" '
179
277
  # is above it in the file whether or not this diff touched it. A
180
278
  # bare/unwrapped <section> still trips.
181
279
  ds_section = (line ~ /<section[ \t]/ && prev ~ /<Section asChild>/)
182
- if (!ui_override && !rhf_form && !ds_section &&
183
- f ~ /^ui\/packages\/app\/.*\.(tsx|jsx)$/ &&
184
- line ~ /<(section|button|input|dialog|article|nav|header|form)[ \t>\/]/) {
280
+ if (ui_cand && !ui_override && !rhf_form && !ds_section) {
185
281
  print "UI " f ": " line
186
282
  }
187
283
  }
@@ -193,12 +289,14 @@ hits=$(
193
289
  for path in "${EXPLICIT[@]}"; do
194
290
  [ -f "$path" ] || continue
195
291
  # diff returns one for differences; that is expected input here.
196
- { git diff --no-index --no-ext-diff -U0 -- /dev/null "$path" || [ "$?" -eq 1 ]; } | scan_diff "$path"
292
+ { git -c core.quotePath=false diff --no-index --no-ext-diff -U0 --src-prefix=a/ --dst-prefix=b/ -- /dev/null "$path" || [ "$?" -eq 1 ]; } | ORLY_AUDIT_PATH="$path" scan_diff
197
293
  done
198
294
  else
199
- while IFS= read -r -d '' path; do
200
- git diff --no-ext-diff -U0 "${DIFF_ARGS[@]}" -- "$path" | scan_diff "$path"
201
- done < <(git diff --name-only -z --diff-filter=ACMRT "${DIFF_ARGS[@]}")
295
+ # --no-renames pins a rename to its delete+add shape: this gate scores
296
+ # added lines, and its verdicts are defined over renamed files being
297
+ # re-scanned as whole-file additions. Rename detection would silently
298
+ # stop scanning their contents.
299
+ git -c core.quotePath=false diff --no-ext-diff -U0 --no-renames --diff-filter=ACMRT --src-prefix=a/ --dst-prefix=b/ "${DIFF_ARGS[@]}" | ORLY_AUDIT_PATH="" scan_diff
202
300
  fi
203
301
  )
204
302
 
@@ -7,7 +7,7 @@ type Section = { title: string; nodes: MarkdownNode[] };
7
7
  type Commands = Record<string, string[][]>;
8
8
 
9
9
  const CONFIG_PATH = ".orly/orly.json";
10
- const SPEC_LINE_LIMIT = 320;
10
+ const SPEC_LINE_LIMIT = 500;
11
11
  const BACKTICK = "`";
12
12
  const EMPTY = "";
13
13
  const PIPE_OUTPUT = "pipe";
@@ -11,7 +11,7 @@ You are this repository's coding agent: deterministic, autonomous, command-line-
11
11
 
12
12
  **The human is Kishore** — casual handle **Indy**; either name, any case, addresses him. Ambiguous "the user" / "they" here = Kishore. The agent is **Oracle**, casual handle **Aiwa**. `orly` names the agentsfleet CLI only (`orly gate`, `orly init`, `orly update`, and the `Orly-Override` trailer it writes).
13
13
 
14
- **Address tags.** Kishore: **🤠 Indy**; agent: **🦉 Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains → `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps → flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `core.fleet_*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
14
+ **Address tags.** Kishore: **🤠 Indy**; agent: **🦉 Aiwa** (`Oracle`). **Project name:** `agentsfleet` (domains → `agentsfleet.net`). Write product as `agentsfleet` (inline code), never bare; stale legacy-brand reps → flag, replace when in scope, inform Indy. Products: `agentsfleet` / `agentsfleetd` / `agentsfleet-runner`; entities/API: `fleet`, `fleet_id`, `/fleets`, `fleet.*`. Keep: `agentsfleet.dev`, `github.com/agentsfleet/agentsfleet`, `@agentsfleet/*`, `~/Projects/agentsfleet`.
15
15
 
16
16
  MacBook. Languages: Python, Go, Rust, TypeScript, Zig. Tooling: `mise` first, `brew` fallback. Forges: `gh`/`glab`. Commit identity from `git config`, never here.
17
17
 
@@ -57,7 +57,6 @@ The **Schema Table Removal Guard** card dissolves into this façade; its prose i
57
57
  **Triggers** — before any of these, run `cat VERSION` and print the guard output. `0.30.0` is the anchor: below it the datastore is rebuilt from empty, from it the datastore is live production. Compare the version field by field, never as a string — `0.29.0` is under the anchor and `0.30.0` is not, while a lexical compare puts `0.30.0` under `0.9.0` and reads the era backwards.
58
58
 
59
59
  - Creating, editing, or deleting any file under `schema/*.sql`.
60
- - Editing `schema/embed.zig` (any `@embedFile` constant).
61
60
  - Editing the canonical migration array in `rustd/crates/afd_db/src/migration.rs`.
62
61
  - Writing `DROP TABLE`, `ALTER TABLE`, or `SELECT 1;` into any SQL file.
63
62
  - Accepting a spec dimension prescribing a "DROP migration", "ALTER migration", or "version marker".
@@ -73,8 +72,7 @@ The era runs while `VERSION` is under `0.30.0`, and it ends there — the develo
73
72
  To remove a table:
74
73
 
75
74
  1. `rm schema/NNN_foo.sql`
76
- 2. Remove `@embedFile` from `schema/embed.zig`
77
- 3. Remove the entry from the migration array in `rustd/crates/afd_db/src/migration.rs` and update its registration tests.
75
+ 2. Remove the entry from the migration array in `rustd/crates/afd_db/src/migration.rs` and update its registration tests.
78
76
 
79
77
  **Forbidden:** `ALTER TABLE`, `DROP TABLE`, `SELECT 1;` markers, comment-only files, "keep file for slot numbering". Slot gaps are fine — the DB is wiped on rebuild.
80
78
 
@@ -91,11 +89,10 @@ The teardown path is closed. Every schema change assumes a live datastore holdin
91
89
  ```
92
90
  SCHEMA GUARD: VERSION=<v> (<0.30.0 ? teardown : migrate)
93
91
  rm:schema/<file>.sql
94
- rm-embed:<const>
95
92
  rm-migration:v<N>
96
93
  ```
97
94
 
98
- Below `0.30.0`, all three rm- lines appear. From `0.30.0`, replace them with `migration:schema/<NNN>_<change>.sql`.
95
+ Below `0.30.0`, both rm- lines appear. From `0.30.0`, replace them with `migration:schema/<NNN>_<change>.sql`.
99
96
  <!-- oracle-packs:end -->
100
97
 
101
98
  ## Companion SQL rules (retained in `docs/greptile-learnings/RULES.md`)
@@ -18,7 +18,7 @@
18
18
  # (retained) and the GREPTILE GATE enforces them by review until a leaf exists.
19
19
  # SCH's teardown-era forbidden-token floor (no ALTER/DROP/SELECT 1; in schema/*.sql) is
20
20
  # itself a build-the-check (TODO-CHECK) candidate; the teardown-vs-ALTER disposition
21
- # call above it stays JUDGMENT (which file to rm, which @embedFile, which migration index).
21
+ # call above it stays JUDGMENT (which file to rm, which migration index).
22
22
 
23
23
  source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
24
24
 
@@ -30,7 +30,7 @@ dispatch_header
30
30
  dispatch_length_gate 350 # FLL — file ≤ 350
31
31
 
32
32
  # ── judgment gates (no script can decide; agent states verdict in chat) ──
33
- dispatch_judgment "SCH" "removing/altering a table? cat VERSION and compare field by field, not as a string — below 0.30.0 = full teardown (rm schema/NNN.sql + @embedFile + migration-array entry + index tests), never ALTER/DROP/SELECT 1; 0.30.0 and above = live production datastore, numbered ALTER/DROP migration only. print the SCHEMA GUARD block"
33
+ dispatch_judgment "SCH" "removing/altering a table? cat VERSION and compare field by field, not as a string — below 0.30.0 = full teardown (rm schema/NNN.sql + migration-array entry + index tests), never ALTER/DROP/SELECT 1; 0.30.0 and above = live production datastore, numbered ALTER/DROP migration only. print the SCHEMA GUARD block"
34
34
  dispatch_judgment "ITF" "integration test touching a production table? seed via src/db/test_fixtures_<scope>.zig against the real schema — never CREATE TEMP TABLE mocking the production shape"
35
35
 
36
36
  dispatch_verdict
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.5 — 2026-10-17
4
+
5
+ agentsfleet's schema conventions carry the owner-signed Postgres rules, and specs may run to 500 lines.
6
+
7
+ - **Schema conventions** — placement in owned schemas, key and counter naming, RULE STS in full, timestamp categories, runtime SQL in `PREPARE`-checked `.sql` files, and SQLx's `Migrator` inside a bounded lock. Entities are named `fleet.*`.
8
+ - **Spec cap** — the spec template gate allows 500 lines, up from 320.
9
+ - **Schema guard** — the teardown flow no longer names the retired `schema/embed.zig`.
10
+
11
+ ## 0.14.4 — 2026-10-16
12
+
13
+ The milestone-ID and UI-substitution audit no longer forks per staged file. A pre-commit stage of ~1,600 files that ran over 27 minutes now finishes in about two seconds, with verdicts unchanged.
14
+
15
+ - **Scan** — one `git diff` and one `awk` cover the whole staged set. Reading the line above an added line now costs at most one `git show` per file with a candidate hit, down from one fork pair per added line.
16
+ - **Compatibility** — verdicts, hit order and exit codes match 0.14.3 byte-for-byte across staged, `--diff` and explicit-file modes, including spaced, quoted and non-ASCII filenames, renames re-scanned as whole-file additions, and absolute explicit paths.
17
+
3
18
  ## 0.14.3 — 2026-10-15
4
19
 
5
20
  The REST API guide carries agentsfleet's conversation and reviewed-setup exceptions, and the TypeScript rules name how timestamps are labelled.
@@ -8,13 +8,13 @@ regenerates this file and fails on any difference. To update it, run
8
8
  |---|---|--:|--:|--:|---|
9
9
  | `docs/LOGGING_STANDARD.md` | `LOG` → `logging.sh` | 10 | 13 | 30/30 | mechanical |
10
10
  | `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/69 | mechanical |
11
- | `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/17 | mechanical |
11
+ | `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/12 | mechanical |
12
12
  | `docs/DOCUMENTATION_RULES.md` | — | 0 | 0 | 0/22 | latent |
13
13
  | `docs/LIFECYCLE_PATTERNS.md` | — | 0 | 0 | 0/22 | mechanical |
14
14
  | `docs/CHANGELOG_VOICE.md` | — | 0 | 0 | 0/3 | latent |
15
15
  | `docs/VERIFY_TIERS.md` | — | 0 | 0 | 0/4 | uncited |
16
16
  | `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/115 | mechanical |
17
- | **corpus** | | | | **30/282** | |
17
+ | **corpus** | | | | **30/277** | |
18
18
 
19
19
  ## How to read this
20
20
 
@@ -13,102 +13,115 @@ with the real schema through shared fixture helpers.
13
13
  Keep migration registration and position assertions synchronized with each edit.
14
14
 
15
15
  <!-- oracle-packs:start product.agentsfleet -->
16
- Canonical reference for `agentsfleet` database schema patterns. All new tables **must** follow these conventions. Existing tables are brought into compliance when rebuilt.
16
+ Canonical reference for `agentsfleet` database schema patterns. All new tables **must** follow these conventions. The table-by-table layout lives in the product repository's `docs/architecture/postgres_schemas.md`; this section carries the rules that layout follows.
17
17
 
18
18
  ## Migration Model
19
19
 
20
- **Teardown-rebuild is in force for the M154 rebuild (owner decision, Indy, Jul 31, 2026).** The development database is being dropped and re-created from empty while production is undeployed, so slots `001`–`046` are **retired wholesale** and the schema is re-authored by dependency layer. During this rebuild the frozen-slot rule below does not apply, and `ALTER`/`DROP` statements are forbidden rather than required — a change belongs in the base statement it would have patched. This is the posture RULE SCH already specifies for `VERSION < 0.30.0`; the Jul 22 additive model was the deviation.
20
+ **Additive at `VERSION >= 0.30.0`.** Every schema change is a new numbered slot; shipped slots are frozen history. Destructive changes (`DROP TABLE`, `DROP COLUMN`, type rewrites) need an explicit owner decision per change. A from-empty rebuild past `0.30.0` happens only under an owner-recorded `SCHEMA GUARD: SKIPPED per user override (reason: …)` with the database dropped first — the M229 rebuild (Oct 17, 2026) is the precedent.
21
21
 
22
- **Additive migrations resume at `VERSION >= 0.30.0`, as RULE SCH specifies.**
23
- Completing the rebuild alone does not change the version boundary.
24
- Every subsequent schema change is a new numbered migration file; shipped slots
25
- are frozen history. Migrations are tracked incrementally through
26
- `agentsfleetd doctor --schema-gate`; use `IF NOT EXISTS` where appropriate for
27
- fresh bootstrap and an already provisioned database.
22
+ ## Migrator
28
23
 
29
- Destructive changes (`DROP TABLE`, `DROP COLUMN`, type rewrites) require an explicit owner decision per change — additive is the default an agent may author alone.
30
-
31
- > Historical note: slots `001`–`031` predate the additive model (teardown-rebuild with inline DDL edits, enforced by a since-removed `check-schema-gate` lint target). M154 retires them along with everything up to `046`.
24
+ SQLx's `Migrator` applies the slots from the explicit list in `rustd/crates/afd_db/src/migration.rs` (`Migrator::with_migrations`; the `migrate` feature only, `macros` stays off). It keeps checksums, so an edited applied slot is refused, and records the ledger in the `audit` schema. Locking is ours: SQLx's lock is off (`set_locking(false)`) and a bounded `pg_try_advisory_lock` poll wraps the run, so a held lock fails loudly instead of hanging a deploy. Migrate refuses to start while a ledger from an older migrator exists.
32
25
 
33
26
  ## Schema File Organization
34
27
 
35
- - Each SQL file must be **≤100 lines** and **single-concern** (one table, one logical group, or one additive change).
36
- - Files are numbered sequentially: `001_core_foundation.sql`, `002_core_workflow.sql`, etc. New migrations append the next number; shipped numbers are never reused or slid.
37
- - Every SQL file must be registered in `schema/embed.zig` (compile-time embed) and `rustd/crates/afd_db/src/migration.rs` (migration version array).
38
- - Below `0.30.0`, remove obsolete slots and their registrations completely;
39
- no-op markers are forbidden. At and above `0.30.0`, preserve shipped slots and
40
- apply changes through new migrations.
28
+ - One slot per table: the table, its local constraints, indexes and grants together; each file **≤100 lines**.
29
+ - Slots are ordered by foreign key — a table follows every table it references, across schemas. A foreign key that closes a cycle (a head pointer, for example) gets its own registered slot after both tables exist, `DEFERRABLE INITIALLY DEFERRED`.
30
+ - Every slot is registered in `rustd/crates/afd_db/src/migration.rs`; the `rust-migration-list` lint compares the list to the tracked files.
31
+ - Shipped numbers are never reused or slid.
32
+
33
+ ## Placement and naming
34
+
35
+ 1. **Placement.** A table lives in the schema of the product area that owns it (`identity`, `authoring`, `fleet`, `runner`, `connector`, `model`, `billing`, `vault`, `memory`). Its name is a plural noun that never repeats the schema name: `authoring.drafts`, not `authoring.authoring_drafts`. A singleton is named for its one row and says so in its slot.
36
+ 2. **Foreign keys** end `_id` and name the table they point at (`user_id`, `draft_id`). A prefix names the role when the column alone would not say it (`invited_by_user_id`, `active_grant_id`, `last_runner_id`). A deliberately missing foreign key is explained in the slot comment.
37
+ 3. **Status** columns are named `status`; categories are `<thing>_type`. RULE STS applies in full and without override: no literal that names a value or identifier the application also names — a `DEFAULT`, a `CHECK (col IN (…))` list, a trigger comparison against a column, a `current_setting()` parameter name, or `RAISE EXCEPTION` text a caller matches on. A predicate that needs a state reads a NULL marker column; a privileged path (a purge, for example) is a grant, not a session setting. The only literals left are structural ones nothing in the application names: the UUID version nibble `'7'`, `TG_OP` values, and exception text no caller matches on.
38
+ 4. **Timestamps.** Every table has `created_at`; every table whose rows change has `updated_at`. Any other time column is one of: (a) a deadline named `*expires_at`; (b) a NULL marker an index or trigger reads; (c) a domain instant the API returns that `updated_at` cannot carry; (d) an accounting cursor billing reads.
39
+ 5. **One fact, one home.** No JSON blob copies a column; no counter restates another table's number. Named exceptions, each with its consistency rule in the slot: an aggregate maintained under the same lock or trigger as its source; a delivery snapshot that must outlive its source; a non-secret projection of encrypted content; a scope copy a restricted role needs to filter alone.
40
+ 6. **Retry keys** (`operation_id` plus a request digest) live only in receipt tables.
41
+ 7. **Every column names its reader** before it is created. Accounting and audit records are exempt when the slot names the export or verification that reads them.
42
+ 8. **A new table** carries a written answer to "why not a column, or an existing table".
43
+ 9. **Immutable tables** carry no `updated_at`, no UPDATE grant, and a trigger that refuses UPDATE. DELETE stays governed by grants and the deletion matrix, so cascades keep working.
44
+
45
+ ### Counter and pointer names
46
+
47
+ | Kind | Name |
48
+ |---|---|
49
+ | Position within a parent (1, 2, 3…) | `sequence` |
50
+ | Pointer to another table's position | `<thing>_sequence` |
51
+ | Highest position so far | `last_<thing>_sequence` |
52
+ | "Everything up to N" watermark | `<verb>_through_sequence` |
53
+ | Global insertion order | `sequence` (identity column) |
54
+ | Frozen content version | `revision`; pointer `<x>_revision_id`; the live one `head_revision_id` |
55
+ | Compare-and-swap edit counter | `resource_version`, or `<scope>_version` when it covers one action |
56
+ | Lease epoch | `fencing_token` |
57
+ | Tally | `<thing>_count` |
58
+ | Deadline | `*expires_at` |
59
+
60
+ Wire field names (API and runner protocol) are not renamed by these rules.
61
+
62
+ ## Deletion matrix
63
+
64
+ Every root a user or operator can delete — draft, user, Library card, fleet, workspace, tenant — has a stated row in the product's deletion matrix: what goes with it, what stays, and what is scrubbed at once. Money records (charges, settlements, the attempts they hang off) never cascade from an erasable owner; they keep opaque identifiers. Private text is scrubbed on every erasure path, not only the one that purges drafts.
41
65
 
42
66
  ## SQL Qualification
43
67
 
44
- - Use schema-qualified table names in SQL (`core.platform_llm_keys`, `core.workspaces`, etc.) for new queries and handlers.
68
+ - Use schema-qualified table names in SQL (`identity.workspaces`, `fleet.fleets`).
45
69
  - Do not rely on session `search_path` defaults for correctness.
46
- - Legacy unqualified queries may remain temporarily, but touched paths should be migrated to schema-qualified names.
70
+
71
+ ## Runtime SQL
72
+
73
+ - Every static statement lives in a `.sql` file beside the use case that runs it (for example `library/sql/list.sql`), loaded with `include_str!` and bound by a typed Rust function that states its parameters, authorization and locks.
74
+ - Rows decode into named `FromRow` types selected by explicit column lists; `Json<T>` is for real JSON columns only; storage rows stay separate from wire types. No `to_jsonb(<row>)` whole-row reads.
75
+ - One integration test `PREPARE`s every `.sql` file against the rebuilt schema, so a stale table or column fails before merge. A statement that must be assembled at run time is named in its module and covered by its own test.
76
+ - Domain stores take the caller's connection or transaction; there is no generic repository trait.
77
+ - Every pooled runtime connection carries `lock_timeout` and `statement_timeout` named constants; the migrate connection carries neither.
47
78
 
48
79
  ## Schema-Backed Runtime Defaults
49
80
 
50
- - If a numeric schema default is also used in runtime fallback or provisioning logic, define it once in Zig as a named constant and import that constant in runtime code.
51
- - Keep the database DDL value unchanged unless the product default is intentionally changing; the Zig constant mirrors the schema default for drift detection.
52
- - Add an adjacent `Canonical constant:` SQL comment next to each shared numeric default so reviewers can verify the linkage quickly.
81
+ - A numeric schema default also used in runtime fallback or provisioning logic is defined once as a Rust named constant in the owning crate and referenced by the slot comment (`Canonical constant: crate::path::NAME`).
82
+ - Keep the DDL value unchanged unless the product default is intentionally changing.
53
83
 
54
84
  ## Identity Column
55
85
 
56
- **One identity column per table, named `id` (owner decision, Indy, Jul 31, 2026 — M154).** The earlier `uid` rule paired a generated `uid` primary key with a duplicate twin column (`id`, `tenant_id`, `workspace_id`, …) holding the same value under its own `UNIQUE` constraint. That shape is retired.
86
+ **One identity column per table, named `id` (owner decision, Indy, Jul 31, 2026 — M154).**
57
87
 
58
- - **Column:** `id`
59
- - **Type:** Universally Unique Identifier (UUID) `PRIMARY KEY`
60
- - **Generation:** Application-side UUID version 7 (UUIDv7) via `rustd/crates/afd_core/src/id.rs`, never `gen_random_uuid()`.
61
- - **No second unique key on the same value.** This is a correctness rule, not only a storage one: `ON CONFLICT` can arbitrate exactly one constraint, so a table carrying two unique keys over the same value makes two sessions inserting a brand-new row race to a duplicate-key error on the *other* index instead of taking the update arm. `schema/043_runner_lifetime_counters.sql` recorded this before the rule was generalised.
88
+ - **Type:** Universally Unique Identifier (UUID) `PRIMARY KEY`, application-minted UUID version 7 (UUIDv7) through `afd_core::id`, never `gen_random_uuid()`.
89
+ - **No second unique key on the same value.** `ON CONFLICT` arbitrates exactly one constraint, so two unique keys over one value make concurrent inserts race to a duplicate-key error on the other index.
62
90
  - **Foreign keys reference the primary key**, never a secondary unique constraint.
63
- - **Constraint:** every table carries a UUIDv7 CHECK:
64
- ```sql
65
- CONSTRAINT ck_{table}_id_uuidv7 CHECK (substring(id::text from 15 for 1) = '7')
66
- ```
67
- - **Adding a new table:** add a `generate{TableName}Id()` function to `rustd/crates/afd_core/src/id.rs`.
68
- - **API shape:** unchanged — public fields keep exposing `id`, `tenant_id`, `workspace_id` and the other documented names. The column now *is* the public name in most tables, so the aliasing the old rule required mostly disappears; where a public field differs from `id`, alias at the boundary rather than renaming client-facing payloads.
69
- - **Exceptions,** each stated in the slot that creates the table: a curated catalogue keyed by a stable slug (`core.fleet_library`) and a singleton keyed by a pinned integer (`core.model_catalogue_revision`) carry no UUID.
91
+ - **Constraint:** every UUID-keyed table carries `CONSTRAINT ck_{table}_id_uuidv7 CHECK (substring(id::text from 15 for 1) = '7')`.
92
+ - **Exceptions,** each stated in its slot: a row that exists at most once per parent is keyed by the parent (`fleet.sessions`, `fleet.activity_counters`, `runner.fleet_slots`, `runner.lifetime_counters`, `billing.wallets`, `model.tenant_selections`); a singleton keyed by a pinned integer (`model.catalogue_version`); a natural key addressed by every statement (`model.platform_defaults` by `provider`).
70
93
 
71
94
  ## Timestamps
72
95
 
73
- - **Type:** `BIGINT NOT NULL` — milliseconds since Unix epoch.
74
- - **Generation:** `std.time.milliTimestamp()` in Zig application code.
75
- - **Never** use `TIMESTAMPTZ`, `TIMESTAMP`, or `DEFAULT now()`.
96
+ - **Type:** `BIGINT NOT NULL` — milliseconds since the Unix epoch, from the application clock (`afd_core::clock::UnixMillis`).
97
+ - **Never** `TIMESTAMPTZ`, `TIMESTAMP` or `DEFAULT now()`.
76
98
 
77
99
  ## Standard Columns
78
100
 
79
- Every table must have:
80
-
81
101
  | Column | Type | Required | Notes |
82
102
  |--------|------|----------|-------|
83
- | `id` | `UUID PRIMARY KEY` | Yes | UUIDv7, app-generated; the table's only identity column |
103
+ | `id` | `UUID PRIMARY KEY` | Yes | UUIDv7, application-minted |
84
104
  | `created_at` | `BIGINT NOT NULL` | Yes | Set once at INSERT |
85
- | `updated_at` | `BIGINT NOT NULL` | If mutable | Set at INSERT and every UPDATE |
86
-
87
- **Naming is uniform.** `created_at` and `updated_at` are the only names for row lifecycle time — not `recorded_at`, `created_at_ms`, or `updated_at_ms`. A column carrying *domain* time distinct from row birth (for example the originating event's creation instant on a money row) is named for that domain meaning and documented in its slot.
88
-
89
- **Mutable tables** (any table where UPDATE is a valid operation) must have `updated_at`.
90
-
91
- **Append-only/event tables** (where UPDATE is blocked by trigger or by design) are exempt from `updated_at`.
105
+ | `updated_at` | `BIGINT NOT NULL` | If the row ever changes | Set at INSERT and every UPDATE |
92
106
 
93
107
  ## Audit Pattern
94
108
 
95
- - **Actor tracking:** Use `actor TEXT` in event/transition tables (e.g., `policy_events`, `usage_ledger`).
96
- - **No `updated_by` column.** Changes to mutable tables are tracked via separate event-sourced audit tables (e.g., `workspace_billing_audit`, `harness_change_log`), not inline `updated_by`.
97
- - **Append-only enforcement:** Tables that must never be updated should have a trigger:
109
+ - **Actor tracking:** `actor TEXT` in event and transition tables.
110
+ - **No `updated_by` column.** Changes to mutable tables are tracked in event tables, not inline.
111
+ - **Append-only enforcement:** an immutable table refuses UPDATE by trigger; DELETE follows grants and the deletion matrix.
98
112
  ```sql
99
- CREATE OR REPLACE FUNCTION core.{table}_append_only() RETURNS trigger AS $$
113
+ CREATE OR REPLACE FUNCTION {schema}.{table}_refuse_update() RETURNS trigger AS $$
100
114
  BEGIN
101
- RAISE EXCEPTION '{table} is append-only — UPDATE and DELETE are not permitted';
115
+ RAISE EXCEPTION '{table} is append-only';
102
116
  END;
103
117
  $$ LANGUAGE plpgsql;
104
118
 
105
- CREATE TRIGGER trg_{table}_append_only
106
- BEFORE UPDATE OR DELETE ON core.{table}
107
- FOR EACH ROW EXECUTE FUNCTION core.{table}_append_only();
119
+ CREATE TRIGGER trg_{table}_refuse_update
120
+ BEFORE UPDATE ON {schema}.{table}
121
+ FOR EACH ROW EXECUTE FUNCTION {schema}.{table}_refuse_update();
108
122
  ```
109
123
 
110
- ## RNG
124
+ ## Randomness
111
125
 
112
- - **Always** use `std.crypto.random` (via `allocUuidV7` in `id_format.zig`).
113
- - **Never** use custom RNG implementations or `std.rand`.
126
+ - Identifiers and secrets come from `afd_core::id` and the operating system's cryptographic source; never a custom random number generator.
114
127
  <!-- oracle-packs:end -->
package/docs/TEMPLATE.md CHANGED
@@ -17,7 +17,7 @@ Three markup classes appear below the divider. The gate tells them apart mechani
17
17
  | `tpl:` guidance comment | How to fill the section — written for the authoring agent only | **Deleted after filling.** The gate BLOCKs any survivor. |
18
18
  | SPEC AUTHORING RULES banner comment | Standing constraints on the instance | The one comment that survives, verbatim. |
19
19
 
20
- Everything else — headings, table skeletons, the standard rubric rows — is kept and filled. The result: the executing agent reads **100% instance content, zero template noise**, and the 320-line budget buys signal, not boilerplate.
20
+ Everything else — headings, table skeletons, the standard rubric rows — is kept and filled. The result: the executing agent reads **100% instance content, zero template noise**, and the 500-line budget buys signal, not boilerplate.
21
21
 
22
22
  ## What a spec pins
23
23
 
@@ -89,7 +89,7 @@ A milestone is not complete until evidence is captured: commands, logs, screensh
89
89
 
90
90
  ## Guardrails
91
91
 
92
- - **Length — hard upper bound 320 lines for the filled spec** (after `tpl:` comment deletion). Typical specs 150–300. Past the bound before the Acceptance Rubric → scope is two specs sharing one file; split.
92
+ - **Length — hard upper bound 500 lines for the filled spec** (after `tpl:` comment deletion). Typical specs 150–350. Past the bound before the Acceptance Rubric → scope is two specs sharing one file; split.
93
93
  - **Sections** — 3–9. **Workstreams** — ≤4 (5th cross-cutting only).
94
94
  - **File naming** — `docs/v{N}/{pending|active|done}/M{N}_{NNN}_P{Priority}_{CATEGORIES}_{NAME}.md` (e.g. `M40_001_P0_API_WORKER_SUBSTRATE.md`). Categories alphabetised.
95
95
 
@@ -409,7 +409,7 @@ another product's version number never authorizes a rebuild here.
409
409
 
410
410
  <!-- oracle-packs:start product.agentsfleet -->
411
411
 
412
- **Rule:** While `cat VERSION` < 0.30.0 (teardown-rebuild era), removing tables MUST be a full teardown: (1) delete the SQL file (`rm schema/NNN_foo.sql`), (2) remove the `@embedFile` constant from `schema/embed.zig`, (3) remove the migration array entry from `rustd/crates/afd_db/src/migration.rs` and update its registration tests. Never write ALTER TABLE, DROP TABLE, or `SELECT 1;` placeholders. Never keep version-marker files. Migration slot numbers are not sacred pre-v0.30 — the DB is wiped on every rebuild, and gaps in numbering are fine. From VERSION >= 0.30.0 the datastore is live production data, so every removal or reshape is a numbered ALTER/DROP migration and the teardown path is closed. **Compare the version field by field, never as a string:** `0.29.0` is under the anchor, `0.30.0` is not, and a lexical compare puts `0.30.0` below `0.9.0` — the anchor moved off a major-number boundary precisely so the minor field decides.
412
+ **Rule:** While `cat VERSION` < 0.30.0 (teardown-rebuild era), removing tables MUST be a full teardown: (1) delete the SQL file (`rm schema/NNN_foo.sql`), (2) remove the migration array entry from `rustd/crates/afd_db/src/migration.rs` and update its registration tests. Never write ALTER TABLE, DROP TABLE, or `SELECT 1;` placeholders. Never keep version-marker files. Migration slot numbers are not sacred pre-v0.30 — the DB is wiped on every rebuild, and gaps in numbering are fine. From VERSION >= 0.30.0 the datastore is live production data, so every removal or reshape is a numbered ALTER/DROP migration and the teardown path is closed. **Compare the version field by field, never as a string:** `0.29.0` is under the anchor, `0.30.0` is not, and a lexical compare puts `0.30.0` below `0.9.0` — the anchor moved off a major-number boundary precisely so the minor field decides.
413
413
  **Why:** Markers accumulate dead code and still force CI to splitter-parse them. Below 0.30.0 the development database is dropped and re-created from empty, so there is no production data to protect and full removal leaves no false grep hits or stale migration slots. At 0.30.0 that stops being true: the deployment holds data nobody can re-create, which is what makes an in-place migration the only safe shape from then on.
414
414
  **Tags:** sql, process
415
415
  **Example:** harness teardown — supersedes prior "replace with `SELECT 1;`" guidance. Under the old rule, comment-only markers broke CI (apostrophe in "slots" opened unterminated string in splitter); full deletion avoids the marker problem entirely.
@@ -1004,7 +1004,7 @@ Never infer the absence of deployed clients from another product's version.
1004
1004
  - RPC / handler signature changes → edit the existing struct + every caller in the same commit. No `Foo` and `FooV2`.
1005
1005
  - "Versioned" / "additive optional fields" framing in a spec is fine — what's banned is a *parallel* type, not nullable extensions on the existing one.
1006
1006
  - Removing a behavior → delete the function, the call sites, and any dispatch arm that selected it. Tests of the removed path go too.
1007
- - Schema removals already covered by the Schema Table Removal Guard (rm file, rm `@embedFile`, rm migration array entry) — that's this rule applied to SQL.
1007
+ - Schema removals already covered by the Schema Table Removal Guard (rm file, rm migration array entry) — that's this rule applied to SQL.
1008
1008
  - Legacy-Design Consult Guard still fires when you find a *pre-existing* legacy shim left over from the v1→v2 teardown. This rule says: don't create new ones.
1009
1009
  - Command-line renames follow the same rule: if the product verb moves to `create`, do not add `add` as an alias. If a flag or command spelling changes, use the new spelling only unless Indy explicitly asks for a compatibility alias in the same session.
1010
1010
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary \u2014 for any coding agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,7 +44,7 @@ describe("opt-in pack selection", () => {
44
44
  const installed = await Promise.all(pages.map((path) => Bun.file(join(repo, ".orly", path)).text()));
45
45
  for (const content of installed) {
46
46
  expect(content).not.toContain("playbooks/founding/");
47
- expect(content).not.toContain("schema/embed.zig");
47
+ expect(content).not.toContain("rustd/crates/afd_db/src/migration.rs");
48
48
  expect(content).not.toContain("src/cmd/common.zig");
49
49
  expect(content).not.toContain("build_runner.zig");
50
50
  expect(content).not.toContain("~/Projects/docs/");
@@ -73,7 +73,7 @@ describe("opt-in pack selection", () => {
73
73
  const sql = await Bun.file(join(repo, ".orly/dispatch/write_sql.md")).text();
74
74
  const zig = await Bun.file(join(repo, ".orly/dispatch/write_zig.md")).text();
75
75
  expect(sql).toContain("0.30.0");
76
- expect(sql).toContain("schema/embed.zig");
76
+ expect(sql).toContain("rustd/crates/afd_db/src/migration.rs");
77
77
  expect(sql).toContain("**Forbidden:**");
78
78
  expect(zig).toContain("build_runner.zig");
79
79
  });
@@ -30,6 +30,10 @@ const ERROR_TYPE = "pub struct Error;\n";
30
30
  const VALUE_INIT = "pub fn init() Self { return .{}; }\n";
31
31
  const ALLOCATING_INIT = "pub fn init(a: std.mem.Allocator) !Self { const owned = try a.alloc(u8, COUNT); return .{ .owned = owned }; }\n";
32
32
  const DIFF = "--diff";
33
+ const SPACED = "lib/has space.ts";
34
+ const TRAILING_SPACE = "lib/trail .ts";
35
+ const OVERRIDE = "MILESTONE ID ALLOWED per user override (reason: pin)";
36
+ const MARKED_T = 'const t = "T' + '7";\n';
33
37
  const SCANNERS = ["ufs.sh", "logging.sh", "deinit-pairs.sh", "sql-mod.sh", "error-codes.sh", "rust-error.sh", "design-tokens.sh"];
34
38
 
35
39
  setDefaultTimeout(TEST_TIMEOUT_MS);
@@ -189,6 +193,39 @@ test("diff marker explicit files use working bytes and missing comparisons refus
189
193
  expect(missing.text).toContain("comparison revision is missing: missing-revision");
190
194
  });
191
195
 
196
+ test("diff markers survive spaced and trailing-space filenames", async () => {
197
+ const project = newRepository();
198
+ await staged(project, SPACED, MARKED);
199
+ await staged(project, TRAILING_SPACE, MARKED);
200
+ const result = scan(project, "audits/msid-ui.sh", [STAGED]);
201
+ expect(result.code).toBe(1);
202
+ expect(result.text).toContain(`MS-ID ${SPACED}: // ${MARKER}`);
203
+ expect(result.text).toContain(`MS-ID ${TRAILING_SPACE}: // ${MARKER}`);
204
+ });
205
+
206
+ test("pure rename re-scans the file as a whole-file addition", async () => {
207
+ const project = newRepository();
208
+ await staged(project, "lib/old.ts", MARKED);
209
+ git(project, COMMIT, "-qm", "test: seed rename");
210
+ git(project, "mv", "lib/old.ts", "lib/new.ts");
211
+ const result = scan(project, "audits/msid-ui.sh", [STAGED]);
212
+ expect(result.code).toBe(1);
213
+ expect(result.text).toContain(`MS-ID lib/new.ts: // ${MARKER}`);
214
+ });
215
+
216
+ test("explicit absolute paths keep the caller path and read its post-image", async () => {
217
+ const project = newRepository();
218
+ const absolute = join(temporaryDirectory(), "outside.ts");
219
+ await Bun.write(absolute, `// ${OVERRIDE}\n${MARKED_T}`);
220
+ const suppressed = scan(project, "audits/msid-ui.sh", [absolute]);
221
+ expect(suppressed.code).toBe(0);
222
+ expect(suppressed.text).toContain("0 hits");
223
+ await Bun.write(absolute, MARKED_T);
224
+ const flagged = scan(project, "audits/msid-ui.sh", [absolute]);
225
+ expect(flagged.code).toBe(1);
226
+ expect(flagged.text).toContain(`MS-ID ${absolute}: const t = "T` + `7";`);
227
+ });
228
+
192
229
  test("indexed source links cannot read outside their owned snapshot", async () => {
193
230
  const project = newRepository();
194
231
  const external = join(temporaryDirectory(), "owner.ts");
@@ -104,6 +104,13 @@ describe("spec content readiness", () => {
104
104
  });
105
105
 
106
106
  test("oversize specs report their measured size against the cap", () => {
107
- expect(validateSpec(`${VALID}\n${"line\n".repeat(320)}`, COMMANDS, EXISTS).join("\n")).toMatch(/spec has \d+ lines; cap 320/);
107
+ expect(validateSpec(`${VALID}\n${"line\n".repeat(500)}`, COMMANDS, EXISTS).join("\n")).toMatch(/spec has \d+ lines; cap 500/);
108
+ });
109
+
110
+ test("a spec at the cap passes and one line over fails", () => {
111
+ const base = VALID.trimEnd();
112
+ const padded = (lines: number) => `${base}\n${Array(lines - base.split(/\r?\n/).length).fill("line").join("\n")}`;
113
+ expect(validateSpec(padded(500), COMMANDS, EXISTS).join("\n")).not.toMatch(/cap 500/);
114
+ expect(validateSpec(padded(501), COMMANDS, EXISTS)).toContain("spec has 501 lines; cap 500");
108
115
  });
109
116
  });