@agentsfleet/orly 0.14.3 → 0.14.4

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/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.4 after its version change merges to `main`.
82
82
 
83
83
  ---
84
84
 
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
 
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.4 — 2026-10-16
4
+
5
+ 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.
6
+
7
+ - **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.
8
+ - **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.
9
+
3
10
  ## 0.14.3 — 2026-10-15
4
11
 
5
12
  The REST API guide carries agentsfleet's conversation and reviewed-setup exceptions, and the TypeScript rules name how timestamps are labelled.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
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",
@@ -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");