-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathowl.fish
More file actions
1955 lines (1748 loc) · 71.3 KB
/
Copy pathowl.fish
File metadata and controls
1955 lines (1748 loc) · 71.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
function owl --description 'Universal code scanner'
if test (count $argv) -eq 0
__owl_usage
return 1
end
set -l subcmd $argv[1]
set -e argv[1]
# Type detection and flag parsing now live inside each command body, which
# classifies $argv itself (a key=value param or a forwarded dash-flag may
# precede the bare type token). The `help` keyword reaches the command as a
# bare positional; `--help` is a forwarded dash-flag (goes to the agent).
switch $subcmd
case scan
__owl_scan $argv
case check
__owl_check $argv
case list
__owl_list $argv
case '*'
echo "owl: unknown command '"(__owl_strip_nonprintable $subcmd)"'" >&2
__owl_usage
return 1
end
end
# Classify every token in $argv[4..] into three caller-scoped lists, named by
# the first three arguments (forward-list, param-list, positional-list).
# Classification:
# - starts with '-' (single or double dash) → forwarded verbatim to agent
# - matches ^[a-z][a-z-]*= (e.g. agent=qwen, p=-p) → owl key=value param (stored verbatim)
# - otherwise (bare token) → positional
# Sets the three named variables globally; the caller copies them to locals and
# erases the globals immediately after.
function __owl_classify_args --argument-names fwd_name param_name pos_name
set -l fwd
set -l params
set -l positionals
for tok in $argv[4..]
if string match -rq '^-' -- $tok
set -a fwd $tok
else if string match -rq '^[a-z][a-z-]*=' -- $tok
set -a params $tok
else
set -a positionals $tok
end
end
set -g $fwd_name $fwd
set -g $param_name $params
set -g $pos_name $positionals
end
# Look up a single key=value param. Echoes the value (everything after the first
# '=') of the LAST matching entry, or nothing if absent. Use `set -ql` semantics
# at the call site by checking exit status: returns 0 if found, 1 if not.
function __owl_param_value --argument-names key
set -l found 1
set -l result
for kv in $argv[2..]
set -l parts (string split -m1 '=' -- $kv)
if test "$parts[1]" = "$key"
set result $parts[2]
set found 0
end
end
test $found -eq 0; and printf '%s\n' $result
return $found
end
function __owl_usage
echo "Usage: owl <command> <type> [options]" >&2
echo "" >&2
echo "Commands:" >&2
echo " scan Scan files for issues of a given type" >&2
echo " check Verify existing reports of a given type" >&2
echo " list List all owl-created files for a given type" >&2
echo "" >&2
echo "Type is a free-form search term (e.g., vulnerability, performance, simplification, \"memory leak\")." >&2
echo "" >&2
echo "Run 'owl scan help' or 'owl check help' for options." >&2
end
function __owl_usage_cmd --argument-names subcmd
switch $subcmd
case scan
echo "Usage: owl scan <type> [key=value ...] [agent-flags ...] [file ...]" >&2
echo "" >&2
echo "Type is what to scan for (e.g., vulnerability, performance, \"memory leak\")." >&2
echo "" >&2
echo "Run 'owl scan help' for full options." >&2
case check
echo "Usage: owl check <type> [key=value ...] [agent-flags ...] [file ...]" >&2
echo "" >&2
echo "Type is what to verify (e.g., vulnerability, performance, \"memory leak\")." >&2
echo "" >&2
echo "Run 'owl check help' for full options." >&2
case list
echo "Usage: owl list [type] [depth=N]" >&2
echo "" >&2
echo "Type is the scan type to list files for (e.g., vulnerability, performance)." >&2
echo "" >&2
echo "Run 'owl list help' for full options." >&2
end
end
function __owl_check_tools --argument-names cmd
# Tool registry: each tool is tagged with the commands that need it
# date → scan check (rate limit time parsing)
# fd → scan check list (file discovery)
# git → scan check list (file discovery)
# tree → list (display)
if contains -- $cmd scan check
if not __owl_is_gnu_date
echo "info: GNU coreutils date recommended for more reliable timezone handling (brew install coreutils)" >&2
end
end
if contains -- $cmd scan check list
if not command -sq fd
echo "info: installing fd is recommended — helps reduce checking irrelevant files" >&2
end
if not command -sq git
echo "info: installing git is recommended for repository-aware file discovery" >&2
end
if not command -sq fd; and not command -sq git
echo "warning: no files will be skipped — consider installing git or fd if that is a concern" >&2
end
end
if contains -- $cmd list
if not command -sq tree
echo "info: installing tree is recommended for better owl list output (brew install tree)" >&2
end
end
return 0
end
function __owl_resolve_agent --argument-names agent_override
if test -n "$agent_override"
# A path (contains /) must be executable as given; a bare name resolves on $PATH.
if string match -q '*/*' -- $agent_override
if test -x "$agent_override"
echo $agent_override
return 0
end
echo "owl: agent binary not found at '$agent_override'" >&2
return 1
end
if command -sq $agent_override
command -s $agent_override
return 0
end
echo "owl: agent binary '$agent_override' not found on \$PATH" >&2
return 1
end
for bin in claude claude-code
if command -sq $bin
command -s $bin
return 0
end
end
echo "owl: no agent binary found — install claude or pass agent=<name|path>" >&2
return 1
end
# Filename-safe agent label for state-file names: basename of the flag value
# (so /usr/local/bin/mimo → mimo), dots neutralized so they can't clash with
# the '.' that separates agent from slug; empty → the default 'claude'.
function __owl_agent_name --argument-names agent_flag
if test -z "$agent_flag"
echo claude
return 0
end
string replace -r '^.*/' '' -- $agent_flag | string replace -a '.' '-'
end
function __owl_slugify --argument-names input
string lower -- $input | string replace -a ' ' '-' | string replace -ra '[^a-z0-9-]' ''
end
function __owl_strip_nonprintable --argument-names input
string replace -ra '[^[:print:]]' '' -- $input
end
function __owl_chk_path --argument-names filepath
string replace -r '\.md$' '.chk.md' -- $filepath
end
function __owl_is_gnu_date
date --version >/dev/null 2>&1
end
function __owl_parse_rate_limit --argument-names output retry_delay
# Match: "resets 11:30pm (America/New_York)" (with minutes)
# or: "resets 4am (Europe/Berlin)" (without minutes)
# Fish omits non-participating optional groups, so use two patterns.
set -l hour
set -l minute 0
set -l ampm
set -l tz
set -l match (string match -r 'resets (\d{1,2}):(\d{2})(am|pm) \(([^)]+)\)' -- $output)
if test (count $match) -ge 5
set hour $match[2]
set minute $match[3]
set ampm $match[4]
set tz $match[5]
else
set match (string match -r 'resets (\d{1,2})(am|pm) \(([^)]+)\)' -- $output)
if test (count $match) -ge 4
set hour $match[2]
set ampm $match[3]
set tz $match[4]
else
# Parsing failed — return fallback 30 minutes + retry_delay
echo (math "1800 + $retry_delay")
return 1
end
end
# Convert to 24h
if test "$ampm" = pm -a "$hour" -ne 12
set hour (math "$hour + 12")
else if test "$ampm" = am -a "$hour" -eq 12
set hour 0
end
set -l time_str (printf '%02d:%02d:00' $hour $minute)
# Compute target epoch — cross-platform
set -l target_epoch
if __owl_is_gnu_date
set target_epoch (TZ=$tz date -d "today $time_str" +%s 2>/dev/null)
else
set target_epoch (TZ=$tz date -j -f '%H:%M:%S' $time_str +%s 2>/dev/null)
end
if test -z "$target_epoch"
echo (math "1800 + $retry_delay")
return 1
end
set -l now (date +%s)
set -l delta (math "$target_epoch - $now")
if test $delta -le 0
set delta (math "$delta + 86400")
end
set delta (math "$delta + $retry_delay")
echo $delta
return 0
end
function __owl_format_reset_time --argument-names output
set -l match (string match -r 'resets (\d{1,2}(?::\d{2})?(?:am|pm)) \(([^)]+)\)' -- $output)
if test (count $match) -lt 3
echo "unknown"
return 1
end
echo (string upper $match[2])" "$match[3]
return 0
end
function __owl_validate_uint --argument-names val
string match -rq '^[0-9]+$' -- $val
end
# Collect error.* keys from params → "ACTION=PATTERN" lines (strips "error." prefix).
function __owl_collect_error_signals
for param in $argv
set -l pkv (string split -m1 '=' -- $param)
if string match -q 'error.*' -- $pkv[1]
echo (string sub -s 7 -- $pkv[1])"=$pkv[2]"
end
end
end
function __owl_validate_bool --argument-names val
switch $val
case true false
return 0
case '*'
return 1
end
end
function __owl_validate_ignore --argument-names val
switch $val
case true false yes no 0 1
return 0
case '*'
return 1
end
end
function __owl_validate_extension --argument-names val
string match -rq '^[a-zA-Z0-9._-]+$' -- $val
end
function __owl_validate_state_file --argument-names val
if test -z "$val"
return 1
end
if string match -rq '^-' -- $val
return 1
end
if string match -rq '[[:cntrl:]]' -- $val
return 1
end
return 0
end
function __owl_safe_path --argument-names p
# Prepared-statement binder for paths reaching find/fd.
# Neutralizes leading-dash filenames by anchoring with './' so downstream
# tools parse the value as a path, never as an option or action primary.
if string match -q '/*' -- $p; or string match -q './*' -- $p; or test "$p" = .
echo $p
else
echo "./$p"
end
end
function __owl_check_in_cwd --argument-names filepath
set -l cwd (pwd -P)
set -l real (realpath -- $filepath 2>/dev/null; or realpath -- (dirname -- $filepath)/(basename -- $filepath) 2>/dev/null)
# Reject multi-element realpath output (filepath contains a newline or other char
# that fish splits on) — a single logical path must resolve to exactly one value.
if test (count $real) -ne 1
echo "owl: refusing to access $filepath — ambiguous path resolution" >&2
return 1
end
# Use anchored regex with escaped cwd so glob metachars ('*', '?', '[') inside
# cwd cannot broaden the prefix match. Trailing '/' on $real normalizes cwd itself.
set -l cwd_re (string escape --style=regex -- $cwd)
if not string match -rq "^$cwd_re/" -- "$real/"
echo "owl: refusing to access $filepath — outside cwd ($cwd)" >&2
echo "owl: run from a directory containing the target, or pass a path relative to cwd" >&2
return 1
end
return 0
end
function __owl_state_commit --argument-names state_file
# Reads content from stdin, writes atomically to state_file.
# Rejects symlinks and paths outside cwd. Uses mktemp + mv to eliminate TOCTOU races.
if test -L $state_file
echo "owl: refusing to write — $state_file is a symlink" >&2
return 1
end
__owl_check_in_cwd $state_file; or return 1
# Colocate tmp with destination so mv uses rename() (atomic, same filesystem,
# does not follow symlinks at destination). Default mktemp puts files in $TMPDIR,
# which is typically a different filesystem → mv falls back to copy-through,
# which opens the destination path and follows symlinks (TOCTOU window).
set -l tmp (mktemp "$state_file.XXXXXX")
cat > $tmp
command mv -f -- $tmp $state_file
end
function __owl_state_write --argument-names state_file
# Remaining argv = key:value pairs, then -- separator, then files
set -l params
set -l files
set -l past_sep no
for arg in $argv[2..]
if test "$past_sep" = yes
set -a files $arg
else if test "$arg" = --
set past_sep yes
else
set -a params $arg
end
end
begin
echo "---"
for param in $params
echo "$param"
end
echo "---"
echo ""
for file in $files
echo "- [ ] $file"
end
end | __owl_state_commit $state_file
end
function __owl_state_read_params --argument-names state_file
if not test -f "$state_file"
echo "owl: state file not found: $state_file" >&2
return 1
end
set -l in_frontmatter no
while read -l line
if test "$line" = "---"
if test "$in_frontmatter" = yes
return 0
end
set in_frontmatter yes
continue
end
if test "$in_frontmatter" = yes
echo "$line"
end
end < $state_file
return 0
end
function __owl_state_read_files --argument-names state_file
set -l past_frontmatter no
set -l frontmatter_count 0
while read -l line
if test "$line" = "---"
set frontmatter_count (math "$frontmatter_count + 1")
if test $frontmatter_count -ge 2
set past_frontmatter yes
end
continue
end
if test "$past_frontmatter" = yes
set -l entry (string match -r '^\- \[([ x])\] (.+)$' -- $line)
if test (count $entry) -ge 3
set -l filepath $entry[3]
if not test -f "$filepath"
echo "owl: skipping $filepath — not found" >&2
continue
end
__owl_check_in_cwd $filepath; or continue
printf '%s\t%s\n' $entry[2] $filepath
end
end
end < $state_file
end
function __owl_state_mark_done --argument-names state_file filepath
# Exact-line rewrite. No regex on either side — immune to $N, {}, glob metachars
# in filenames. Attacker-controlled names can't corrupt state entries.
begin
while read -l line
if test "$line" = "- [ ] $filepath"
echo "- [x] $filepath"
else
echo $line
end
end < $state_file
end | __owl_state_commit $state_file
end
function __owl_state_update_params --argument-names state_file
set -l params $argv[2..]
# Read everything after frontmatter
set -l body
set -l past_frontmatter no
set -l frontmatter_count 0
while read -l line
if test "$line" = "---"
set frontmatter_count (math "$frontmatter_count + 1")
if test $frontmatter_count -ge 2
set past_frontmatter yes
end
continue
end
if test "$past_frontmatter" = yes
set -a body $line
end
end < $state_file
begin
echo "---"
for param in $params
echo "$param"
end
echo "---"
for line in $body
echo "$line"
end
end | __owl_state_commit $state_file
end
function __owl_discover_files --argument-names mode depth respect_ignore slug search_dir
# remaining argv: includes... -- excludes...
set -l includes
set -l excludes
set -l past_sep no
for arg in $argv[6..]
if test "$arg" = --
set past_sep yes
else if test "$past_sep" = yes
set -a excludes $arg
else
set -a includes $arg
end
end
test -z "$search_dir"; and set search_dir .
set -l safe_dir (__owl_safe_path $search_dir)
set -l has_fd (command -sq fd; and echo yes; or echo no)
set -l has_git (command -sq git; and echo yes; or echo no)
set -l in_git_repo no
if test "$has_git" = yes; and git rev-parse --is-inside-work-tree >/dev/null 2>&1
set in_git_repo yes
end
# Tier 1: fd
if test "$has_fd" = yes
set -l fd_args --type f --max-depth $depth
if test "$mode" = check
set -a fd_args -e "$slug.md"
else if test (count $includes) -gt 0
for inc in $includes
set -a fd_args -e $inc
end
end
for exc in $excludes
set -a fd_args --exclude "*$exc"
end
if test "$respect_ignore" = false
set -a fd_args --no-ignore
end
fd $fd_args $safe_dir
return
end
# Tier 2: git ls-files (pipe through grep for excludes)
if test "$has_git" = yes; and test "$in_git_repo" = yes
if test "$respect_ignore" = false
echo "info: --ignore flag has no effect with git backend" >&2
end
set -l git_output
set -l dir_prefix
if test "$search_dir" != .
set dir_prefix "$search_dir/"
end
if test "$mode" = check
set git_output (git ls-files -- "$dir_prefix*.$slug.md")
else if test (count $includes) -gt 0
set -l patterns
for inc in $includes
set -a patterns "$dir_prefix*.$inc"
end
set git_output (git ls-files -- $patterns)
else if test -n "$dir_prefix"
set git_output (git ls-files -- "$dir_prefix")
else
set git_output (git ls-files)
end
# Apply exclude filters
for f in $git_output
set -l skip no
for exc in $excludes
if string match -q "*$exc" -- $f
set skip yes
break
end
end
if test "$skip" = no
echo $f
end
end
return
end
# Tier 3: find (pipe through grep for excludes)
set -l find_output
if test "$mode" = check
set find_output (command find $safe_dir -maxdepth $depth -name "*.$slug.md" -type f)
else if test (count $includes) -gt 0
set -l find_expr
for i in (seq (count $includes))
test $i -gt 1; and set -a find_expr -o
set -a find_expr -name "*.$includes[$i]"
end
set find_output (command find $safe_dir -maxdepth $depth -type f \( $find_expr \))
else
set find_output (command find $safe_dir -maxdepth $depth -type f)
end
# Apply exclude filters
for f in $find_output
set -l skip no
for exc in $excludes
if string match -q "*$exc" -- $f
set skip yes
break
end
end
if test "$skip" = no
echo $f
end
end
end
# Resolve positional args (files and directories) into a flat file list.
# Usage: __owl_resolve_paths depth respect_ignore includes... -- excludes... -- paths...
function __owl_resolve_paths
set -l depth $argv[1]
set -l respect_ignore $argv[2]
set -l includes
set -l excludes
set -l paths
set -l section includes
for arg in $argv[3..]
if test "$arg" = --
switch $section
case includes
set section excludes
case excludes
set section paths
end
else
switch $section
case includes
set -a includes $arg
case excludes
set -a excludes $arg
case paths
set -a paths $arg
end
end
end
for p in $paths
__owl_check_in_cwd $p; or continue
if test -d "$p"
__owl_discover_files all $depth $respect_ignore "" $p $includes -- $excludes
else if test -f "$p"
# Apply exclude filter to explicit files too
set -l skip no
for exc in $excludes
if string match -q "*$exc" -- $p
set skip yes
break
end
end
if test "$skip" = no
echo $p
end
else
echo "owl: path not found: $p" >&2
end
end
end
function __owl_print_params
echo "--- parameters ---" >&2
for param in $argv
echo " $param" >&2
end
echo "------------------" >&2
end
# Shared loop: runs the agent on each file with state tracking and rate limit handling.
# Usage: __owl_run_agent AGENT_BIN USE_MEMORY LABEL PROMPT_TEMPLATE STATE_FILE RETRY_DELAY TIMEOUT P_FLAG S_FLAG SYSTEM_PROMPT [FORWARD_ARGS...] -- FILE...
# {} in PROMPT_TEMPLATE is replaced with the current file path.
# owl owns prompt building, file iteration, state, rate-limit retry, the per-file
# timeout watchdog and SIGINT; the agent's own flags are supplied by the user and
# forwarded verbatim (FORWARD_ARGS). Prompt/system-prompt delivery is wired by the
# caller via P_FLAG/S_FLAG: an empty P_FLAG means "do not inject the prompt", an
# empty S_FLAG means "do not send the system prompt".
function __owl_run_agent
set -l agent_bin $argv[1]
set -l use_memory $argv[2]
set -l label $argv[3]
set -l prompt_tpl $argv[4]
set -l state_file $argv[5]
set -l retry_delay $argv[6]
set -l timeout $argv[7]
set -l p_flag $argv[8]
set -l s_flag $argv[9]
set -l system_prompt $argv[10]
string match -rq '^[0-9]+$' -- "$timeout"; or set timeout 0
set -l forward_args
set -l files
set -l past_sep no
for arg in $argv[11..]
if test "$past_sep" = yes
set -a files $arg
else if test "$arg" = --
set past_sep yes
else
set -a forward_args $arg
end
end
set -l total (count $files)
echo "Found $total files" >&2
if test $total -eq 0
return 0
end
# Guard against concurrent owl instances in the same shell
if set -qg __owl_agent_pid_$fish_pid
echo "owl: another instance is already running in this shell — use a separate terminal" >&2
return 1
end
# Track agent PID for interrupt handler ($fish_pid-scoped)
set -l _apid __owl_agent_pid_$fish_pid
set -l _int __owl_interrupted_$fish_pid
set -g $_apid 0
set -g $_int no
# Scoped SIGINT handler
function __owl_sigint_handler_$fish_pid --on-signal SIGINT
set -l _apid __owl_agent_pid_$fish_pid
set -l _int __owl_interrupted_$fish_pid
set -g $_int yes
if test "$$_apid" -ne 0
kill -TERM $$_apid 2>/dev/null
wait $$_apid 2>/dev/null
end
end
set -l completed 0
for file in $files
# Check if interrupted
if test "$$_int" = yes
echo "" >&2
echo "Interrupted — progress saved to $state_file" >&2
echo "Resume with: owl $label resume state-file=$state_file" >&2
functions -e __owl_sigint_handler_$fish_pid
set -e $_apid $_int
return 130
end
# Check state file — skip if already done
set -l file_escaped (string escape --style=regex -- $file)
set -l file_line (string match -r "^\- \[([ x])\] $file_escaped\$" < $state_file)
if test (count $file_line) -ge 2; and test "$file_line[2]" = x
set completed (math "$completed + 1")
continue
end
set completed (math "$completed + 1")
printf '\033]0;owl %s [%d/%d] %s\007' $label $completed $total $file >&2
echo "[$completed/$total] $file" >&2
# Two-pass substitution with per-iteration sentinels. Pass 1 rewrites template
# markers to unique tokens (no attacker data involved). Pass 2 substitutes real
# content for the tokens. Filenames containing {}, {raw}, {chk}, {raw-chk} cannot
# be re-consumed because no markers remain after pass 1.
set -l sid (random)(random)(random)
set -l s_rawchk "@@OWL_"$sid"_RAWCHK@@"
set -l s_raw "@@OWL_"$sid"_RAW@@"
set -l s_braces "@@OWL_"$sid"_BRACES@@"
set -l s_chk "@@OWL_"$sid"_CHK@@"
set -l prompt (string replace --all -- '{raw-chk}' $s_rawchk "$prompt_tpl" | string join \n)
set prompt (string replace --all -- '{raw}' $s_raw "$prompt" | string join \n)
set prompt (string replace --all -- '{}' $s_braces "$prompt" | string join \n)
set prompt (string replace --all -- '{chk}' $s_chk "$prompt" | string join \n)
set prompt (string replace --all -- $s_rawchk (__owl_chk_path $file) "$prompt" | string join \n)
set prompt (string replace --all -- $s_raw $file "$prompt" | string join \n)
set prompt (string replace --all -- $s_braces '`'"$file"'`' "$prompt" | string join \n)
set prompt (string replace --all -- $s_chk '`'(__owl_chk_path $file)'`' "$prompt" | string join \n)
# Invocation order: <forwarded-args...> [<s-flag> <system-prompt>] [<p-flag> <prompt>].
# owl makes no assumptions about the agent's flags — they are forwarded as given.
# If s_flag contains a space (e.g. "-c developer_instructions="), the part before
# the space is the flag and the part after is prepended to the system prompt as a
# single argument: -c "developer_instructions=<system-prompt>".
set -l agent_args $forward_args
if test -n "$s_flag"
if string match -q '* *' -- $s_flag
set -l s_parts (string split -m1 ' ' -- $s_flag)
set -a agent_args $s_parts[1] "$s_parts[2]$system_prompt"
else
set -a agent_args $s_flag $system_prompt
end
end
if test -n "$p_flag"
set -a agent_args $p_flag $prompt
end
# Retry loop for rate limits on this file
while true
if test "$$_int" = yes
break
end
set -l tmp_out (mktemp)
set -l run_cmd $agent_bin $agent_args
if test "$use_memory" = false
set run_cmd env -i HOME=$HOME PATH=(string join : $PATH) TMPDIR=$TMPDIR USER=$USER \
SECURITYSESSIONID=$SECURITYSESSIONID CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 $run_cmd
end
$run_cmd > $tmp_out 2>&1 &
set -g $_apid $last_pid
# Watchdog: poll the agent, kill it if it runs past the timeout (0 = no limit)
set -l elapsed 0
set -l timed_out no
while kill -0 $$_apid 2>/dev/null
if test "$$_int" = yes
break
end
if test "$timeout" -gt 0; and test $elapsed -ge "$timeout"
set timed_out yes
kill -TERM $$_apid 2>/dev/null
break
end
sleep 1
set elapsed (math "$elapsed + 1")
end
wait $$_apid 2>/dev/null
set -g $_apid 0
# Read once, then discard
set -l output (cat $tmp_out)
rm -f $tmp_out
# Show captured output
printf '%s\n' $output >&2
# Check for interrupt during agent execution
if test "$$_int" = yes
break
end
# Agent exceeded the per-file timeout — skip without marking done
if test "$timed_out" = yes
echo "" >&2
echo "owl: agent timed out after "(math "floor($timeout / 60)")"m on $file — left unmarked; rerun with resume to retry" >&2
break
end
# Check error signals (profile/CLI-configured stop/pause patterns).
# Each signal is ACTION=PATTERN where ACTION is stop, pause.N, or
# pause.smart. PATTERN is a glob unless prefixed with "regex:".
# First match wins. No match → success.
set -l _signals $__owl_error_signals_$fish_pid
if test (count $_signals) -eq 0
# Backward compat: no profile/CLI signals → legacy Claude patterns
set _signals \
'stop=*Not logged in*' \
'pause.smart=regex:resets \d{1,2}(?::\d{2})?(?:am|pm) \('
end
set -l signal_action none
set -l signal_wait 0
set -l signal_pattern
for signal in $_signals
set -l skv (string split -m1 '=' -- $signal)
test (count $skv) -lt 2; and continue
set -l action $skv[1]
set -l pattern $skv[2]
set -l matched no
if string match -q 'regex:*' -- $pattern
string match -rq -- (string sub -s 7 -- $pattern) $output; and set matched yes
else
string match -q -- $pattern $output; and set matched yes
end
test "$matched" = yes; or continue
set signal_pattern $pattern
if test "$action" = stop
set signal_action stop
else if test "$action" = pause.smart
set signal_action pause
set signal_wait (__owl_parse_rate_limit "$output" $retry_delay)
else if string match -q 'pause.*' -- $action
set signal_action pause
set signal_wait (string sub -s 7 -- $action)
string match -rq '^[0-9]+$' -- $signal_wait; or set signal_wait 60
end
break
end
if test "$signal_action" = stop
echo "owl: fatal error matched ($signal_pattern) — aborting run" >&2
set -g $_int yes
break
else if test "$signal_action" = pause
if test "$signal_wait" -gt 120
set -l display (math "floor($signal_wait / 60)")"m"
else
set -l display "$signal_wait""s"
end
echo "owl: error matched ($signal_pattern) — pausing $display" >&2
printf '\033]0;owl %s [%d/%d]: paused %s\007' $label $completed $total "$display" >&2
sleep $signal_wait
if test "$$_int" = yes
break
end
printf '\033]0;owl %s [%d/%d] %s\007' $label $completed $total $file >&2
echo "Resuming — retrying [$completed/$total] $file" >&2
continue
end
# Success — mark done in state file
__owl_state_mark_done $state_file $file
break
end
end
# Clean up handler and globals
functions -e __owl_sigint_handler_$fish_pid
set -l was_interrupted $$_int
set -e $_apid $_int __owl_error_signals_$fish_pid
# Check final state
if test "$was_interrupted" = yes
echo "" >&2
echo "Interrupted — progress saved to $state_file" >&2
echo "Resume with: owl $label --resume --state-file $state_file" >&2
return 130
end
printf '\033]0;owl %s [%d/%d]: done\007' $label $total $total >&2
echo "All $total files processed" >&2
end
# Load a profile file and echo key=value lines for the given command (scan|check).
# Shared keys (agent, p, s) are always included; per-command keys (scan.*, check.*)
# are included only for the matching command. forward= may repeat.
function __owl_load_profile --argument-names name cmd
set -l profile_file
# A path (contains /) is used as-is; a bare name searches known directories.
if string match -q '*/*' -- $name
set profile_file $name
else
for dir in ~/.config/owl/profiles (dirname (status current-filename 2>/dev/null) 2>/dev/null)/profiles
if test -f "$dir/$name"
set profile_file "$dir/$name"
break
end
end
end
if test -z "$profile_file"; or not test -f "$profile_file"
echo "owl: profile not found: $name" >&2
return 1
end
while read -l line
# Skip comments and blank lines
string match -qr '^\s*#' -- $line; and continue
string match -qr '^\s*$' -- $line; and continue
set -l kv (string split -m1 '=' -- $line)
test (count $kv) -lt 2; and continue
set -l key $kv[1]
set -l val $kv[2]
# Per-command keys: include only the matching command's.
# Dotted keys that are NOT a known command prefix (scan/check) are
# shared keys (e.g. error.stop) and pass through unchanged.
if string match -q "$cmd.*" -- $key
set key (string sub -s (math (string length "$cmd.") + 1) -- $key)
else if string match -q 'scan.*' -- $key; or string match -q 'check.*' -- $key
continue
end
printf '%s=%s\n' $key $val
end < $profile_file
end
function __owl_scan_help
echo "Usage: owl scan <type> [key=value ...] [agent-flags ...] [file|dir ...]" >&2
echo "" >&2
echo "owl params (key=value):" >&2
echo " agent=NAME|PATH Agent binary name or path (default: claude)" >&2
echo " profile=NAME Load agent defaults from a profile (e.g. profile=claude)" >&2
echo " CLI params override profile values" >&2
echo " depth=N Max directory depth (default: 10)" >&2
echo " ignore=BOOL Respect ignore files (default: true)" >&2
echo " include=EXT,EXT Include files by extension (comma-separated)" >&2
echo " exclude=SFX,SFX Exclude files by suffix (comma-separated)" >&2
echo " memory=BOOL Allow agent memory and skills (default: false)" >&2
echo " state-file=PATH Progress file path (default: .owl-scn-\$agent.\$slug.md)" >&2
echo " retry-delay=N Extra seconds after rate limit reset (default: 1)" >&2
echo " timeout=N Max seconds per file before killing a stalled agent (0=off, default: 1200)" >&2
echo " p=FLAG Prompt-delivery flag — owl appends '<FLAG> <prompt>' (e.g. p=-p, p=exec)" >&2
echo " Omit p= and owl does not inject the prompt (wire it via forwarded args)." >&2
echo " s=FLAG System-prompt-delivery flag — owl appends '<FLAG> <system-prompt>'" >&2
echo " (e.g. s=--append-system-prompt). Omit s= and no system prompt is sent." >&2
echo " error.stop=GLOB Abort the run when agent output matches GLOB (file stays unmarked)" >&2
echo " error.pause.N=GLOB Sleep N seconds then retry the file when output matches GLOB" >&2
echo " error.pause.smart=REGEX Like pause, but parses a rate-limit reset time from output" >&2
echo " Prefix GLOB with 'regex:' for regex matching. May repeat." >&2
echo "" >&2
echo "Keywords (bare):" >&2