@ordewell/core 0.4.7 → 0.4.9

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/dist/index.mjs CHANGED
@@ -108,7 +108,6 @@ var AUTO_COMMANDS = [
108
108
  "pwd",
109
109
  "which",
110
110
  "type",
111
- "env",
112
111
  "uname",
113
112
  "whoami",
114
113
  "cut",
@@ -119,13 +118,1233 @@ var AUTO_COMMANDS = [
119
118
  "jq",
120
119
  "yq"
121
120
  ];
121
+ var UNIVERSAL_FLAGS = ["--help", "--version"];
122
+ var COUNT_FLAG = /^-\d+$/;
123
+ var FLAG_POLICY = {
124
+ ls: {
125
+ cluster: true,
126
+ booleans: [
127
+ "-1",
128
+ "-a",
129
+ "-A",
130
+ "-b",
131
+ "-c",
132
+ "-C",
133
+ "-d",
134
+ "-D",
135
+ "-f",
136
+ "-F",
137
+ "-g",
138
+ "-G",
139
+ "-h",
140
+ "-H",
141
+ "-i",
142
+ "-k",
143
+ "-l",
144
+ "-L",
145
+ "-m",
146
+ "-n",
147
+ "-N",
148
+ "-o",
149
+ "-p",
150
+ "-q",
151
+ "-Q",
152
+ "-r",
153
+ "-R",
154
+ "-s",
155
+ "-S",
156
+ "-t",
157
+ "-u",
158
+ "-U",
159
+ "-v",
160
+ "-x",
161
+ "-X",
162
+ "-Z",
163
+ "--all",
164
+ "--almost-all",
165
+ "--author",
166
+ "--escape",
167
+ "--classify",
168
+ "--file-type",
169
+ "--full-time",
170
+ "--no-group",
171
+ "--human-readable",
172
+ "--si",
173
+ "--dereference-command-line",
174
+ "--inode",
175
+ "--kibibytes",
176
+ "--dereference",
177
+ "--numeric-uid-gid",
178
+ "--literal",
179
+ "--hide-control-chars",
180
+ "--show-control-chars",
181
+ "--quote-name",
182
+ "--reverse",
183
+ "--recursive",
184
+ "--size",
185
+ "--directory",
186
+ "--group-directories-first",
187
+ "--context",
188
+ "--zero"
189
+ ],
190
+ values: [
191
+ "-w",
192
+ "-I",
193
+ "-T",
194
+ "--width",
195
+ "--ignore",
196
+ "--hide",
197
+ "--block-size",
198
+ "--color",
199
+ "--format",
200
+ "--sort",
201
+ "--time",
202
+ "--time-style",
203
+ "--indicator-style",
204
+ "--quoting-style",
205
+ "--tabsize"
206
+ ]
207
+ },
208
+ tree: {
209
+ cluster: true,
210
+ // `-o` is absent on purpose: it is tree's write-to-a-file flag, which the
211
+ // redirect check never sees because there is no redirect.
212
+ booleans: [
213
+ "-a",
214
+ "-d",
215
+ "-f",
216
+ "-i",
217
+ "-l",
218
+ "-x",
219
+ "-C",
220
+ "-n",
221
+ "-p",
222
+ "-s",
223
+ "-h",
224
+ "-u",
225
+ "-g",
226
+ "-D",
227
+ "-t",
228
+ "-c",
229
+ "-r",
230
+ "-F",
231
+ "-q",
232
+ "-N",
233
+ "-Q",
234
+ "-v",
235
+ "-J",
236
+ "-X",
237
+ "-Z",
238
+ "--noreport",
239
+ "--dirsfirst",
240
+ "--du",
241
+ "--prune",
242
+ "--matchdirs",
243
+ "--inodes",
244
+ "--device",
245
+ "--si",
246
+ "--gitignore",
247
+ "--ignore-case",
248
+ "--filesfirst",
249
+ "--nolinks",
250
+ "--hintro"
251
+ ],
252
+ values: [
253
+ "-L",
254
+ "-P",
255
+ "-I",
256
+ "-H",
257
+ "--filelimit",
258
+ "--timefmt",
259
+ "--charset",
260
+ "--sort",
261
+ "--gitfile",
262
+ "--info",
263
+ "--prefix"
264
+ ]
265
+ },
266
+ git: {
267
+ cluster: true,
268
+ // Absent on purpose, and each for a reason that was demonstrated rather than
269
+ // guessed at:
270
+ // -c, --config-env set configuration inline, and several configuration
271
+ // keys name a program to run (`core.pager`, `core.editor`,
272
+ // `diff.external`), so this is an execution flag with one
273
+ // indirection in front of it.
274
+ // --exec-path redirects where git finds its helper programs; a
275
+ // fabricated helper ran during an ordinary remote listing.
276
+ // -O, --open-files-in-pager runs an arbitrary named program over search hits.
277
+ // --upload-pack, --receive-pack name the program run on the other end.
278
+ // --textconv, --ext-diff run filters the repository itself configures.
279
+ // --git-dir, --work-tree repoint git at a tree whose configuration is not
280
+ // the one the developer is looking at.
281
+ // -o, --output write a file. `git ls-files -o` is a legitimate spelling
282
+ // of `--others`, but `git archive -o` writes, and the
283
+ // long spelling stays available for the read.
284
+ booleans: [
285
+ "-a",
286
+ "-b",
287
+ "-E",
288
+ "-F",
289
+ "-h",
290
+ "-H",
291
+ "-i",
292
+ "-I",
293
+ "-l",
294
+ "-p",
295
+ "-P",
296
+ "-q",
297
+ "-r",
298
+ "-R",
299
+ "-s",
300
+ "-t",
301
+ "-u",
302
+ "-v",
303
+ "-w",
304
+ "-z",
305
+ "--oneline",
306
+ "--graph",
307
+ "--stat",
308
+ "--shortstat",
309
+ "--numstat",
310
+ "--dirstat",
311
+ "--summary",
312
+ "--name-only",
313
+ "--name-status",
314
+ "--raw",
315
+ "--patch",
316
+ "--no-patch",
317
+ "--patch-with-stat",
318
+ "--cached",
319
+ "--staged",
320
+ "--porcelain",
321
+ "--short",
322
+ "--long",
323
+ "--branch",
324
+ "--show-stash",
325
+ "--show-current",
326
+ "--all",
327
+ "--tags",
328
+ "--heads",
329
+ "--remotes",
330
+ "--list",
331
+ "--verbose",
332
+ "--quiet",
333
+ "--abbrev-ref",
334
+ "--count",
335
+ "--reverse",
336
+ "--first-parent",
337
+ "--no-merges",
338
+ "--merges",
339
+ "--follow",
340
+ "--full-history",
341
+ "--simplify-merges",
342
+ "--sparse",
343
+ "--dense",
344
+ "--ancestry-path",
345
+ "--left-right",
346
+ "--cherry",
347
+ "--cherry-pick",
348
+ "--cherry-mark",
349
+ "--boundary",
350
+ "--topo-order",
351
+ "--date-order",
352
+ "--author-date-order",
353
+ "--relative-date",
354
+ "--parents",
355
+ "--children",
356
+ "--timestamp",
357
+ "--no-walk",
358
+ "--do-walk",
359
+ "--decorate",
360
+ "--no-decorate",
361
+ "--abbrev-commit",
362
+ "--no-abbrev-commit",
363
+ "--abbrev",
364
+ "--no-abbrev",
365
+ "--color",
366
+ "--no-color",
367
+ "--pretty",
368
+ "--format",
369
+ "--expand-tabs",
370
+ "--no-expand-tabs",
371
+ "--numbered",
372
+ "--email",
373
+ "--no-notes",
374
+ "--notes",
375
+ "--show-signature",
376
+ "--find-renames",
377
+ "--find-copies",
378
+ "--find-copies-harder",
379
+ "--break-rewrites",
380
+ "--no-renames",
381
+ "--irreversible-delete",
382
+ "--full-index",
383
+ "--binary",
384
+ "--text",
385
+ "--check",
386
+ "--exit-code",
387
+ "--word-diff",
388
+ "--function-context",
389
+ "--ignore-all-space",
390
+ "--ignore-space-change",
391
+ "--ignore-space-at-eol",
392
+ "--ignore-blank-lines",
393
+ "--ignore-cr-at-eol",
394
+ "--no-prefix",
395
+ "--default-prefix",
396
+ "--no-textconv",
397
+ "--no-ext-diff",
398
+ "--submodule",
399
+ "--ignore-submodules",
400
+ "--recurse-submodules",
401
+ "--no-recurse-submodules",
402
+ "--others",
403
+ "--deleted",
404
+ "--modified",
405
+ "--unmerged",
406
+ "--stage",
407
+ "--killed",
408
+ "--ignored",
409
+ "--exclude-standard",
410
+ "--error-unmatch",
411
+ "--full-name",
412
+ "--directory",
413
+ "--no-empty-directory",
414
+ "--untracked-files",
415
+ "--no-optional-locks",
416
+ "--no-pager",
417
+ "--paginate",
418
+ "--bare",
419
+ "--literal-pathspecs",
420
+ "--glob-pathspecs",
421
+ "--noglob-pathspecs",
422
+ "--icase-pathspecs",
423
+ "--line-number",
424
+ "--column",
425
+ "--heading",
426
+ "--no-index",
427
+ "--untracked",
428
+ "--no-recursive",
429
+ "--name-rev",
430
+ "--root",
431
+ "--dirty",
432
+ "--broken",
433
+ "--always",
434
+ "--merged",
435
+ "--no-merged",
436
+ "--show-toplevel",
437
+ "--show-cdup",
438
+ "--git-common-dir",
439
+ "--absolute-git-dir",
440
+ "--is-inside-work-tree",
441
+ "--is-inside-git-dir",
442
+ "--is-bare-repository",
443
+ "--symbolic",
444
+ "--symbolic-full-name",
445
+ "--verify",
446
+ "--short-hash",
447
+ "--textual",
448
+ "--batch",
449
+ "--batch-check",
450
+ "--batch-all-objects",
451
+ "--buffer",
452
+ "--follow-symlinks",
453
+ "--use-mailmap"
454
+ ],
455
+ values: [
456
+ "-n",
457
+ "-C",
458
+ "-B",
459
+ "-M",
460
+ "-L",
461
+ "-S",
462
+ "-G",
463
+ "-U",
464
+ "-e",
465
+ "-m",
466
+ "-f",
467
+ "-g",
468
+ "-x",
469
+ "-X",
470
+ "-W",
471
+ "--max-count",
472
+ "--skip",
473
+ "--since",
474
+ "--after",
475
+ "--until",
476
+ "--before",
477
+ "--author",
478
+ "--grep",
479
+ "--grep-reflog",
480
+ "--committer",
481
+ "--date",
482
+ "--diff-filter",
483
+ "--unified",
484
+ "--stat-width",
485
+ "--stat-name-width",
486
+ "--stat-count",
487
+ "--word-diff-regex",
488
+ "--line-prefix",
489
+ "--src-prefix",
490
+ "--dst-prefix",
491
+ "--pickaxe-regex",
492
+ "--anchored",
493
+ "--color-moved",
494
+ "--color-words",
495
+ "--inter-hunk-context",
496
+ "--max-parents",
497
+ "--min-parents",
498
+ "--simplify-by-decoration",
499
+ "--glob",
500
+ "--exclude",
501
+ "--sort",
502
+ "--contains",
503
+ "--points-at",
504
+ "--decorate-refs",
505
+ "--decorate-refs-exclude",
506
+ "--encoding",
507
+ "--context",
508
+ "--max-depth",
509
+ // `--filters` is absent from this list on purpose: it runs the clean and
510
+ // smudge filters the repository under research configures, which is the
511
+ // same execution vector as the configuration flag above with a different
512
+ // spelling.
513
+ "--and",
514
+ "--or",
515
+ "--not",
516
+ "--all-match",
517
+ "--threads",
518
+ "--path"
519
+ ],
520
+ patterns: [COUNT_FLAG]
521
+ },
522
+ wc: {
523
+ cluster: true,
524
+ booleans: [
525
+ "-c",
526
+ "-l",
527
+ "-m",
528
+ "-w",
529
+ "-L",
530
+ "--bytes",
531
+ "--chars",
532
+ "--lines",
533
+ "--words",
534
+ "--max-line-length",
535
+ "--total"
536
+ ]
537
+ },
538
+ du: {
539
+ cluster: true,
540
+ booleans: [
541
+ "-0",
542
+ "-a",
543
+ "-b",
544
+ "-c",
545
+ "-D",
546
+ "-h",
547
+ "-H",
548
+ "-k",
549
+ "-l",
550
+ "-L",
551
+ "-m",
552
+ "-P",
553
+ "-s",
554
+ "-S",
555
+ "-x",
556
+ "--all",
557
+ "--apparent-size",
558
+ "--bytes",
559
+ "--total",
560
+ "--dereference-args",
561
+ "--human-readable",
562
+ "--si",
563
+ "--inodes",
564
+ "--count-links",
565
+ "--dereference",
566
+ "--no-dereference",
567
+ "--separate-dirs",
568
+ "--summarize",
569
+ "--one-file-system",
570
+ "--null"
571
+ ],
572
+ values: [
573
+ "-d",
574
+ "-t",
575
+ "-B",
576
+ "-X",
577
+ "--max-depth",
578
+ "--threshold",
579
+ "--block-size",
580
+ "--exclude",
581
+ "--exclude-from",
582
+ "--time",
583
+ "--time-style",
584
+ "--files0-from"
585
+ ]
586
+ },
587
+ df: {
588
+ cluster: true,
589
+ booleans: [
590
+ "-a",
591
+ "-h",
592
+ "-H",
593
+ "-i",
594
+ "-k",
595
+ "-l",
596
+ "-m",
597
+ "-P",
598
+ "-T",
599
+ "-v",
600
+ "--all",
601
+ "--human-readable",
602
+ "--si",
603
+ "--inodes",
604
+ "--local",
605
+ "--portability",
606
+ "--print-type",
607
+ "--total",
608
+ "--sync",
609
+ "--no-sync"
610
+ ],
611
+ values: ["-B", "-t", "-x", "--block-size", "--type", "--exclude-type"]
612
+ },
613
+ file: {
614
+ cluster: true,
615
+ // `-C`/`--compile` is absent: it writes a compiled magic file.
616
+ booleans: [
617
+ "-b",
618
+ "-c",
619
+ "-h",
620
+ "-i",
621
+ "-k",
622
+ "-L",
623
+ "-n",
624
+ "-N",
625
+ "-p",
626
+ "-r",
627
+ "-s",
628
+ "-S",
629
+ "-v",
630
+ "-z",
631
+ "-0",
632
+ "--brief",
633
+ "--mime",
634
+ "--mime-type",
635
+ "--mime-encoding",
636
+ "--dereference",
637
+ "--no-dereference",
638
+ "--keep-going",
639
+ "--no-buffer",
640
+ "--no-pad",
641
+ "--raw",
642
+ "--special-files",
643
+ "--uncompress",
644
+ "--print0",
645
+ "--checking-printout"
646
+ ],
647
+ values: [
648
+ "-e",
649
+ "-m",
650
+ "-P",
651
+ "-F",
652
+ "--exclude",
653
+ "--magic-file",
654
+ "--separator",
655
+ "--parameter",
656
+ "--extension",
657
+ "--files-from"
658
+ ]
659
+ },
660
+ head: {
661
+ cluster: true,
662
+ booleans: ["-q", "-v", "-z", "--quiet", "--silent", "--verbose", "--zero-terminated"],
663
+ values: ["-c", "-n", "--bytes", "--lines"],
664
+ patterns: [COUNT_FLAG]
665
+ },
666
+ tail: {
667
+ cluster: true,
668
+ booleans: [
669
+ "-f",
670
+ "-F",
671
+ "-q",
672
+ "-v",
673
+ "-z",
674
+ "--follow",
675
+ "--retry",
676
+ "--quiet",
677
+ "--silent",
678
+ "--verbose",
679
+ "--zero-terminated"
680
+ ],
681
+ values: [
682
+ "-c",
683
+ "-n",
684
+ "-s",
685
+ "--bytes",
686
+ "--lines",
687
+ "--sleep-interval",
688
+ "--pid",
689
+ "--max-unchanged-stats"
690
+ ],
691
+ patterns: [COUNT_FLAG]
692
+ },
693
+ cat: {
694
+ cluster: true,
695
+ booleans: [
696
+ "-A",
697
+ "-b",
698
+ "-e",
699
+ "-E",
700
+ "-n",
701
+ "-s",
702
+ "-t",
703
+ "-T",
704
+ "-u",
705
+ "-v",
706
+ "--show-all",
707
+ "--number-nonblank",
708
+ "--show-ends",
709
+ "--number",
710
+ "--squeeze-blank",
711
+ "--show-tabs",
712
+ "--show-nonprinting"
713
+ ]
714
+ },
715
+ sort: {
716
+ cluster: true,
717
+ // Absent on purpose: `-o`/`--output` writes a file with no redirect for the
718
+ // redirect check to see, and `--compress-program` names a program that sort
719
+ // runs over its own temporary files — a permitted binary as an interpreter.
720
+ booleans: [
721
+ "-b",
722
+ "-c",
723
+ "-C",
724
+ "-d",
725
+ "-f",
726
+ "-g",
727
+ "-h",
728
+ "-i",
729
+ "-M",
730
+ "-n",
731
+ "-r",
732
+ "-R",
733
+ "-s",
734
+ "-u",
735
+ "-V",
736
+ "-z",
737
+ "--ignore-leading-blanks",
738
+ "--check",
739
+ "--dictionary-order",
740
+ "--ignore-case",
741
+ "--general-numeric-sort",
742
+ "--human-numeric-sort",
743
+ "--ignore-nonprinting",
744
+ "--month-sort",
745
+ "--numeric-sort",
746
+ "--random-sort",
747
+ "--reverse",
748
+ "--stable",
749
+ "--unique",
750
+ "--version-sort",
751
+ "--zero-terminated",
752
+ "--debug"
753
+ ],
754
+ values: [
755
+ "-k",
756
+ "-t",
757
+ "-S",
758
+ "-T",
759
+ "--key",
760
+ "--field-separator",
761
+ "--buffer-size",
762
+ "--temporary-directory",
763
+ "--random-source",
764
+ "--batch-size",
765
+ "--parallel",
766
+ "--sort"
767
+ ]
768
+ },
769
+ uniq: {
770
+ cluster: true,
771
+ booleans: [
772
+ "-c",
773
+ "-d",
774
+ "-D",
775
+ "-i",
776
+ "-u",
777
+ "-z",
778
+ "--count",
779
+ "--repeated",
780
+ "--all-repeated",
781
+ "--ignore-case",
782
+ "--unique",
783
+ "--zero-terminated"
784
+ ],
785
+ values: ["-f", "-s", "-w", "--skip-fields", "--skip-chars", "--check-chars", "--group"]
786
+ },
787
+ // No flag on either runs a program or writes a file, and the planner's
788
+ // commonest use of `echo` is a section marker — `echo --- files changed ---`
789
+ // is three tokens that all begin with a dash and none of them is a flag.
790
+ echo: { freeform: true },
791
+ printf: { freeform: true },
792
+ date: {
793
+ cluster: true,
794
+ // `-s`/`--set` is absent: it sets the system clock.
795
+ booleans: [
796
+ "-u",
797
+ "-R",
798
+ "-I",
799
+ "--utc",
800
+ "--universal",
801
+ "--rfc-email",
802
+ "--rfc-3339",
803
+ "--iso-8601",
804
+ "--debug"
805
+ ],
806
+ values: ["-d", "-f", "-r", "--date", "--file", "--reference"]
807
+ },
808
+ stat: {
809
+ cluster: true,
810
+ booleans: ["-f", "-L", "-t", "--file-system", "--terse", "--dereference"],
811
+ values: ["-c", "--format", "--printf", "--cached"]
812
+ },
813
+ basename: {
814
+ cluster: true,
815
+ booleans: ["-a", "-z", "--multiple", "--zero"],
816
+ values: ["-s", "--suffix"]
817
+ },
818
+ dirname: {
819
+ cluster: true,
820
+ booleans: ["-z", "--zero"]
821
+ },
822
+ realpath: {
823
+ cluster: true,
824
+ booleans: [
825
+ "-e",
826
+ "-m",
827
+ "-L",
828
+ "-P",
829
+ "-q",
830
+ "-s",
831
+ "-z",
832
+ "--canonicalize-existing",
833
+ "--canonicalize-missing",
834
+ "--logical",
835
+ "--physical",
836
+ "--quiet",
837
+ "--strip",
838
+ "--no-symlinks",
839
+ "--zero"
840
+ ],
841
+ values: ["--relative-to", "--relative-base"]
842
+ },
843
+ pwd: { cluster: true, booleans: ["-L", "-P", "--logical", "--physical"] },
844
+ which: { cluster: true, booleans: ["-a", "-s", "--all"] },
845
+ type: { cluster: true, booleans: ["-a", "-f", "-p", "-P", "-t"] },
846
+ uname: {
847
+ cluster: true,
848
+ booleans: [
849
+ "-a",
850
+ "-i",
851
+ "-m",
852
+ "-n",
853
+ "-o",
854
+ "-p",
855
+ "-r",
856
+ "-s",
857
+ "-v",
858
+ "--all",
859
+ "--kernel-name",
860
+ "--nodename",
861
+ "--kernel-release",
862
+ "--kernel-version",
863
+ "--machine",
864
+ "--processor",
865
+ "--hardware-platform",
866
+ "--operating-system"
867
+ ]
868
+ },
869
+ whoami: {},
870
+ cut: {
871
+ cluster: true,
872
+ // `--output-delimiter` names a separator string, not a file.
873
+ booleans: ["-n", "-s", "-z", "--only-delimited", "--complement", "--zero-terminated"],
874
+ values: [
875
+ "-b",
876
+ "-c",
877
+ "-d",
878
+ "-f",
879
+ "--bytes",
880
+ "--characters",
881
+ "--delimiter",
882
+ "--fields",
883
+ "--output-delimiter"
884
+ ]
885
+ },
886
+ nl: {
887
+ cluster: true,
888
+ booleans: ["-p", "--no-renumber"],
889
+ values: [
890
+ "-b",
891
+ "-d",
892
+ "-f",
893
+ "-h",
894
+ "-i",
895
+ "-l",
896
+ "-n",
897
+ "-s",
898
+ "-v",
899
+ "-w",
900
+ "--body-numbering",
901
+ "--section-delimiter",
902
+ "--footer-numbering",
903
+ "--header-numbering",
904
+ "--join-blank-lines",
905
+ "--line-increment",
906
+ "--number-format",
907
+ "--number-separator",
908
+ "--starting-line-number",
909
+ "--number-width"
910
+ ]
911
+ },
912
+ rg: {
913
+ cluster: true,
914
+ // `--pre` runs a named preprocessor over every file searched and
915
+ // `--hostname-bin` runs a named program to resolve the hostname; both are
916
+ // absent, and `--pre-glob` with them.
917
+ booleans: [
918
+ "-a",
919
+ "-c",
920
+ "-F",
921
+ "-h",
922
+ "-H",
923
+ "-i",
924
+ "-I",
925
+ "-l",
926
+ "-L",
927
+ "-n",
928
+ "-N",
929
+ "-o",
930
+ "-p",
931
+ "-P",
932
+ "-q",
933
+ "-s",
934
+ "-S",
935
+ "-u",
936
+ "-uu",
937
+ "-uuu",
938
+ "-U",
939
+ "-v",
940
+ "-V",
941
+ "-w",
942
+ "-x",
943
+ "-z",
944
+ "--text",
945
+ "--count",
946
+ "--count-matches",
947
+ "--fixed-strings",
948
+ "--files",
949
+ "--files-with-matches",
950
+ "--files-without-match",
951
+ "--follow",
952
+ "--hidden",
953
+ "--no-hidden",
954
+ "--ignore-case",
955
+ "--case-sensitive",
956
+ "--smart-case",
957
+ "--invert-match",
958
+ "--line-number",
959
+ "--no-line-number",
960
+ "--with-filename",
961
+ "--no-filename",
962
+ "--heading",
963
+ "--no-heading",
964
+ "--column",
965
+ "--no-column",
966
+ "--byte-offset",
967
+ "--vimgrep",
968
+ "--json",
969
+ "--stats",
970
+ "--trim",
971
+ "--pretty",
972
+ "--passthru",
973
+ "--word-regexp",
974
+ "--line-regexp",
975
+ "--multiline",
976
+ "--multiline-dotall",
977
+ "--no-multiline",
978
+ "--crlf",
979
+ "--null",
980
+ "--null-data",
981
+ "--binary",
982
+ "--search-zip",
983
+ "--no-ignore",
984
+ "--no-ignore-dot",
985
+ "--no-ignore-exclude",
986
+ "--no-ignore-files",
987
+ "--no-ignore-global",
988
+ "--no-ignore-parent",
989
+ "--no-ignore-vcs",
990
+ "--no-ignore-messages",
991
+ "--no-messages",
992
+ "--no-require-git",
993
+ "--require-git",
994
+ "--one-file-system",
995
+ "--pcre2",
996
+ "--no-pcre2",
997
+ "--auto-hybrid-regex",
998
+ "--line-buffered",
999
+ "--block-buffered",
1000
+ "--unicode",
1001
+ "--no-unicode",
1002
+ "--unrestricted",
1003
+ "--type-list",
1004
+ "--debug",
1005
+ "--trace",
1006
+ "--include-zero",
1007
+ "--no-context-separator",
1008
+ "--stop-on-nonmatch",
1009
+ "--quiet",
1010
+ "--no-binary"
1011
+ ],
1012
+ values: [
1013
+ "-A",
1014
+ "-B",
1015
+ "-C",
1016
+ "-d",
1017
+ "-e",
1018
+ "-E",
1019
+ "-f",
1020
+ "-g",
1021
+ "-j",
1022
+ "-m",
1023
+ "-M",
1024
+ "-r",
1025
+ "-t",
1026
+ "-T",
1027
+ "--after-context",
1028
+ "--before-context",
1029
+ "--context",
1030
+ "--context-separator",
1031
+ "--field-context-separator",
1032
+ "--field-match-separator",
1033
+ "--regexp",
1034
+ "--file",
1035
+ "--glob",
1036
+ "--iglob",
1037
+ "--glob-case-insensitive",
1038
+ "--type",
1039
+ "--type-not",
1040
+ "--type-add",
1041
+ "--type-clear",
1042
+ "--max-count",
1043
+ "--max-columns",
1044
+ "--max-depth",
1045
+ "--maxdepth",
1046
+ "--max-filesize",
1047
+ "--replace",
1048
+ "--threads",
1049
+ "--encoding",
1050
+ "--engine",
1051
+ "--sort",
1052
+ "--sortr",
1053
+ "--color",
1054
+ "--colors",
1055
+ "--path-separator",
1056
+ "--ignore-file",
1057
+ "--dfa-size-limit",
1058
+ "--regex-size-limit",
1059
+ "--binary-files",
1060
+ "--max-columns-preview"
1061
+ ]
1062
+ },
1063
+ grep: {
1064
+ cluster: true,
1065
+ // `-o` here is `--only-matching`, which writes nothing. It is the same two
1066
+ // characters as `sort -o`, which writes a file — the reason these sets are
1067
+ // per-binary rather than one shared list.
1068
+ booleans: [
1069
+ "-a",
1070
+ "-b",
1071
+ "-c",
1072
+ "-E",
1073
+ "-F",
1074
+ "-G",
1075
+ "-H",
1076
+ "-h",
1077
+ "-i",
1078
+ "-I",
1079
+ "-l",
1080
+ "-L",
1081
+ "-n",
1082
+ "-o",
1083
+ "-P",
1084
+ "-q",
1085
+ "-r",
1086
+ "-R",
1087
+ "-s",
1088
+ "-T",
1089
+ "-U",
1090
+ "-v",
1091
+ "-w",
1092
+ "-x",
1093
+ "-y",
1094
+ "-z",
1095
+ "-Z",
1096
+ "--extended-regexp",
1097
+ "--fixed-strings",
1098
+ "--basic-regexp",
1099
+ "--perl-regexp",
1100
+ "--ignore-case",
1101
+ "--no-ignore-case",
1102
+ "--invert-match",
1103
+ "--word-regexp",
1104
+ "--line-regexp",
1105
+ "--count",
1106
+ "--files-with-matches",
1107
+ "--files-without-match",
1108
+ "--line-number",
1109
+ "--with-filename",
1110
+ "--no-filename",
1111
+ "--only-matching",
1112
+ "--quiet",
1113
+ "--silent",
1114
+ "--no-messages",
1115
+ "--text",
1116
+ "--binary",
1117
+ "--line-buffered",
1118
+ "--null",
1119
+ "--null-data",
1120
+ "--byte-offset",
1121
+ "--recursive",
1122
+ "--dereference-recursive",
1123
+ "--initial-tab",
1124
+ "--unix-byte-offsets",
1125
+ "--no-group-separator",
1126
+ "--color",
1127
+ "--colour"
1128
+ ],
1129
+ values: [
1130
+ "-A",
1131
+ "-B",
1132
+ "-C",
1133
+ "-d",
1134
+ "-D",
1135
+ "-e",
1136
+ "-f",
1137
+ "-m",
1138
+ "--after-context",
1139
+ "--before-context",
1140
+ "--context",
1141
+ "--regexp",
1142
+ "--file",
1143
+ "--max-count",
1144
+ "--devices",
1145
+ "--directories",
1146
+ "--binary-files",
1147
+ "--include",
1148
+ "--exclude",
1149
+ "--exclude-dir",
1150
+ "--exclude-from",
1151
+ "--label",
1152
+ "--group-separator"
1153
+ ]
1154
+ },
1155
+ find: {
1156
+ // No clustering: find spells its flags as single-dash words, so walking the
1157
+ // characters of `-name` would read it as five short flags. And its arguments
1158
+ // are an expression, so `--` does not end them.
1159
+ expressionArgs: true,
1160
+ // Absent on purpose: `-exec`, `-execdir`, `-ok` and `-okdir` run an inner
1161
+ // command (also caught by the named guard below, which says so more
1162
+ // precisely), `-delete` removes files, and `-fprint`, `-fprint0`, `-fprintf`
1163
+ // and `-fls` are find's write family — the same write as `> file` with no
1164
+ // redirect for the redirect check to see.
1165
+ booleans: [
1166
+ "-H",
1167
+ "-L",
1168
+ "-P",
1169
+ "-a",
1170
+ "-and",
1171
+ "-o",
1172
+ "-or",
1173
+ "-not",
1174
+ "-d",
1175
+ "-depth",
1176
+ "-daystart",
1177
+ "-empty",
1178
+ "-executable",
1179
+ "-false",
1180
+ "-follow",
1181
+ "-help",
1182
+ "--help",
1183
+ "-ignore_readdir_race",
1184
+ "-noignore_readdir_race",
1185
+ "-ls",
1186
+ "-mount",
1187
+ "-nogroup",
1188
+ "-noleaf",
1189
+ "-nouser",
1190
+ "-print",
1191
+ "-print0",
1192
+ "-prune",
1193
+ "-quit",
1194
+ "-readable",
1195
+ "-true",
1196
+ "-version",
1197
+ "-writable",
1198
+ "-xdev"
1199
+ ],
1200
+ values: [
1201
+ "-maxdepth",
1202
+ "-mindepth",
1203
+ "-name",
1204
+ "-iname",
1205
+ "-path",
1206
+ "-ipath",
1207
+ "-wholename",
1208
+ "-iwholename",
1209
+ "-regex",
1210
+ "-iregex",
1211
+ "-lname",
1212
+ "-ilname",
1213
+ "-type",
1214
+ "-xtype",
1215
+ "-size",
1216
+ "-newer",
1217
+ "-anewer",
1218
+ "-cnewer",
1219
+ "-mtime",
1220
+ "-mmin",
1221
+ "-ctime",
1222
+ "-cmin",
1223
+ "-atime",
1224
+ "-amin",
1225
+ "-used",
1226
+ "-uid",
1227
+ "-gid",
1228
+ "-user",
1229
+ "-group",
1230
+ "-perm",
1231
+ "-links",
1232
+ "-inum",
1233
+ "-samefile",
1234
+ "-fstype",
1235
+ "-regextype",
1236
+ "-files0-from",
1237
+ "-printf",
1238
+ "-context",
1239
+ "-D"
1240
+ ],
1241
+ // `find -O2` glues the optimisation level on, and `-newermt`/`-newerat` and
1242
+ // friends spell the two comparison fields into the flag name.
1243
+ patterns: [/^-O\d$/, /^-newer[acmBt][acmBt]$/]
1244
+ },
1245
+ jq: {
1246
+ cluster: true,
1247
+ booleans: [
1248
+ "-a",
1249
+ "-c",
1250
+ "-C",
1251
+ "-e",
1252
+ "-h",
1253
+ "-j",
1254
+ "-M",
1255
+ "-n",
1256
+ "-r",
1257
+ "-R",
1258
+ "-s",
1259
+ "-S",
1260
+ "--ascii-output",
1261
+ "--compact-output",
1262
+ "--color-output",
1263
+ "--monochrome-output",
1264
+ "--exit-status",
1265
+ "--join-output",
1266
+ "--null-input",
1267
+ "--raw-input",
1268
+ "--raw-output",
1269
+ "--raw-output0",
1270
+ "--slurp",
1271
+ "--sort-keys",
1272
+ "--seq",
1273
+ "--stream",
1274
+ "--stream-errors",
1275
+ "--tab",
1276
+ "--unbuffered",
1277
+ "--args",
1278
+ "--jsonargs"
1279
+ ],
1280
+ values: [
1281
+ "-f",
1282
+ "-L",
1283
+ "--arg",
1284
+ "--argjson",
1285
+ "--slurpfile",
1286
+ "--rawfile",
1287
+ "--indent",
1288
+ "--from-file"
1289
+ ]
1290
+ },
1291
+ yq: {
1292
+ cluster: true,
1293
+ // `-i`/`--inplace`/`--in-place` rewrite the file, and `-s`/`--split-exp`
1294
+ // writes one file per document. All absent — and all bare or near-bare
1295
+ // booleans, which is why exempting booleans was not an option.
1296
+ booleans: [
1297
+ "-C",
1298
+ "-e",
1299
+ "-h",
1300
+ "-j",
1301
+ "-M",
1302
+ "-n",
1303
+ "-N",
1304
+ "-P",
1305
+ "-r",
1306
+ "-S",
1307
+ "-v",
1308
+ "-y",
1309
+ "-Y",
1310
+ "--colors",
1311
+ "--no-colors",
1312
+ "--exit-status",
1313
+ "--no-doc",
1314
+ "--null-input",
1315
+ "--prettyPrint",
1316
+ "--raw-output",
1317
+ "--sort-keys",
1318
+ "--tojson",
1319
+ "--yaml-output",
1320
+ "--yaml-roundtrip",
1321
+ "--unwrapScalar",
1322
+ "--header-preprocess",
1323
+ "--no-exit-status",
1324
+ "--verbose",
1325
+ "--xml-strict-mode"
1326
+ ],
1327
+ values: [
1328
+ "-I",
1329
+ "-o",
1330
+ "-p",
1331
+ "-L",
1332
+ "--indent",
1333
+ "--output-format",
1334
+ "--input-format",
1335
+ "--expression",
1336
+ "--width",
1337
+ "--xml-attribute-prefix",
1338
+ "--xml-content-name"
1339
+ ]
1340
+ }
1341
+ };
122
1342
  var GIT_READONLY_SUBCOMMANDS = [
123
1343
  "log",
124
1344
  "status",
125
1345
  "diff",
126
1346
  "show",
127
1347
  "ls-files",
128
- "ls-remote",
129
1348
  "ls-tree",
130
1349
  "branch",
131
1350
  "tag",
@@ -240,6 +1459,26 @@ var REFUSED_SUBCOMMANDS = {
240
1459
  az: ["login", "logout"],
241
1460
  terraform: ["apply", "destroy"]
242
1461
  };
1462
+ var SHELL_KEYWORDS = [
1463
+ "{",
1464
+ "}",
1465
+ "if",
1466
+ "then",
1467
+ "elif",
1468
+ "else",
1469
+ "fi",
1470
+ "for",
1471
+ "while",
1472
+ "until",
1473
+ "do",
1474
+ "done",
1475
+ "case",
1476
+ "esac",
1477
+ "select",
1478
+ "function",
1479
+ "time",
1480
+ "export"
1481
+ ];
243
1482
  var INTERPRETERS = [
244
1483
  "sh",
245
1484
  "bash",
@@ -309,6 +1548,52 @@ var MULTIPLEXERS = [
309
1548
  "flutter",
310
1549
  "dart"
311
1550
  ];
1551
+ var HELP_FLAGS = ["--help", "--version"];
1552
+ var WRAPPER_FAMILY = {
1553
+ env: {
1554
+ valueFlags: ["-u", "--unset", "-C", "--chdir"],
1555
+ booleanFlags: [
1556
+ "-i",
1557
+ "--ignore-environment",
1558
+ "-0",
1559
+ "--null",
1560
+ "-v",
1561
+ "--debug",
1562
+ // Optional-value flags: see WrapperSpec.booleanFlags.
1563
+ "--block-signal",
1564
+ "--default-signal",
1565
+ "--ignore-signal",
1566
+ "--list-signal-handling",
1567
+ ...HELP_FLAGS
1568
+ ],
1569
+ stringFlags: ["-S", "--split-string"],
1570
+ acceptsAssignments: true
1571
+ },
1572
+ nice: {
1573
+ valueFlags: ["-n", "--adjustment"],
1574
+ booleanFlags: HELP_FLAGS,
1575
+ booleanPattern: /^-\d+$/
1576
+ },
1577
+ timeout: {
1578
+ valueFlags: ["-s", "--signal", "-k", "--kill-after"],
1579
+ booleanFlags: ["--preserve-status", "--foreground", "-v", "--verbose", ...HELP_FLAGS],
1580
+ positionals: 1
1581
+ },
1582
+ nohup: { booleanFlags: HELP_FLAGS },
1583
+ setsid: { booleanFlags: ["-c", "--ctty", "-f", "--fork", "-w", "--wait", "-h", "-V", ...HELP_FLAGS] },
1584
+ stdbuf: {
1585
+ valueFlags: ["-i", "--input", "-o", "--output", "-e", "--error"],
1586
+ booleanFlags: HELP_FLAGS
1587
+ },
1588
+ ionice: {
1589
+ valueFlags: ["-c", "--class", "-n", "--classdata", "-p", "--pid", "-P", "--pgid", "-u", "--uid"],
1590
+ booleanFlags: ["-t", "--ignore", "-h", "-V", ...HELP_FLAGS]
1591
+ },
1592
+ // The multi-call binary ships its own `rm`, `mv` and `sh`, so the refused
1593
+ // name arrives as its first argument.
1594
+ busybox: { booleanFlags: ["--list", "--install", ...HELP_FLAGS] },
1595
+ command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] }
1596
+ };
312
1597
  var POSIX_DIALECT = {
313
1598
  escape: "\\",
314
1599
  escapeInQuotes: true,
@@ -523,10 +1808,92 @@ function binaryName(token, dialect) {
523
1808
  if (dot <= 0) return base;
524
1809
  return dialect.strippedExtensions.includes(base.slice(dot).toLowerCase()) ? base.slice(0, dot) : base;
525
1810
  }
1811
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
526
1812
  function toSegment(tokens, piped, dialect) {
527
1813
  const rest = [...tokens];
528
- while (rest.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(rest[0])) rest.shift();
529
- return { binary: rest.length > 0 ? binaryName(rest[0], dialect) : "", args: rest.slice(1), piped, expandable: false };
1814
+ const assignments = [];
1815
+ while (rest.length > 0 && ASSIGNMENT.test(rest[0])) assignments.push(rest.shift());
1816
+ return {
1817
+ binary: rest.length > 0 ? binaryName(rest[0], dialect) : "",
1818
+ args: rest.slice(1),
1819
+ assignments,
1820
+ piped,
1821
+ expandable: false
1822
+ };
1823
+ }
1824
+ function joinedShortFlag(list, token) {
1825
+ if (token.length <= 2) return void 0;
1826
+ return list.find((f) => f.length === 2 && token.startsWith(f));
1827
+ }
1828
+ function scanWrapperArgs(binary, spec, args) {
1829
+ const valueFlags = spec.valueFlags ?? [];
1830
+ const stringFlags = spec.stringFlags ?? [];
1831
+ const assignments = [];
1832
+ let positionals = spec.positionals ?? 0;
1833
+ let i = 0;
1834
+ while (i < args.length) {
1835
+ const token = args[i];
1836
+ if (token === "--") {
1837
+ i++;
1838
+ break;
1839
+ }
1840
+ if (/^-./.test(token)) {
1841
+ const eq = token.startsWith("--") ? token.indexOf("=") : -1;
1842
+ const name = eq > 0 ? token.slice(0, eq) : token;
1843
+ const selfContained = eq > 0;
1844
+ if ((spec.noExecFlags ?? []).includes(name)) return { kind: "no-exec" };
1845
+ if (stringFlags.includes(name) || joinedShortFlag(stringFlags, token)) {
1846
+ return {
1847
+ kind: "refuse",
1848
+ reason: `"${binary} ${name}" splits a string into a command, which this classifier cannot inspect. Use the read-only research tools, or describe it as a task.`
1849
+ };
1850
+ }
1851
+ if ((spec.booleanFlags ?? []).includes(name) || spec.booleanPattern?.test(token)) {
1852
+ i++;
1853
+ continue;
1854
+ }
1855
+ if (valueFlags.includes(name)) {
1856
+ i += selfContained ? 1 : 2;
1857
+ continue;
1858
+ }
1859
+ if (joinedShortFlag(valueFlags, token)) {
1860
+ i++;
1861
+ continue;
1862
+ }
1863
+ return {
1864
+ kind: "refuse",
1865
+ reason: `"${token}" is not a flag this classifier knows on the wrapper "${binary}", so it cannot tell which command "${binary}" would actually run. Re-run without it.`
1866
+ };
1867
+ }
1868
+ if (spec.acceptsAssignments && ASSIGNMENT.test(token)) {
1869
+ assignments.push(token);
1870
+ i++;
1871
+ continue;
1872
+ }
1873
+ if (positionals > 0) {
1874
+ positionals--;
1875
+ i++;
1876
+ continue;
1877
+ }
1878
+ break;
1879
+ }
1880
+ return { kind: "command", tokens: args.slice(i), assignments };
1881
+ }
1882
+ function unwrap(seg, dialect) {
1883
+ const wrappers = [];
1884
+ const assignments = [...seg.assignments];
1885
+ let current = seg;
1886
+ while (WRAPPER_FAMILY[current.binary]) {
1887
+ const scan = scanWrapperArgs(current.binary, WRAPPER_FAMILY[current.binary], current.args);
1888
+ if (scan.kind === "refuse") return { seg: { ...current, assignments }, wrappers, reason: scan.reason };
1889
+ if (scan.kind === "no-exec" || scan.tokens.length === 0) break;
1890
+ assignments.push(...scan.assignments);
1891
+ wrappers.push(current.binary);
1892
+ const inner = toSegment(scan.tokens, current.piped, dialect);
1893
+ assignments.push(...inner.assignments);
1894
+ current = { ...inner, assignments: [], expandable: current.expandable };
1895
+ }
1896
+ return { seg: { ...current, assignments }, wrappers };
530
1897
  }
531
1898
  function lexAll(command, dialect) {
532
1899
  const queue = [];
@@ -555,16 +1922,79 @@ function pathLikeArgs(command, opts = {}) {
555
1922
  return !a.startsWith("-") && looksLikePath(a) ? [a] : [];
556
1923
  }));
557
1924
  }
1925
+ function matchFlag(spec, booleans, values, token) {
1926
+ if (token.startsWith("--")) {
1927
+ const eq = token.indexOf("=");
1928
+ const name = eq > 0 ? token.slice(0, eq) : token;
1929
+ if (booleans.has(name)) return "boolean";
1930
+ if (values.has(name)) return eq > 0 ? "boolean" : "value";
1931
+ return void 0;
1932
+ }
1933
+ if (booleans.has(token)) return "boolean";
1934
+ if (values.has(token)) return "value";
1935
+ if (spec.patterns?.some((p) => p.test(token))) return "boolean";
1936
+ if (!spec.cluster) return void 0;
1937
+ for (let i = 1; i < token.length; i++) {
1938
+ const short = `-${token[i]}`;
1939
+ if (booleans.has(short)) continue;
1940
+ if (values.has(short)) return i === token.length - 1 ? "value" : "boolean";
1941
+ return void 0;
1942
+ }
1943
+ return "boolean";
1944
+ }
1945
+ function takesNextToken(next) {
1946
+ if (next === void 0) return false;
1947
+ return !/^-./.test(next) || /^-\d/.test(next);
1948
+ }
1949
+ function flagLabel(token) {
1950
+ const eq = token.startsWith("--") ? token.indexOf("=") : -1;
1951
+ return eq > 0 ? token.slice(0, eq) : token;
1952
+ }
1953
+ function scanFlags(seg) {
1954
+ const spec = FLAG_POLICY[seg.binary];
1955
+ if (!spec || spec.freeform) return void 0;
1956
+ const booleans = /* @__PURE__ */ new Set([...spec.booleans ?? [], ...UNIVERSAL_FLAGS]);
1957
+ const values = new Set(spec.values ?? []);
1958
+ const operands = [];
1959
+ let i = 0;
1960
+ while (i < seg.args.length) {
1961
+ const token = seg.args[i];
1962
+ if (token === "--" && !spec.expressionArgs) {
1963
+ operands.push(...seg.args.slice(i + 1));
1964
+ break;
1965
+ }
1966
+ if (!/^-./.test(token)) {
1967
+ operands.push(token);
1968
+ i++;
1969
+ continue;
1970
+ }
1971
+ const shape = matchFlag(spec, booleans, values, token);
1972
+ if (shape === void 0) return { unknown: flagLabel(token), operands };
1973
+ i += shape === "value" && takesNextToken(seg.args[i + 1]) ? 2 : 1;
1974
+ }
1975
+ return { operands };
1976
+ }
1977
+ function subcommandOf(seg) {
1978
+ const scan = scanFlags(seg);
1979
+ if (scan) return scan.operands[0];
1980
+ return seg.args.find((a) => !a.startsWith("-"));
1981
+ }
1982
+ var SCOPE_LEAD_ARGS = 2;
558
1983
  function scopeFor(seg) {
559
1984
  if (!MULTIPLEXERS.includes(seg.binary)) return seg.binary;
560
- const sub = seg.args.find((a) => !a.startsWith("-"));
561
- return sub ? `${seg.binary} ${sub}` : seg.binary;
1985
+ const lead = [];
1986
+ for (const arg of seg.args) {
1987
+ if (arg.startsWith("-")) break;
1988
+ lead.push(arg);
1989
+ if (lead.length === SCOPE_LEAD_ARGS) break;
1990
+ }
1991
+ return [seg.binary, ...lead].join(" ");
562
1992
  }
563
1993
  function isAuto(seg) {
564
1994
  if (!AUTO_COMMANDS.includes(seg.binary)) return false;
565
1995
  if (seg.expandable) return false;
566
1996
  if (seg.binary !== "git") return true;
567
- const sub = seg.args.find((a) => !a.startsWith("-"));
1997
+ const sub = subcommandOf(seg);
568
1998
  return sub !== void 0 && GIT_READONLY_SUBCOMMANDS.includes(sub);
569
1999
  }
570
2000
  function refusalFor(seg) {
@@ -574,20 +2004,32 @@ function refusalFor(seg) {
574
2004
  if (seg.binary === "eval" || seg.binary === "exec") {
575
2005
  return `"${seg.binary}" runs a string as a command, which this classifier cannot inspect. Use the read-only research tools, or describe it as a task.`;
576
2006
  }
2007
+ if (seg.binary === "source" || seg.binary === ".") {
2008
+ return `"${seg.binary}" runs a file's contents as commands, which this classifier cannot inspect. Use the read-only research tools, or describe it as a task.`;
2009
+ }
2010
+ if (SHELL_KEYWORDS.includes(seg.binary)) {
2011
+ return `"${seg.binary}" is a shell keyword or compound-command opener. This classifier only inspects the command that runs first, and a keyword hides the real one from it. Run the inner command directly, or describe it as a task.`;
2012
+ }
577
2013
  const subs = REFUSED_SUBCOMMANDS[seg.binary];
578
2014
  if (subs) {
579
- const sub = seg.args.find((a) => !a.startsWith("-"));
2015
+ const sub = subcommandOf(seg);
580
2016
  if (sub && subs.includes(sub)) {
581
2017
  return `"${seg.binary} ${sub}" changes state or reaches outward. You are a read-only planner \u2014 put it in the plan and let the runner do it.`;
582
2018
  }
583
2019
  }
584
2020
  if (seg.binary === "git") {
585
- const sub = seg.args.find((a) => !a.startsWith("-"));
2021
+ const sub = subcommandOf(seg);
586
2022
  if (sub === "branch" || sub === "tag") {
587
2023
  const flag = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a));
588
2024
  if (flag) {
589
2025
  return `"git ${sub} ${flag}" mutates refs. You are a read-only planner \u2014 describe the change as a task instead.`;
590
2026
  }
2027
+ const listing = seg.args.includes("-l") || seg.args.includes("--list");
2028
+ const scan2 = scanFlags(seg);
2029
+ const target = scan2?.operands[1];
2030
+ if (!listing && target) {
2031
+ return `"git ${sub} ${target}" creates or moves a ref. You are a read-only planner \u2014 describe the change as a task instead.`;
2032
+ }
591
2033
  }
592
2034
  }
593
2035
  if (seg.binary === "find") {
@@ -611,8 +2053,24 @@ function refusalFor(seg) {
611
2053
  return `Inline code via "${seg.binary} ${seg.args.find(isInlineCodeFlag)}" is not available to the planner. Use the read-only research tools, or describe it as a task.`;
612
2054
  }
613
2055
  }
2056
+ if (seg.assignments.length > 0) {
2057
+ return `The environment assignment "${seg.assignments[0]}" decides what "${seg.binary}" actually does, and this classifier cannot judge the value. Re-run the command without the assignment.`;
2058
+ }
2059
+ const scan = scanFlags(seg);
2060
+ if (scan?.unknown) {
2061
+ return `"${scan.unknown}" is not a flag this classifier knows to be read-only on "${seg.binary}", and flags on otherwise read-only binaries can run a helper program or write a file. Re-run with read-only flags only, or describe the work as a task.`;
2062
+ }
2063
+ if (seg.binary === "uniq" && scan && scan.operands.length > 1) {
2064
+ return `"uniq" writes to its second file argument ("${scan.operands[1]}"). You are a read-only planner \u2014 pipe the output instead of naming an output file, or describe the write as a task.`;
2065
+ }
614
2066
  return void 0;
615
2067
  }
2068
+ function withWrapperNote(reason, wrappers) {
2069
+ if (wrappers.length === 0) return reason;
2070
+ const quoted = wrappers.map((w) => `"${w}"`);
2071
+ const names = quoted.length > 1 ? `${quoted.slice(0, -1).join(", ")} and ${quoted[quoted.length - 1]}` : quoted[0];
2072
+ return `${reason} Wrapping it in ${names} does not change what runs.`;
2073
+ }
616
2074
  function classifyCommand(command, opts = {}) {
617
2075
  const trimmed = command.trim();
618
2076
  if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
@@ -641,11 +2099,13 @@ function classifyCommand(command, opts = {}) {
641
2099
  };
642
2100
  }
643
2101
  if (segments.length === 0) return { tier: "refuse", scope: "", reason: "No command found." };
644
- for (const seg of segments) {
2102
+ const unwrapped = segments.map((seg) => unwrap(seg, dialect));
2103
+ for (const { seg, wrappers, reason: wrapperReason } of unwrapped) {
2104
+ if (wrapperReason) return { tier: "refuse", scope: "", reason: wrapperReason };
645
2105
  const reason = refusalFor(seg);
646
- if (reason) return { tier: "refuse", scope: "", reason };
2106
+ if (reason) return { tier: "refuse", scope: "", reason: withWrapperNote(reason, wrappers) };
647
2107
  }
648
- const nonAuto = segments.filter((s) => !isAuto(s));
2108
+ const nonAuto = unwrapped.map((u) => u.seg).filter((s) => !isAuto(s));
649
2109
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
650
2110
  const scope = [...new Set(nonAuto.map(scopeFor))].sort().join(" + ");
651
2111
  return { tier: "ask", scope };
@@ -989,6 +2449,139 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
989
2449
  }
990
2450
  };
991
2451
 
2452
+ // src/utils/redactSecrets.ts
2453
+ var REDACTION_MARKER = "REDACTED";
2454
+ function marker(kind, length) {
2455
+ return `[${REDACTION_MARKER} ${kind}: ${length} chars]`;
2456
+ }
2457
+ var LINE_PREFIX = String.raw`(?:[ \t]*\d+\||[^\s:]*:\d+:|)[ \t]*`;
2458
+ var LINE_PREFIX_ANCHORED = new RegExp("^" + LINE_PREFIX);
2459
+ var CREDENTIAL_WORD = String.raw`(?:api[_-]?keys?|secrets?|passwords?|passwd|passphrase|tokens?|access[_-]?keys?|private[_-]?keys?|client[_-]?secrets?|credentials?|auth)(?![a-z])`;
2460
+ var NAMED_CREDENTIAL = new RegExp(
2461
+ String.raw`((?:^|[^A-Za-z0-9_])["']?[A-Za-z0-9_.\-]*` + CREDENTIAL_WORD + String.raw`[A-Za-z0-9_.\-]*["']?[ \t]*(?:=>|[:=])[ \t]*)(["'\x60]?)([A-Za-z0-9_\-./+=:~!@#%^&*]{8,})\2`,
2462
+ "gi"
2463
+ );
2464
+ var DOTENV_CREDENTIAL = new RegExp(
2465
+ String.raw`^(` + LINE_PREFIX + String.raw`(?:export[ \t]+)?[A-Z0-9_]*` + CREDENTIAL_WORD.toUpperCase() + String.raw`[A-Z0-9_]*[ \t]*=[ \t]*)(["'\x60]?)([^\s"'\x60]{3,})\2[ \t]*$`,
2466
+ "gm"
2467
+ );
2468
+ var PROVIDER_KEY_PATTERNS = [
2469
+ // OpenAI / Anthropic / OpenRouter and the many `sk-` derivatives
2470
+ /(?<![A-Za-z0-9])sk-(?:ant-|proj-|or-v1-|live-|test-)?[A-Za-z0-9_-]{20,}/g,
2471
+ // GitHub personal-access, OAuth, app and refresh tokens
2472
+ /(?<![A-Za-z0-9])(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{22,}/g,
2473
+ // Slack bot / user / app tokens
2474
+ /(?<![A-Za-z0-9])xox[abprse]-[A-Za-z0-9-]{10,}/g,
2475
+ // Stripe secret and restricted keys
2476
+ /(?<![A-Za-z0-9])[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g,
2477
+ // Google API keys and OAuth access tokens
2478
+ /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{30,}/g,
2479
+ /(?<![A-Za-z0-9])ya29\.[A-Za-z0-9_-]{20,}/g,
2480
+ // npm, Hugging Face, Groq, GitLab, DigitalOcean, Shopify
2481
+ /(?<![A-Za-z0-9])(?:npm|hf|gsk|glpat|dop_v1|shpat|shpss)[_-][A-Za-z0-9]{20,}/g,
2482
+ /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}/g,
2483
+ // Three-segment JWTs, whose payload segment is base64 for `{"`
2484
+ /(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g
2485
+ ];
2486
+ var CLOUD_ACCESS_KEY_ID = /(?<![A-Za-z0-9])(?:A3T[A-Z0-9]{2}|AKIA|ASIA|ABIA|ACCA|AGPA|AIDA|AIPA|ANPA|ANVA|AROA)[A-Z0-9]{16}(?![A-Za-z0-9])/g;
2487
+ var PEM_BEGIN = /-----BEGIN ((?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?)-----/;
2488
+ var PEM_BODY_LINE = /^[A-Za-z0-9+/=:,.\- \t]*$/;
2489
+ function redactPrivateKeyBlocks(text) {
2490
+ if (!PEM_BEGIN.test(text)) return text;
2491
+ const out = [];
2492
+ let label = null;
2493
+ let bodyChars = 0;
2494
+ const closeBlock = () => {
2495
+ if (label === null) return;
2496
+ out.push(marker("private key", bodyChars));
2497
+ label = null;
2498
+ bodyChars = 0;
2499
+ };
2500
+ for (const line of text.split("\n")) {
2501
+ if (label === null) {
2502
+ out.push(line);
2503
+ const begun = PEM_BEGIN.exec(line);
2504
+ if (begun) label = begun[1];
2505
+ continue;
2506
+ }
2507
+ const content = line.replace(LINE_PREFIX_ANCHORED, "");
2508
+ if (content.includes(`-----END ${label}-----`)) {
2509
+ closeBlock();
2510
+ out.push(line);
2511
+ } else if (PEM_BODY_LINE.test(content)) {
2512
+ bodyChars += content.trim().length;
2513
+ } else {
2514
+ closeBlock();
2515
+ out.push(line);
2516
+ }
2517
+ }
2518
+ closeBlock();
2519
+ return out.join("\n");
2520
+ }
2521
+ var NUMERIC = /^[-+]?\d+(?:\.\d+)?$/;
2522
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2523
+ var MEMBER_EXPRESSION = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)+$/;
2524
+ var SLUG = /^[a-z]+(?:[-_][a-z]+)+$/;
2525
+ var PATHISH = /^(?:\.{0,2}\/|~\/)/;
2526
+ var URLISH = /^[a-z][a-z0-9+.-]*:\/\//i;
2527
+ var CODE_KEYWORDS = /* @__PURE__ */ new Set([
2528
+ "true",
2529
+ "false",
2530
+ "null",
2531
+ "none",
2532
+ "nil",
2533
+ "undefined",
2534
+ "void",
2535
+ "string",
2536
+ "number",
2537
+ "boolean",
2538
+ "object",
2539
+ "any",
2540
+ "unknown",
2541
+ "bool",
2542
+ "str",
2543
+ "int"
2544
+ ]);
2545
+ var UNAMBIGUOUS_CREDENTIAL_NAME = /(?:password|passwd|passphrase|secret|credentials?|private[_-]?key)/i;
2546
+ function looksLikeKeyMaterial(value) {
2547
+ const hasDigit = /\d/.test(value);
2548
+ const hasAlpha = /[A-Za-z]/.test(value);
2549
+ const mixedCase = /[a-z]/.test(value) && /[A-Z]/.test(value);
2550
+ return hasDigit && hasAlpha || mixedCase || value.length >= 24;
2551
+ }
2552
+ function looksLikeCode(value) {
2553
+ return NUMERIC.test(value) || CODE_KEYWORDS.has(value.toLowerCase()) || MEMBER_EXPRESSION.test(value) || PATHISH.test(value) || URLISH.test(value);
2554
+ }
2555
+ function trimTrailingPunctuation(value) {
2556
+ const match = /[.:!]+$/.exec(value);
2557
+ if (!match || match.index === 0) return { value, tail: "" };
2558
+ return { value: value.slice(0, match.index), tail: match[0] };
2559
+ }
2560
+ function redactSecrets(text) {
2561
+ if (!text) return text;
2562
+ let out = redactPrivateKeyBlocks(text);
2563
+ out = out.replace(DOTENV_CREDENTIAL, (full, head, quote, value) => {
2564
+ if (looksLikeCode(value) || value.startsWith("$")) return full;
2565
+ return `${head}${quote}${marker("credential", value.length)}${quote}`;
2566
+ });
2567
+ out = out.replace(NAMED_CREDENTIAL, (full, head, quote, raw) => {
2568
+ const { value, tail } = trimTrailingPunctuation(raw);
2569
+ if (value.length < 8) return full;
2570
+ if (looksLikeCode(value) || value.startsWith("$")) return full;
2571
+ if (UNAMBIGUOUS_CREDENTIAL_NAME.test(head) && /\d/.test(value)) {
2572
+ return `${head}${quote}${marker("credential", value.length)}${quote}${tail}`;
2573
+ }
2574
+ if (IDENTIFIER.test(value) || SLUG.test(value)) return full;
2575
+ if (!UNAMBIGUOUS_CREDENTIAL_NAME.test(head) && !looksLikeKeyMaterial(value)) return full;
2576
+ return `${head}${quote}${marker("credential", value.length)}${quote}${tail}`;
2577
+ });
2578
+ for (const pattern of PROVIDER_KEY_PATTERNS) {
2579
+ out = out.replace(pattern, (hit) => marker("api key", hit.length));
2580
+ }
2581
+ out = out.replace(CLOUD_ACCESS_KEY_ID, (hit) => marker("access key id", hit.length));
2582
+ return out;
2583
+ }
2584
+
992
2585
  // src/services/executeTool.ts
993
2586
  var GREP_OUTPUT_MODES = ["content", "files", "count"];
994
2587
  function num(value) {
@@ -1015,6 +2608,10 @@ function grepOptions(args) {
1015
2608
  }
1016
2609
  var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
1017
2610
  async function executeTool(name, args, fs10, fetcher, signal) {
2611
+ const outcome = await runTool(name, args, fs10, fetcher, signal);
2612
+ return { ...outcome, output: redactSecrets(outcome.output) };
2613
+ }
2614
+ async function runTool(name, args, fs10, fetcher, signal) {
1018
2615
  if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
1019
2616
  switch (name) {
1020
2617
  case "read_file": {
@@ -1144,14 +2741,14 @@ var PendingApprovals = class {
1144
2741
  ask(request) {
1145
2742
  const id = `ap-${Date.now()}-${counter++}`;
1146
2743
  const pending = { id, request, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
1147
- return new Promise((resolve3) => {
2744
+ return new Promise((resolve4) => {
1148
2745
  const settle = (granted) => {
1149
2746
  const entry = this.entries.get(id);
1150
2747
  if (!entry) return;
1151
2748
  clearTimeout(entry.timer);
1152
2749
  this.entries.delete(id);
1153
2750
  this.opts.onSettled?.(id, granted);
1154
- resolve3(granted);
2751
+ resolve4(granted);
1155
2752
  };
1156
2753
  const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
1157
2754
  timer.unref?.();
@@ -4350,11 +5947,11 @@ function composeAugmentedPrompt(task, allTasks, opts) {
4350
5947
  if (isHitlTask(task)) {
4351
5948
  blocks.push(renderCheckpointInstruction());
4352
5949
  }
4353
- const marker = renderCompletionMarker(task);
4354
- if (blocks.length === 0) return basePrompt + marker;
5950
+ const marker2 = renderCompletionMarker(task);
5951
+ if (blocks.length === 0) return basePrompt + marker2;
4355
5952
  return `${blocks.join("\n\n")}
4356
5953
 
4357
- ${basePrompt}${marker}`;
5954
+ ${basePrompt}${marker2}`;
4358
5955
  }
4359
5956
 
4360
5957
  // src/services/VerdictEngine.ts
@@ -5791,28 +7388,28 @@ function posixBinDirs() {
5791
7388
  ];
5792
7389
  }
5793
7390
  function windowsBinDirs(env, home) {
5794
- const join14 = (...parts) => path5.win32.join(...parts);
5795
- const under = (base, ...parts) => base ? [join14(base, ...parts)] : [];
7391
+ const join16 = (...parts) => path5.win32.join(...parts);
7392
+ const under = (base, ...parts) => base ? [join16(base, ...parts)] : [];
5796
7393
  const appData = env.APPDATA;
5797
7394
  const localAppData = env.LOCALAPPDATA;
5798
7395
  const programData = env.ProgramData;
5799
7396
  return [
5800
7397
  // Native per-user installers, PowerShell-driven and otherwise.
5801
- join14(home, ".local", "bin"),
5802
- join14(home, ".opencode", "bin"),
5803
- join14(home, ".claude", "bin"),
5804
- join14(home, ".codex", "bin"),
7398
+ join16(home, ".local", "bin"),
7399
+ join16(home, ".opencode", "bin"),
7400
+ join16(home, ".claude", "bin"),
7401
+ join16(home, ".codex", "bin"),
5805
7402
  ...under(localAppData, "Programs", "claude"),
5806
7403
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
5807
7404
  // literal spelling is kept as a fallback for a host that does not export
5808
7405
  // APPDATA (a service account, a stripped CI container).
5809
7406
  ...under(appData, "npm"),
5810
- join14(home, "AppData", "Roaming", "npm"),
7407
+ join16(home, "AppData", "Roaming", "npm"),
5811
7408
  ...under(localAppData, "pnpm"),
5812
7409
  ...under(localAppData, "Yarn", "bin"),
5813
- join14(home, ".bun", "bin"),
7410
+ join16(home, ".bun", "bin"),
5814
7411
  // Windows package managers.
5815
- join14(home, "scoop", "shims"),
7412
+ join16(home, "scoop", "shims"),
5816
7413
  ...under(env.SCOOP, "shims"),
5817
7414
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
5818
7415
  ...under(programData, "chocolatey", "bin"),
@@ -5822,7 +7419,7 @@ function windowsBinDirs(env, home) {
5822
7419
  ...under(localAppData, "Microsoft", "WindowsApps"),
5823
7420
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
5824
7421
  ...under(localAppData, "Volta", "bin"),
5825
- join14(home, ".volta", "bin")
7422
+ join16(home, ".volta", "bin")
5826
7423
  ];
5827
7424
  }
5828
7425
  function wellKnownBinDirs(deps = {}) {
@@ -6123,7 +7720,7 @@ var defaultAppServerClient = async (command, args, options) => {
6123
7720
  const PATH = await augmentedPath();
6124
7721
  const launch = await planDirectLaunch(command, args).catch(() => null);
6125
7722
  if (!launch) return null;
6126
- return new Promise((resolve3) => {
7723
+ return new Promise((resolve4) => {
6127
7724
  let child;
6128
7725
  try {
6129
7726
  child = spawn(launch.file, launch.args, {
@@ -6132,7 +7729,7 @@ var defaultAppServerClient = async (command, args, options) => {
6132
7729
  windowsVerbatimArguments: launch.verbatim
6133
7730
  });
6134
7731
  } catch {
6135
- resolve3(null);
7732
+ resolve4(null);
6136
7733
  return;
6137
7734
  }
6138
7735
  let settled = false;
@@ -6141,7 +7738,7 @@ var defaultAppServerClient = async (command, args, options) => {
6141
7738
  settled = true;
6142
7739
  clearTimeout(timer);
6143
7740
  killTree(child);
6144
- resolve3(value);
7741
+ resolve4(value);
6145
7742
  };
6146
7743
  const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
6147
7744
  child.on("error", () => finish(null));
@@ -7181,11 +8778,71 @@ var RunnerInstallation = class {
7181
8778
  }
7182
8779
  };
7183
8780
 
8781
+ // src/services/settingsEnvAllowlist.ts
8782
+ var ORDEWELL_SETTABLE_ENV = [
8783
+ "AI_PROVIDER",
8784
+ "ORCHESTRATOR_MODEL",
8785
+ "ORDEWELL_SUBAGENT_MODEL",
8786
+ "ORDEWELL_PLANNER_EFFORT",
8787
+ "ORDEWELL_RESEARCH_ENABLED",
8788
+ "ORDEWELL_MAX_PARALLEL"
8789
+ ];
8790
+ var SETTINGS_ENV_REFUSED = [
8791
+ // Approval posture and pre-approved scope.
8792
+ "ORDEWELL_AUTONOMOUS_MODE",
8793
+ "ORDEWELL_APPROVAL_MODE",
8794
+ "ORDEWELL_APPROVAL_ALLOW",
8795
+ // Where settings are persisted.
8796
+ "ORDEWELL_SETTINGS_PATH",
8797
+ // Runtime options and dynamic loaders.
8798
+ "NODE_OPTIONS",
8799
+ "NODE_REPL_EXTERNAL_MODULE",
8800
+ "NODE_PATH",
8801
+ "ELECTRON_RUN_AS_NODE",
8802
+ "LD_PRELOAD",
8803
+ "LD_LIBRARY_PATH",
8804
+ "LD_AUDIT",
8805
+ "DYLD_INSERT_LIBRARIES",
8806
+ "DYLD_LIBRARY_PATH",
8807
+ "DYLD_FRAMEWORK_PATH",
8808
+ // Executable resolution.
8809
+ "PATH",
8810
+ "PATHEXT",
8811
+ "NODE"
8812
+ ];
8813
+ var REFUSED = new Set(SETTINGS_ENV_REFUSED);
8814
+ function providerEnvKeys() {
8815
+ const keys = [];
8816
+ for (const meta of Object.values(ALL_PROVIDERS)) {
8817
+ if (meta.apiKeyEnvVar) keys.push(meta.apiKeyEnvVar);
8818
+ if (meta.baseUrlEnvVar) keys.push(meta.baseUrlEnvVar);
8819
+ for (const detect of meta.detectEnvVars) keys.push(detect);
8820
+ }
8821
+ return keys;
8822
+ }
8823
+ var PROVIDER_CREDENTIAL_ENV = new Set(providerEnvKeys());
8824
+ var SETTINGS_ENV_ALLOWLIST = new Set(
8825
+ [...providerEnvKeys(), ...ORDEWELL_SETTABLE_ENV].filter((key) => !REFUSED.has(key))
8826
+ );
8827
+ function admitSettingsEnv(env) {
8828
+ const accepted = {};
8829
+ const rejected = [];
8830
+ for (const [key, value] of Object.entries(env)) {
8831
+ if (REFUSED.has(key) || !SETTINGS_ENV_ALLOWLIST.has(key) || typeof value !== "string") {
8832
+ rejected.push(key);
8833
+ continue;
8834
+ }
8835
+ accepted[key] = value;
8836
+ }
8837
+ return { accepted, rejected };
8838
+ }
8839
+
7184
8840
  // src/services/harness/CliAgentAiService.ts
7185
8841
  import { spawn as nodeSpawn } from "child_process";
7186
8842
 
7187
8843
  // src/utils/workspace.ts
7188
8844
  import * as fs3 from "fs";
8845
+ import * as path9 from "path";
7189
8846
  var WorkspaceNotFoundError = class extends Error {
7190
8847
  constructor(workspace) {
7191
8848
  super(`Workspace "${workspace}" does not exist.`);
@@ -7205,6 +8862,46 @@ function assertWorkspaceExists(workspace, deps = {}) {
7205
8862
  const isDirectory = deps.isDirectory ?? defaultIsDirectory;
7206
8863
  if (!isDirectory(workspace)) throw new WorkspaceNotFoundError(workspace);
7207
8864
  }
8865
+ var WorkspaceNotAProjectError = class extends Error {
8866
+ constructor(workspace) {
8867
+ super(`Workspace "${workspace}" is not a project directory (no .ordewell state, version control, or recognized manifest found).`);
8868
+ this.workspace = workspace;
8869
+ this.name = "WorkspaceNotAProjectError";
8870
+ }
8871
+ workspace;
8872
+ };
8873
+ var PROJECT_MARKER_DIRS = [".ordewell", ".git", ".hg", ".svn", ".bzr"];
8874
+ var PROJECT_MARKER_FILES = [
8875
+ "package.json",
8876
+ "Cargo.toml",
8877
+ "go.mod",
8878
+ "pyproject.toml",
8879
+ "setup.py",
8880
+ "requirements.txt",
8881
+ "Pipfile",
8882
+ "pom.xml",
8883
+ "build.gradle",
8884
+ "build.gradle.kts",
8885
+ "Gemfile",
8886
+ "composer.json",
8887
+ "pubspec.yaml",
8888
+ "mix.exs",
8889
+ "CMakeLists.txt"
8890
+ ];
8891
+ function assertWorkspaceIsProject(workspace, deps = {}) {
8892
+ const realpath = deps.realpath ?? ((candidate) => fs3.realpathSync(candidate));
8893
+ const exists = deps.exists ?? ((candidate) => fs3.existsSync(candidate));
8894
+ let resolved;
8895
+ try {
8896
+ resolved = realpath(workspace);
8897
+ } catch {
8898
+ throw new WorkspaceNotAProjectError(workspace);
8899
+ }
8900
+ const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
8901
+ (marker2) => exists(path9.join(resolved, marker2))
8902
+ );
8903
+ if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
8904
+ }
7208
8905
 
7209
8906
  // src/services/harness/AgentAdapter.ts
7210
8907
  var LineBuffer = class {
@@ -7260,8 +8957,8 @@ var StdioAgentAdapter = class {
7260
8957
  }
7261
8958
  async start(opts) {
7262
8959
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
7263
- this.processEnded = new Promise((resolve3) => {
7264
- this.markEnded = resolve3;
8960
+ this.processEnded = new Promise((resolve4) => {
8961
+ this.markEnded = resolve4;
7265
8962
  });
7266
8963
  const spec = this.spawnSpec(opts);
7267
8964
  const resolvePath = this.deps.resolvePath ?? augmentedPath;
@@ -7309,7 +9006,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
7309
9006
  onEvent({ type: "error", message: this.exitMessage() });
7310
9007
  return;
7311
9008
  }
7312
- await new Promise((resolve3) => {
9009
+ await new Promise((resolve4) => {
7313
9010
  let settled = false;
7314
9011
  const finish = () => {
7315
9012
  if (settled) return;
@@ -7318,7 +9015,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
7318
9015
  signal?.removeEventListener("abort", onAbort);
7319
9016
  this.process?.removeListener("exit", onExit);
7320
9017
  this.process?.removeListener("error", onExit);
7321
- resolve3();
9018
+ resolve4();
7322
9019
  };
7323
9020
  const emit = (event) => {
7324
9021
  onEvent(event);
@@ -7519,14 +9216,14 @@ function codexSandboxUnavailableMessage() {
7519
9216
  ].join("\n");
7520
9217
  }
7521
9218
  function runProbe(deps, cwd, env, flags) {
7522
- return new Promise((resolve3) => {
9219
+ return new Promise((resolve4) => {
7523
9220
  let output = "";
7524
9221
  let settled = false;
7525
9222
  const finish = (code) => {
7526
9223
  if (settled) return;
7527
9224
  settled = true;
7528
9225
  clearTimeout(timer);
7529
- resolve3({ code, output });
9226
+ resolve4({ code, output });
7530
9227
  };
7531
9228
  const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
7532
9229
  timer.unref?.();
@@ -7609,8 +9306,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
7609
9306
  async handshake(opts) {
7610
9307
  this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
7611
9308
  if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
7612
- const ready = new Promise((resolve3) => {
7613
- this.settleHandshake = resolve3;
9309
+ const ready = new Promise((resolve4) => {
9310
+ this.settleHandshake = resolve4;
7614
9311
  });
7615
9312
  this.writeLine({
7616
9313
  jsonrpc: "2.0",
@@ -7623,8 +9320,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
7623
9320
  // A binary that rejects `app-server` outright dies immediately; waiting
7624
9321
  // out the timeout would turn a one-line diagnostic into a 30s stall.
7625
9322
  this.processEnded.then(() => false),
7626
- new Promise((resolve3) => {
7627
- const t = setTimeout(() => resolve3(false), HANDSHAKE_TIMEOUT_MS);
9323
+ new Promise((resolve4) => {
9324
+ const t = setTimeout(() => resolve4(false), HANDSHAKE_TIMEOUT_MS);
7628
9325
  t.unref?.();
7629
9326
  })
7630
9327
  ]);
@@ -7912,12 +9609,12 @@ var OpenCodeAdapter = class {
7912
9609
  cwd: opts.cwd,
7913
9610
  windowsVerbatimArguments: launch.verbatim
7914
9611
  });
7915
- const banner = new Promise((resolve3) => {
9612
+ const banner = new Promise((resolve4) => {
7916
9613
  let seen = "";
7917
9614
  const scan = (chunk) => {
7918
9615
  seen += chunk.toString();
7919
9616
  const match = seen.match(/https?:\/\/[^\s]+/);
7920
- if (match) resolve3(match[0].replace(/[.,)]$/, ""));
9617
+ if (match) resolve4(match[0].replace(/[.,)]$/, ""));
7921
9618
  };
7922
9619
  this.process.stdout?.on("data", scan);
7923
9620
  this.process.stderr?.on("data", (chunk) => {
@@ -7926,15 +9623,15 @@ var OpenCodeAdapter = class {
7926
9623
  });
7927
9624
  this.process.on("exit", () => {
7928
9625
  this.exited = true;
7929
- resolve3(null);
9626
+ resolve4(null);
7930
9627
  });
7931
9628
  this.process.on("error", (err) => {
7932
9629
  this.exited = true;
7933
9630
  this.stderrTail = (this.stderrTail + `
7934
9631
  ${err.message}`).slice(-STDERR_TAIL_CHARS2);
7935
- resolve3(null);
9632
+ resolve4(null);
7936
9633
  });
7937
- const timer = setTimeout(() => resolve3(null), SERVER_READY_TIMEOUT_MS);
9634
+ const timer = setTimeout(() => resolve4(null), SERVER_READY_TIMEOUT_MS);
7938
9635
  timer.unref?.();
7939
9636
  });
7940
9637
  this.baseUrl = await banner;
@@ -7964,8 +9661,8 @@ ${this.stderrTail.trim()}` : ""}`);
7964
9661
  const streamAbort = new AbortController();
7965
9662
  let connected = () => {
7966
9663
  };
7967
- const streamReady = new Promise((resolve3) => {
7968
- connected = resolve3;
9664
+ const streamReady = new Promise((resolve4) => {
9665
+ connected = resolve4;
7969
9666
  });
7970
9667
  const live = this.streamEvents(
7971
9668
  streamAbort.signal,
@@ -8117,14 +9814,14 @@ ${this.stderrTail.trim()}` : ""}`);
8117
9814
  }
8118
9815
  }
8119
9816
  }
8120
- async json(method, path15, body, signal) {
8121
- const response = await this.deps.fetch(`${this.baseUrl}${path15}`, {
9817
+ async json(method, path17, body, signal) {
9818
+ const response = await this.deps.fetch(`${this.baseUrl}${path17}`, {
8122
9819
  method,
8123
9820
  headers: { "content-type": "application/json" },
8124
9821
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
8125
9822
  ...signal ? { signal } : {}
8126
9823
  });
8127
- if (!response.ok) throw new Error(`${method} ${path15} failed: ${response.status} ${response.statusText}`);
9824
+ if (!response.ok) throw new Error(`${method} ${path17} failed: ${response.status} ${response.statusText}`);
8128
9825
  return await response.json().catch(() => null);
8129
9826
  }
8130
9827
  nativeSessionId() {
@@ -8421,7 +10118,8 @@ var CliAgentAiService = class {
8421
10118
  let stepIndex = 0;
8422
10119
  const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
8423
10120
  [... truncated, total ${value.length} chars]` : value;
8424
- const settle = (id, output, success, outcome) => {
10121
+ const settle = (id, rawOutput, success, outcome) => {
10122
+ const output = redactSecrets(rawOutput);
8425
10123
  const call = pendingCalls.get(id);
8426
10124
  pendingCalls.delete(id);
8427
10125
  const step = {
@@ -8938,11 +10636,11 @@ function executionSummary(tasks) {
8938
10636
 
8939
10637
  // src/utils/sessionStore.ts
8940
10638
  import * as fs5 from "fs";
8941
- import * as path10 from "path";
10639
+ import * as path11 from "path";
8942
10640
 
8943
10641
  // src/utils/fsHelpers.ts
8944
10642
  import * as fs4 from "fs";
8945
- import * as path9 from "path";
10643
+ import * as path10 from "path";
8946
10644
  var STATE_DIR = ".ordewell";
8947
10645
  function ensureDir(dir) {
8948
10646
  if (!fs4.existsSync(dir)) {
@@ -8950,13 +10648,29 @@ function ensureDir(dir) {
8950
10648
  }
8951
10649
  }
8952
10650
  function getStateDir(baseDir) {
8953
- return path9.join(baseDir ?? process.cwd(), STATE_DIR);
10651
+ return path10.join(baseDir ?? process.cwd(), STATE_DIR);
10652
+ }
10653
+ var IGNORE_FILE = ".gitignore";
10654
+ function ensureStateDirIgnored(baseDir) {
10655
+ const dir = getStateDir(baseDir);
10656
+ ensureDir(dir);
10657
+ try {
10658
+ fs4.writeFileSync(path10.join(dir, IGNORE_FILE), "*\n", { flag: "wx" });
10659
+ } catch (err) {
10660
+ if (err.code !== "EEXIST") throw err;
10661
+ }
10662
+ }
10663
+
10664
+ // src/utils/sessionId.ts
10665
+ import { randomBytes } from "crypto";
10666
+ function mintSessionId() {
10667
+ return `session-${randomBytes(16).toString("hex")}`;
8954
10668
  }
8955
10669
 
8956
10670
  // src/utils/sessionStore.ts
8957
10671
  var SESSIONS_SUBDIR = "sessions";
8958
10672
  function getSessionsDir(baseDir) {
8959
- return path10.join(getStateDir(baseDir), SESSIONS_SUBDIR);
10673
+ return path11.join(getStateDir(baseDir), SESSIONS_SUBDIR);
8960
10674
  }
8961
10675
  var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
8962
10676
  function hasScalarRunner(parsed) {
@@ -8975,9 +10689,10 @@ function sessionFilename(session) {
8975
10689
  function saveSession(plan, goal, baseDir, id) {
8976
10690
  const sessionsDir = getSessionsDir(baseDir);
8977
10691
  ensureDir(sessionsDir);
10692
+ ensureStateDirIgnored(baseDir);
8978
10693
  const now = (/* @__PURE__ */ new Date()).toISOString();
8979
10694
  const meta = {
8980
- id: id || `session-${Date.now()}`,
10695
+ id: id || mintSessionId(),
8981
10696
  goal: goal || "(untitled)",
8982
10697
  runners: plan.runners,
8983
10698
  taskCount: plan.tasks.flatMap((t) => [t, ...t.subtasks || []]).length,
@@ -8990,7 +10705,7 @@ function saveSession(plan, goal, baseDir, id) {
8990
10705
  plan: JSON.parse(JSON.stringify(plan))
8991
10706
  };
8992
10707
  const filename = sessionFilename(meta);
8993
- const filePath = path10.join(sessionsDir, filename);
10708
+ const filePath = path11.join(sessionsDir, filename);
8994
10709
  fs5.writeFileSync(filePath, JSON.stringify(session, null, 2));
8995
10710
  return meta;
8996
10711
  }
@@ -9000,7 +10715,7 @@ function listSessions(baseDir, logger = defaultLogger) {
9000
10715
  const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
9001
10716
  return files.map((f) => {
9002
10717
  try {
9003
- const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
10718
+ const raw = fs5.readFileSync(path11.join(sessionsDir, f), "utf-8");
9004
10719
  const parsed = JSON.parse(raw);
9005
10720
  if (hasScalarRunner(parsed)) {
9006
10721
  logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
@@ -9018,7 +10733,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
9018
10733
  const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
9019
10734
  for (const f of files) {
9020
10735
  try {
9021
- const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
10736
+ const raw = fs5.readFileSync(path11.join(sessionsDir, f), "utf-8");
9022
10737
  const session = JSON.parse(raw);
9023
10738
  if (session.meta.id === sessionId) return f;
9024
10739
  } catch (err) {
@@ -9033,7 +10748,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
9033
10748
  try {
9034
10749
  const file = findSessionFile(sessionsDir, sessionId, logger);
9035
10750
  if (file) {
9036
- const raw = fs5.readFileSync(path10.join(sessionsDir, file), "utf-8");
10751
+ const raw = fs5.readFileSync(path11.join(sessionsDir, file), "utf-8");
9037
10752
  const parsed = JSON.parse(raw);
9038
10753
  if (hasScalarRunner(parsed)) {
9039
10754
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -9067,7 +10782,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
9067
10782
  try {
9068
10783
  const file = findSessionFile(sessionsDir, sessionId, logger);
9069
10784
  if (file) {
9070
- fs5.unlinkSync(path10.join(sessionsDir, file));
10785
+ fs5.unlinkSync(path11.join(sessionsDir, file));
9071
10786
  return true;
9072
10787
  }
9073
10788
  } catch (err) {
@@ -9086,11 +10801,6 @@ var PlanEditError = class extends Error {
9086
10801
  function sessionRuntimeSettings(settings) {
9087
10802
  return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist };
9088
10803
  }
9089
- var lastMintedAt = 0;
9090
- function mintSessionId() {
9091
- lastMintedAt = Math.max(Date.now(), lastMintedAt + 1);
9092
- return `session-${lastMintedAt}`;
9093
- }
9094
10804
  var Session = class {
9095
10805
  /** Injected by a test; when present it is the service, forever. */
9096
10806
  pinnedAiService;
@@ -10531,8 +12241,8 @@ var HeadlessRunner = class extends AbstractRunner {
10531
12241
  import { execFile as execFile2 } from "child_process";
10532
12242
  import { promisify as promisify4 } from "util";
10533
12243
  import { tmpdir } from "os";
10534
- import { join as join9 } from "path";
10535
- import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
12244
+ import { join as join10 } from "path";
12245
+ import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
10536
12246
 
10537
12247
  // src/utils/tmux.ts
10538
12248
  import { execSync as execSync2 } from "child_process";
@@ -10623,7 +12333,7 @@ var TmuxSession = class extends AbstractTerminalSession {
10623
12333
  * lets the user keep poking at a finished task's terminal.
10624
12334
  */
10625
12335
  async start(command, args, cwd, env) {
10626
- writeFileSync5(this.logPath, "");
12336
+ writeFileSync6(this.logPath, "");
10627
12337
  const envAssignments = Object.entries(env ?? {}).map(([k, v]) => `${k}=${posixShellQuote(v)}`).join(" ");
10628
12338
  const inner = [command, ...args].map(posixShellQuote).join(" ");
10629
12339
  const prefix = envAssignments ? `env ${envAssignments} ` : "";
@@ -10637,7 +12347,7 @@ var TmuxSession = class extends AbstractTerminalSession {
10637
12347
  if (this.exited) return;
10638
12348
  let content;
10639
12349
  try {
10640
- content = existsSync5(this.logPath) ? readFileSync4(this.logPath, "utf8") : "";
12350
+ content = existsSync6(this.logPath) ? readFileSync4(this.logPath, "utf8") : "";
10641
12351
  } catch {
10642
12352
  return;
10643
12353
  }
@@ -10816,7 +12526,7 @@ var TmuxRunner = class extends AbstractRunner {
10816
12526
  windowName,
10817
12527
  this.execFileImpl,
10818
12528
  this.pollIntervalMs,
10819
- join9(this.logDir, `${id}.log`),
12529
+ join10(this.logDir, `${id}.log`),
10820
12530
  this.socket
10821
12531
  );
10822
12532
  invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
@@ -10826,26 +12536,128 @@ var TmuxRunner = class extends AbstractRunner {
10826
12536
  }
10827
12537
  };
10828
12538
 
12539
+ // src/utils/daemonToken.ts
12540
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
12541
+ import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeSync } from "fs";
12542
+ import { homedir as homedir5 } from "os";
12543
+ import { join as join11 } from "path";
12544
+ var BEARER_PREFIX = "bearer ";
12545
+ var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
12546
+ var DAEMON_SUBPROTOCOL = "ordewell.v1";
12547
+ function configDir() {
12548
+ return join11(homedir5(), ".config", "ordewell");
12549
+ }
12550
+ function daemonTokenPath(port) {
12551
+ return join11(configDir(), `server-${port}.token`);
12552
+ }
12553
+ function mintDaemonToken(port) {
12554
+ const token = randomBytes2(32).toString("base64url");
12555
+ const file = daemonTokenPath(port);
12556
+ mkdirSync5(configDir(), { recursive: true });
12557
+ try {
12558
+ unlinkSync3(file);
12559
+ } catch {
12560
+ }
12561
+ const fd = openSync(file, "wx", 384);
12562
+ try {
12563
+ writeSync(fd, token);
12564
+ } finally {
12565
+ closeSync(fd);
12566
+ }
12567
+ return { token, file };
12568
+ }
12569
+ function readDaemonToken(port) {
12570
+ try {
12571
+ const token = readFileSync5(daemonTokenPath(port), "utf8").trim();
12572
+ return token === "" ? void 0 : token;
12573
+ } catch {
12574
+ return void 0;
12575
+ }
12576
+ }
12577
+ function clearDaemonToken(port) {
12578
+ try {
12579
+ unlinkSync3(daemonTokenPath(port));
12580
+ } catch {
12581
+ }
12582
+ }
12583
+ function bearerHeaderValue(token) {
12584
+ return `Bearer ${token}`;
12585
+ }
12586
+ function tokenSubprotocols(token) {
12587
+ return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
12588
+ }
12589
+ function extractPresentedToken(carriers) {
12590
+ const authorization = carriers.authorization;
12591
+ if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
12592
+ const value = authorization.slice(BEARER_PREFIX.length).trim();
12593
+ if (value !== "") return value;
12594
+ }
12595
+ for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
12596
+ const value = offered.trim();
12597
+ if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
12598
+ const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
12599
+ if (token !== "") return token;
12600
+ }
12601
+ return void 0;
12602
+ }
12603
+ function tokensMatch(presented, expected) {
12604
+ if (presented === void 0) return false;
12605
+ const a = Buffer.from(presented, "utf8");
12606
+ const b = Buffer.from(expected, "utf8");
12607
+ if (a.length !== b.length) return false;
12608
+ return timingSafeEqual(a, b);
12609
+ }
12610
+
10829
12611
  // src/plugins/RunnerRegistry.ts
10830
12612
  import * as fs7 from "fs";
10831
- import * as path12 from "path";
12613
+ import * as path14 from "path";
10832
12614
  import * as os5 from "os";
10833
- import { execSync as execSync3 } from "child_process";
12615
+ import { execFileSync } from "child_process";
12616
+ import { randomBytes as randomBytes3 } from "crypto";
10834
12617
 
10835
12618
  // src/plugins/FsPluginStore.ts
10836
12619
  import * as fs6 from "fs";
10837
- import * as path11 from "path";
12620
+ import * as path13 from "path";
10838
12621
  import * as os4 from "os";
10839
- function userPluginsDir() {
10840
- return path11.join(os4.homedir(), ".config", "ordewell", "plugins");
12622
+
12623
+ // src/plugins/pluginNames.ts
12624
+ import * as path12 from "path";
12625
+ var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
12626
+ var MAX_PLUGIN_NAME_LENGTH = 64;
12627
+ function isPlainPluginName(name) {
12628
+ return typeof name === "string" && name.length <= MAX_PLUGIN_NAME_LENGTH && PLUGIN_NAME_PATTERN.test(name);
12629
+ }
12630
+ function assertPlainPluginName(name) {
12631
+ if (!isPlainPluginName(name)) {
12632
+ throw new Error(
12633
+ `Invalid plugin name: ${JSON.stringify(name)}. Names must be a single plain segment of letters, digits, '.', '-' or '_' (max ${MAX_PLUGIN_NAME_LENGTH} characters).`
12634
+ );
12635
+ }
10841
12636
  }
12637
+ function resolvePluginInstallDir(pluginsDir, name) {
12638
+ assertPlainPluginName(name);
12639
+ const root = path12.resolve(pluginsDir);
12640
+ const dest = path12.resolve(root, name);
12641
+ const relative3 = path12.relative(root, dest);
12642
+ if (relative3 !== name || relative3 === "" || path12.isAbsolute(relative3)) {
12643
+ throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
12644
+ }
12645
+ return dest;
12646
+ }
12647
+
12648
+ // src/plugins/manifestValidation.ts
10842
12649
  function isValidManifest(obj) {
10843
12650
  if (!obj || typeof obj !== "object") return false;
10844
12651
  const m = obj;
10845
- return typeof m.name === "string" && typeof m.displayName === "string" && typeof m.description === "string" && typeof m.version === "string" && typeof m.runner === "object" && m.runner !== null && typeof m.runner.command === "string" && Array.isArray(m.runner.argsTemplate) && typeof m.features === "object" && m.features !== null && typeof m.modelDiscovery === "object" && m.modelDiscovery !== null;
12652
+ return isPlainPluginName(m.name) && typeof m.displayName === "string" && typeof m.description === "string" && typeof m.version === "string" && typeof m.runner === "object" && m.runner !== null && typeof m.runner.command === "string" && Array.isArray(m.runner.argsTemplate) && typeof m.features === "object" && m.features !== null && typeof m.modelDiscovery === "object" && m.modelDiscovery !== null;
12653
+ }
12654
+
12655
+ // src/plugins/FsPluginStore.ts
12656
+ function userPluginsDir() {
12657
+ return path13.join(os4.homedir(), ".config", "ordewell", "plugins");
10846
12658
  }
10847
12659
  function loadManifestFromDir(dir) {
10848
- const manifestPath = path11.join(dir, "manifest.json");
12660
+ const manifestPath = path13.join(dir, "manifest.json");
10849
12661
  if (!fs6.existsSync(manifestPath)) return null;
10850
12662
  try {
10851
12663
  const raw = fs6.readFileSync(manifestPath, "utf8");
@@ -10860,8 +12672,8 @@ function copyDirSync(src, dest) {
10860
12672
  ensureDir(dest);
10861
12673
  const entries = fs6.readdirSync(src, { withFileTypes: true });
10862
12674
  for (const entry of entries) {
10863
- const srcPath = path11.join(src, entry.name);
10864
- const destPath = path11.join(dest, entry.name);
12675
+ const srcPath = path13.join(src, entry.name);
12676
+ const destPath = path13.join(dest, entry.name);
10865
12677
  if (entry.isDirectory()) {
10866
12678
  copyDirSync(srcPath, destPath);
10867
12679
  } else {
@@ -10873,7 +12685,7 @@ function rmDirSync(dir) {
10873
12685
  if (!fs6.existsSync(dir)) return;
10874
12686
  const entries = fs6.readdirSync(dir, { withFileTypes: true });
10875
12687
  for (const entry of entries) {
10876
- const p = path11.join(dir, entry.name);
12688
+ const p = path13.join(dir, entry.name);
10877
12689
  if (entry.isDirectory()) {
10878
12690
  rmDirSync(p);
10879
12691
  } else {
@@ -10897,6 +12709,13 @@ var FsPluginStore = class {
10897
12709
  }
10898
12710
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
10899
12711
  }
12712
+ listDir(dir) {
12713
+ try {
12714
+ return fs6.readdirSync(dir);
12715
+ } catch {
12716
+ return [];
12717
+ }
12718
+ }
10900
12719
  loadManifest(pluginDir) {
10901
12720
  return loadManifestFromDir(pluginDir);
10902
12721
  }
@@ -10919,11 +12738,11 @@ var FsPluginStore = class {
10919
12738
  return null;
10920
12739
  }
10921
12740
  }
10922
- dirExists(path15) {
10923
- return fs6.existsSync(path15) && fs6.statSync(path15).isDirectory();
12741
+ dirExists(path17) {
12742
+ return fs6.existsSync(path17) && fs6.statSync(path17).isDirectory();
10924
12743
  }
10925
- exists(path15) {
10926
- return fs6.existsSync(path15);
12744
+ exists(path17) {
12745
+ return fs6.existsSync(path17);
10927
12746
  }
10928
12747
  };
10929
12748
 
@@ -11211,17 +13030,98 @@ var OPENCODE_MANIFEST = {
11211
13030
  ]
11212
13031
  };
11213
13032
 
13033
+ // src/plugins/pluginSource.ts
13034
+ var ALLOWED_PLUGIN_HOSTS = [
13035
+ "github.com",
13036
+ "gitlab.com",
13037
+ "bitbucket.org",
13038
+ "codeberg.org"
13039
+ ];
13040
+ var SAFE_REPO_PATH = /^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/;
13041
+ var HAS_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]+:/;
13042
+ var SCP_LIKE_REMOTE = /^[^/\\]+@[^/\\]+:/;
13043
+ function hasControlCharacter(value) {
13044
+ for (let i = 0; i < value.length; i++) {
13045
+ const code = value.charCodeAt(i);
13046
+ if (code < 32 || code === 127) return true;
13047
+ }
13048
+ return false;
13049
+ }
13050
+ function assertInstallablePluginUrl(url) {
13051
+ if (typeof url !== "string" || url.trim() === "") {
13052
+ throw new Error("A plugin repository URL is required.");
13053
+ }
13054
+ if (hasControlCharacter(url)) {
13055
+ throw new Error("Plugin URL contains control characters.");
13056
+ }
13057
+ let parsed;
13058
+ try {
13059
+ parsed = new URL(url.trim());
13060
+ } catch {
13061
+ throw new Error(`Not a valid plugin repository URL: ${url}`);
13062
+ }
13063
+ if (parsed.protocol !== "https:") {
13064
+ throw new Error(
13065
+ `Plugin repositories must be cloned over https (got "${parsed.protocol}//"). git transport helpers and non-https schemes are not accepted.`
13066
+ );
13067
+ }
13068
+ if (parsed.username || parsed.password) {
13069
+ throw new Error("Plugin URL must not embed credentials.");
13070
+ }
13071
+ if (parsed.port) {
13072
+ throw new Error("Plugin URL must not specify a port.");
13073
+ }
13074
+ if (parsed.search || parsed.hash) {
13075
+ throw new Error("Plugin URL must not carry a query string or fragment.");
13076
+ }
13077
+ const host = parsed.hostname.toLowerCase();
13078
+ if (!ALLOWED_PLUGIN_HOSTS.includes(host)) {
13079
+ throw new Error(
13080
+ `Plugin host not allowed: ${host}. Allowed hosts: ${ALLOWED_PLUGIN_HOSTS.join(", ")}.`
13081
+ );
13082
+ }
13083
+ if (!SAFE_REPO_PATH.test(parsed.pathname)) {
13084
+ throw new Error(`Not a plain repository path: ${parsed.pathname}`);
13085
+ }
13086
+ return parsed;
13087
+ }
13088
+ function classifyPluginSource(source) {
13089
+ const trimmed = source.trim();
13090
+ if (trimmed.startsWith("github:")) {
13091
+ const repo = trimmed.slice("github:".length).replace(/\.git$/, "");
13092
+ return { kind: "git", url: `https://github.com/${repo}.git` };
13093
+ }
13094
+ if (HAS_SCHEME.test(trimmed) || SCP_LIKE_REMOTE.test(trimmed)) {
13095
+ return { kind: "git", url: trimmed };
13096
+ }
13097
+ return { kind: "path", path: source };
13098
+ }
13099
+
11214
13100
  // src/plugins/RunnerRegistry.ts
11215
13101
  var BUILTIN_MANIFESTS = [
11216
13102
  CLAUDE_CODE_MANIFEST,
11217
13103
  CODEX_MANIFEST,
11218
13104
  OPENCODE_MANIFEST
11219
13105
  ];
13106
+ var RESERVED_RUNNER_NAMES = new Set(BUILTIN_MANIFESTS.map((m) => m.name.toLowerCase()));
13107
+ function isReservedRunnerName(name) {
13108
+ return RESERVED_RUNNER_NAMES.has(name.toLowerCase());
13109
+ }
13110
+ function gitClone(url, destDir) {
13111
+ execFileSync("git", ["clone", "--depth", "1", "--", url, destDir], {
13112
+ stdio: "pipe",
13113
+ timeout: 3e4,
13114
+ // A repository that demands credentials should fail, not hang on a prompt.
13115
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
13116
+ });
13117
+ }
11220
13118
  var RunnerRegistry = class {
11221
13119
  plugins = /* @__PURE__ */ new Map();
11222
13120
  store;
11223
- constructor(store) {
13121
+ clone;
13122
+ constructor(store, clone) {
11224
13123
  this.store = store ?? new FsPluginStore();
13124
+ this.clone = clone ?? gitClone;
11225
13125
  this.loadBuiltins();
11226
13126
  }
11227
13127
  loadBuiltins() {
@@ -11232,9 +13132,15 @@ var RunnerRegistry = class {
11232
13132
  loadUserPlugins() {
11233
13133
  const names = this.store.listUserPluginDirs();
11234
13134
  for (const name of names) {
11235
- const pluginDir = path12.join(this.store.getUserPluginsDir(), name);
13135
+ let pluginDir;
13136
+ try {
13137
+ pluginDir = resolvePluginInstallDir(this.store.getUserPluginsDir(), name);
13138
+ } catch {
13139
+ continue;
13140
+ }
11236
13141
  const manifest = this.store.loadManifest(pluginDir);
11237
- if (!manifest) continue;
13142
+ if (!manifest || !isPlainPluginName(manifest.name)) continue;
13143
+ if (isReservedRunnerName(manifest.name)) continue;
11238
13144
  this.plugins.set(manifest.name, {
11239
13145
  manifest,
11240
13146
  source: "user",
@@ -11263,7 +13169,7 @@ var RunnerRegistry = class {
11263
13169
  return entry?.source === "builtin";
11264
13170
  }
11265
13171
  installFromPath(sourcePath) {
11266
- const absPath = path12.resolve(sourcePath);
13172
+ const absPath = path14.resolve(sourcePath);
11267
13173
  if (!this.store.dirExists(absPath)) {
11268
13174
  throw new Error(`Plugin source must be a directory: ${absPath}`);
11269
13175
  }
@@ -11271,48 +13177,50 @@ var RunnerRegistry = class {
11271
13177
  if (!manifest) {
11272
13178
  throw new Error(`No valid manifest.json found in: ${absPath}`);
11273
13179
  }
11274
- const destDir = path12.join(this.store.getUserPluginsDir(), manifest.name);
11275
- this.store.ensureDir(destDir);
11276
- this.store.copyDir(absPath, destDir);
11277
- this.plugins.set(manifest.name, {
11278
- manifest,
11279
- source: "user",
11280
- installPath: destDir
11281
- });
11282
- return manifest;
13180
+ return this.registerInstalled(manifest, absPath);
11283
13181
  }
11284
13182
  installFromGit(url) {
11285
- const repoName = url.split("/").pop()?.replace(/\.git$/, "") || "plugin";
11286
- const tmpDir = path12.join(os5.tmpdir(), `ordewell-plugin-${Date.now()}`);
13183
+ const validated = assertInstallablePluginUrl(url);
13184
+ const tmpDir = path14.join(os5.tmpdir(), `ordewell-plugin-${randomBytes3(12).toString("hex")}`);
11287
13185
  this.store.ensureDir(tmpDir);
11288
13186
  try {
11289
- execSync3(`git clone --depth 1 "${url}" "${tmpDir}"`, { stdio: "pipe", timeout: 3e4 });
11290
- } catch (err) {
11291
- const message = err instanceof Error ? err.message : String(err);
11292
- throw new Error(`Failed to clone repository: ${message}`);
11293
- }
11294
- const contents = fsReaddirSync(tmpDir);
11295
- const hasManifest = contents.some((f) => f === "manifest.json");
11296
- if (!hasManifest && contents.length === 1 && this.store.dirExists(path12.join(tmpDir, contents[0]))) {
11297
- const subDir = path12.join(tmpDir, contents[0]);
11298
- return this.finishInstall(subDir, repoName, tmpDir);
11299
- }
11300
- if (!hasManifest) {
13187
+ try {
13188
+ this.clone(validated.toString(), tmpDir);
13189
+ } catch (err) {
13190
+ const message = err instanceof Error ? err.message : String(err);
13191
+ throw new Error(`Failed to clone repository: ${message}`);
13192
+ }
13193
+ const contents = this.store.listDir(tmpDir);
13194
+ const hasManifest = contents.some((f) => f === "manifest.json");
13195
+ let sourceDir = tmpDir;
13196
+ if (!hasManifest) {
13197
+ const onlyEntry = contents.length === 1 ? path14.join(tmpDir, contents[0]) : void 0;
13198
+ if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
13199
+ throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
13200
+ }
13201
+ sourceDir = onlyEntry;
13202
+ }
13203
+ const manifest = this.store.loadManifest(sourceDir);
13204
+ if (!manifest) {
13205
+ throw new Error("No valid manifest.json found in cloned repository");
13206
+ }
13207
+ return this.registerInstalled(manifest, sourceDir);
13208
+ } finally {
11301
13209
  this.store.removeDir(tmpDir);
11302
- throw new Error(`No manifest.json found in repository: ${url}`);
11303
13210
  }
11304
- return this.finishInstall(tmpDir, repoName, tmpDir);
11305
13211
  }
11306
- finishInstall(sourceDir, repoName, tmpDir) {
11307
- const manifest = this.store.loadManifest(sourceDir);
11308
- if (!manifest) {
11309
- this.store.removeDir(tmpDir);
11310
- throw new Error(`No valid manifest.json found in cloned repository`);
13212
+ /**
13213
+ * The single destination-building step both install routes share: the name is
13214
+ * constrained to a plain segment and the resolved destination is asserted to
13215
+ * be inside the plugins directory before anything is copied.
13216
+ */
13217
+ registerInstalled(manifest, sourceDir) {
13218
+ if (isReservedRunnerName(manifest.name)) {
13219
+ throw new Error(`Cannot install a plugin named after a built-in runner: ${manifest.name}`);
11311
13220
  }
11312
- const destDir = path12.join(this.store.getUserPluginsDir(), manifest.name);
13221
+ const destDir = resolvePluginInstallDir(this.store.getUserPluginsDir(), manifest.name);
11313
13222
  this.store.ensureDir(destDir);
11314
13223
  this.store.copyDir(sourceDir, destDir);
11315
- this.store.removeDir(tmpDir);
11316
13224
  this.plugins.set(manifest.name, {
11317
13225
  manifest,
11318
13226
  source: "user",
@@ -11333,7 +13241,8 @@ var RunnerRegistry = class {
11333
13241
  this.plugins.delete(name);
11334
13242
  }
11335
13243
  createSkeleton(name, outputDir) {
11336
- const dir = path12.resolve(outputDir, name);
13244
+ assertPlainPluginName(name);
13245
+ const dir = path14.resolve(outputDir, name);
11337
13246
  this.store.ensureDir(dir);
11338
13247
  const manifest = {
11339
13248
  name,
@@ -11365,8 +13274,8 @@ var RunnerRegistry = class {
11365
13274
  ]
11366
13275
  }
11367
13276
  };
11368
- this.store.writeFile(path12.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
11369
- const runSh = path12.join(dir, "run.sh");
13277
+ this.store.writeFile(path14.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
13278
+ const runSh = path14.join(dir, "run.sh");
11370
13279
  const scriptContent = `#!/usr/bin/env bash
11371
13280
  # Custom runner script for ${name}
11372
13281
  # Receives:
@@ -11390,13 +13299,6 @@ echo "Running ${name} with prompt: $prompt"
11390
13299
  return dir;
11391
13300
  }
11392
13301
  };
11393
- function fsReaddirSync(dir) {
11394
- try {
11395
- return fs7.readdirSync(dir);
11396
- } catch {
11397
- return [];
11398
- }
11399
- }
11400
13302
  function fsChmodSync(fpath, mode) {
11401
13303
  try {
11402
13304
  fs7.chmodSync(fpath, mode);
@@ -11406,14 +13308,14 @@ function fsChmodSync(fpath, mode) {
11406
13308
 
11407
13309
  // src/utils/stateStore.ts
11408
13310
  import * as fs8 from "fs";
11409
- import * as path13 from "path";
13311
+ import * as path15 from "path";
11410
13312
  var STATE_FILE = "state.json";
11411
13313
  function getStatePath(baseDir) {
11412
- return path13.join(getStateDir(baseDir), STATE_FILE);
13314
+ return path15.join(getStateDir(baseDir), STATE_FILE);
11413
13315
  }
11414
13316
  function saveState(plan, baseDir) {
11415
13317
  const statePath = getStatePath(baseDir ?? process.cwd());
11416
- ensureDir(path13.dirname(statePath));
13318
+ ensureDir(path15.dirname(statePath));
11417
13319
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
11418
13320
  fs8.writeFileSync(statePath, JSON.stringify(plan, null, 2));
11419
13321
  }
@@ -11446,7 +13348,7 @@ function stateExists(baseDir) {
11446
13348
 
11447
13349
  // src/services/SettingsService.ts
11448
13350
  import * as fs9 from "fs";
11449
- import * as path14 from "path";
13351
+ import * as path16 from "path";
11450
13352
  import * as os6 from "os";
11451
13353
  var DEFAULTS = {
11452
13354
  grillMe: { enabled: false },
@@ -11459,7 +13361,7 @@ var DEFAULTS = {
11459
13361
  function getSettingsPath() {
11460
13362
  const override = process.env.ORDEWELL_SETTINGS_PATH;
11461
13363
  if (override && override.trim()) return override.trim();
11462
- return path14.join(os6.homedir(), ".config", "ordewell", "settings.json");
13364
+ return path16.join(os6.homedir(), ".config", "ordewell", "settings.json");
11463
13365
  }
11464
13366
  function readPlannerModels(raw) {
11465
13367
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
@@ -11624,7 +13526,7 @@ var SettingsService = class {
11624
13526
  return { ...DEFAULTS, grillMe: { ...DEFAULTS.grillMe }, tdd: { ...DEFAULTS.tdd }, prd: { ...DEFAULTS.prd }, review: { ...DEFAULTS.review }, verification: { ...DEFAULTS.verification }, researchSubagents: { ...DEFAULTS.researchSubagents } };
11625
13527
  }
11626
13528
  persist() {
11627
- const dir = path14.dirname(this.filePath);
13529
+ const dir = path16.dirname(this.filePath);
11628
13530
  if (!fs9.existsSync(dir)) {
11629
13531
  fs9.mkdirSync(dir, { recursive: true });
11630
13532
  }
@@ -11668,6 +13570,7 @@ var PlannerModelMemory = class {
11668
13570
  }
11669
13571
  };
11670
13572
  export {
13573
+ ALLOWED_PLUGIN_HOSTS,
11671
13574
  ALL_PROVIDERS,
11672
13575
  AUTO_COMMANDS,
11673
13576
  AbstractRunner,
@@ -11686,6 +13589,8 @@ export {
11686
13589
  CommandLineTooLongError,
11687
13590
  ConsoleLogger,
11688
13591
  ContextCollector,
13592
+ DAEMON_SUBPROTOCOL,
13593
+ DAEMON_TOKEN_SUBPROTOCOL_PREFIX,
11689
13594
  DENY_ALL,
11690
13595
  EmbeddedNewlineError,
11691
13596
  EnvConfig,
@@ -11702,9 +13607,12 @@ export {
11702
13607
  ModelResolver,
11703
13608
  OPENCODE_MANIFEST,
11704
13609
  ORCHESTRATOR_SHORTCUTS,
13610
+ ORDEWELL_SETTABLE_ENV,
11705
13611
  OpenAiService,
11706
13612
  OpenCodeAdapter,
11707
13613
  PLAN_ENVELOPE_KEY,
13614
+ PLUGIN_NAME_PATTERN,
13615
+ PROVIDER_CREDENTIAL_ENV,
11708
13616
  PROVIDER_DETECT_PRIORITY,
11709
13617
  PROVIDER_LABEL,
11710
13618
  PROVIDER_PRIORITY,
@@ -11719,6 +13627,8 @@ export {
11719
13627
  RunnerInstallation,
11720
13628
  RunnerRegistry,
11721
13629
  SEARCH_EXCLUSIONS,
13630
+ SETTINGS_ENV_ALLOWLIST,
13631
+ SETTINGS_ENV_REFUSED,
11722
13632
  STATE_DIR,
11723
13633
  STOPPED_TOOL_RESULT,
11724
13634
  SYMBOL_LANGUAGES,
@@ -11731,13 +13641,19 @@ export {
11731
13641
  TmuxRunner,
11732
13642
  VerdictEngine,
11733
13643
  WINDOWS_MAX_COMMAND_LINE,
13644
+ WorkspaceNotAProjectError,
11734
13645
  WorkspaceNotFoundError,
11735
13646
  addTaskToPlan,
13647
+ admitSettingsEnv,
11736
13648
  applyHeadLimit,
11737
13649
  applyTaskOps,
13650
+ assertInstallablePluginUrl,
13651
+ assertPlainPluginName,
11738
13652
  assertWorkspaceExists,
13653
+ assertWorkspaceIsProject,
11739
13654
  augmentPromptWithPriorOutputs,
11740
13655
  augmentedPath,
13656
+ bearerHeaderValue,
11741
13657
  buildConversationSystemPrompt,
11742
13658
  buildFallbackGrepArgs,
11743
13659
  buildGlobArgs,
@@ -11765,7 +13681,9 @@ export {
11765
13681
  classifyCommand,
11766
13682
  classifyOutcome,
11767
13683
  classifyPlannerReply,
13684
+ classifyPluginSource,
11768
13685
  clearAugmentedPathCache,
13686
+ clearDaemonToken,
11769
13687
  clearResearchShellCache,
11770
13688
  clearState,
11771
13689
  clipboardCopyCommand,
@@ -11777,6 +13695,7 @@ export {
11777
13695
  createAiService,
11778
13696
  createEmptyPlan,
11779
13697
  createTask,
13698
+ daemonTokenPath,
11780
13699
  defaultLogger,
11781
13700
  definitionPattern,
11782
13701
  deleteSession,
@@ -11787,6 +13706,7 @@ export {
11787
13706
  emptyWarnings,
11788
13707
  enabledRunners,
11789
13708
  ensureDir,
13709
+ ensureStateDirIgnored,
11790
13710
  escapeControlCharsInStrings,
11791
13711
  executionLogBlock,
11792
13712
  executionSummary,
@@ -11794,6 +13714,7 @@ export {
11794
13714
  extractObjectWithBalance,
11795
13715
  extractObjectsWithKey,
11796
13716
  extractPrdBlock,
13717
+ extractPresentedToken,
11797
13718
  fetchAllProviderModels,
11798
13719
  filterFallbackByAnchoredInclude,
11799
13720
  filterModelsForPrompt,
@@ -11811,6 +13732,9 @@ export {
11811
13732
  isCliProvider,
11812
13733
  isExecutableResolved,
11813
13734
  isOpenAiProvider,
13735
+ isPlainPluginName,
13736
+ isReservedRunnerName,
13737
+ isValidManifest,
11814
13738
  killTree,
11815
13739
  knownModelId,
11816
13740
  languageForId,
@@ -11823,6 +13747,8 @@ export {
11823
13747
  migrateLegacyPlan,
11824
13748
  migratePlanState,
11825
13749
  migrateTask,
13750
+ mintDaemonToken,
13751
+ mintSessionId,
11826
13752
  modelContextBlock,
11827
13753
  modifyValidationFeedback,
11828
13754
  normalizeAgentArgs,
@@ -11838,6 +13764,7 @@ export {
11838
13764
  providerForRunner,
11839
13765
  reEmitPlanPrompt,
11840
13766
  reEmitTaskOpsPrompt,
13767
+ readDaemonToken,
11841
13768
  referencePattern,
11842
13769
  removeTaskFromPlan,
11843
13770
  renderPlanMap,
@@ -11849,6 +13776,7 @@ export {
11849
13776
  resolveArgs,
11850
13777
  resolveDefaultMode,
11851
13778
  resolveModelShortcut,
13779
+ resolvePluginInstallDir,
11852
13780
  resolveProvider,
11853
13781
  resolveProviderFromPrefix,
11854
13782
  resolveResearchShell,
@@ -11879,6 +13807,8 @@ export {
11879
13807
  tmuxSocketName,
11880
13808
  tmuxWindowName,
11881
13809
  toOrchestratorOptions,
13810
+ tokenSubprotocols,
13811
+ tokensMatch,
11882
13812
  truncatedPlanReEmitPrompt,
11883
13813
  updateTaskInPlan,
11884
13814
  validateModifiedPlan,