@ordewell/core 0.4.8 → 0.4.10
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/ModeResolver-Dx5gzNPC.d.ts +44 -0
- package/dist/ModeResolver-F5wnlSz1.d.mts +44 -0
- package/dist/{chunk-SRGQBGI5.mjs → chunk-IO6PEEN6.mjs} +3 -1
- package/dist/{chunk-SRGQBGI5.mjs.map → chunk-IO6PEEN6.mjs.map} +1 -1
- package/dist/{chunk-YXAIQKVE.mjs → chunk-TUGJXD4G.mjs} +2 -2
- package/dist/chunk-YI4ZDZEE.mjs +862 -0
- package/dist/chunk-YI4ZDZEE.mjs.map +1 -0
- package/dist/index.d.mts +202 -17
- package/dist/index.d.ts +202 -17
- package/dist/index.js +2568 -433
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1966 -182
- package/dist/index.mjs.map +1 -1
- package/dist/{parsing-RjN9JbyG.d.ts → parsing-BqbJySeo.d.mts} +5 -43
- package/dist/{parsing-xpozY5ac.d.mts → parsing-X0Gvuz-q.d.ts} +5 -43
- package/dist/parsing.d.mts +2 -1
- package/dist/parsing.d.ts +2 -1
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +2 -2
- package/dist/{plan-utils-nfHKSbd3.d.mts → plan-utils-B5rVXNVI.d.mts} +27 -5
- package/dist/{plan-utils-Cgc0cCH1.d.ts → plan-utils-DtsfHnn_.d.ts} +27 -5
- package/dist/plan-utils.d.mts +2 -1
- package/dist/plan-utils.d.ts +2 -1
- package/dist/plan-utils.js +1 -9
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-4KIWPW4K.mjs +0 -510
- package/dist/chunk-4KIWPW4K.mjs.map +0 -1
- /package/dist/{chunk-YXAIQKVE.mjs.map → chunk-TUGJXD4G.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -96,6 +96,11 @@ __export(index_exports, {
|
|
|
96
96
|
SettingsService: () => SettingsService,
|
|
97
97
|
StdioAgentAdapter: () => StdioAgentAdapter,
|
|
98
98
|
TASK_OPS_ENVELOPE_KEY: () => TASK_OPS_ENVELOPE_KEY,
|
|
99
|
+
TASK_QUERY_ANSWER_OR_OPS: () => TASK_QUERY_ANSWER_OR_OPS,
|
|
100
|
+
TASK_QUERY_ENVELOPE_KEY: () => TASK_QUERY_ENVELOPE_KEY,
|
|
101
|
+
TASK_QUERY_FIELDS: () => TASK_QUERY_FIELDS,
|
|
102
|
+
TASK_QUERY_PROTOCOL: () => TASK_QUERY_PROTOCOL,
|
|
103
|
+
TASK_QUERY_REMINDER: () => TASK_QUERY_REMINDER,
|
|
99
104
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION: () => TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
100
105
|
TaskOrchestrator: () => TaskOrchestrator,
|
|
101
106
|
TmuxRunner: () => TmuxRunner,
|
|
@@ -166,6 +171,7 @@ __export(index_exports, {
|
|
|
166
171
|
emptyWarnings: () => emptyWarnings,
|
|
167
172
|
enabledRunners: () => enabledRunners,
|
|
168
173
|
ensureDir: () => ensureDir,
|
|
174
|
+
ensureStateDirIgnored: () => ensureStateDirIgnored,
|
|
169
175
|
escapeControlCharsInStrings: () => escapeControlCharsInStrings,
|
|
170
176
|
executionLogBlock: () => executionLogBlock,
|
|
171
177
|
executionSummary: () => executionSummary,
|
|
@@ -215,6 +221,7 @@ __export(index_exports, {
|
|
|
215
221
|
parsePartialPlan: () => parsePartialPlan,
|
|
216
222
|
parsePlanJson: () => parsePlanJson,
|
|
217
223
|
parseTaskOpsJson: () => parseTaskOpsJson,
|
|
224
|
+
parseTaskQueryJson: () => parseTaskQueryJson,
|
|
218
225
|
pendingEditRulesBlock: () => pendingEditRulesBlock,
|
|
219
226
|
planDirectLaunch: () => planDirectLaunch,
|
|
220
227
|
planShellLaunch: () => planShellLaunch,
|
|
@@ -223,11 +230,13 @@ __export(index_exports, {
|
|
|
223
230
|
providerForRunner: () => providerForRunner,
|
|
224
231
|
reEmitPlanPrompt: () => reEmitPlanPrompt,
|
|
225
232
|
reEmitTaskOpsPrompt: () => reEmitTaskOpsPrompt,
|
|
233
|
+
reEmitTaskQueryPrompt: () => reEmitTaskQueryPrompt,
|
|
226
234
|
readDaemonToken: () => readDaemonToken,
|
|
227
235
|
referencePattern: () => referencePattern,
|
|
228
236
|
removeTaskFromPlan: () => removeTaskFromPlan,
|
|
229
237
|
renderPlanMap: () => renderPlanMap,
|
|
230
238
|
renderPriorOutputs: () => renderPriorOutputs,
|
|
239
|
+
renderTaskQueryAnswer: () => renderTaskQueryAnswer,
|
|
231
240
|
renumberTasks: () => renumberTasks,
|
|
232
241
|
repairLoop: () => repairLoop,
|
|
233
242
|
researchShellWarning: () => researchShellWarning,
|
|
@@ -261,7 +270,9 @@ __export(index_exports, {
|
|
|
261
270
|
summarizeOutput: () => summarizeOutput,
|
|
262
271
|
summarizeToolCall: () => summarizeToolCall,
|
|
263
272
|
taskOpsRejectedPrompt: () => taskOpsRejectedPrompt,
|
|
273
|
+
taskQuerySignature: () => taskQuerySignature,
|
|
264
274
|
textHasTaskOps: () => textHasTaskOps,
|
|
275
|
+
textHasTaskQuery: () => textHasTaskQuery,
|
|
265
276
|
tmuxSessionName: () => tmuxSessionName,
|
|
266
277
|
tmuxSocketName: () => tmuxSocketName,
|
|
267
278
|
tmuxWindowName: () => tmuxWindowName,
|
|
@@ -642,13 +653,1233 @@ var AUTO_COMMANDS = [
|
|
|
642
653
|
"jq",
|
|
643
654
|
"yq"
|
|
644
655
|
];
|
|
656
|
+
var UNIVERSAL_FLAGS = ["--help", "--version"];
|
|
657
|
+
var COUNT_FLAG = /^-\d+$/;
|
|
658
|
+
var FLAG_POLICY = {
|
|
659
|
+
ls: {
|
|
660
|
+
cluster: true,
|
|
661
|
+
booleans: [
|
|
662
|
+
"-1",
|
|
663
|
+
"-a",
|
|
664
|
+
"-A",
|
|
665
|
+
"-b",
|
|
666
|
+
"-c",
|
|
667
|
+
"-C",
|
|
668
|
+
"-d",
|
|
669
|
+
"-D",
|
|
670
|
+
"-f",
|
|
671
|
+
"-F",
|
|
672
|
+
"-g",
|
|
673
|
+
"-G",
|
|
674
|
+
"-h",
|
|
675
|
+
"-H",
|
|
676
|
+
"-i",
|
|
677
|
+
"-k",
|
|
678
|
+
"-l",
|
|
679
|
+
"-L",
|
|
680
|
+
"-m",
|
|
681
|
+
"-n",
|
|
682
|
+
"-N",
|
|
683
|
+
"-o",
|
|
684
|
+
"-p",
|
|
685
|
+
"-q",
|
|
686
|
+
"-Q",
|
|
687
|
+
"-r",
|
|
688
|
+
"-R",
|
|
689
|
+
"-s",
|
|
690
|
+
"-S",
|
|
691
|
+
"-t",
|
|
692
|
+
"-u",
|
|
693
|
+
"-U",
|
|
694
|
+
"-v",
|
|
695
|
+
"-x",
|
|
696
|
+
"-X",
|
|
697
|
+
"-Z",
|
|
698
|
+
"--all",
|
|
699
|
+
"--almost-all",
|
|
700
|
+
"--author",
|
|
701
|
+
"--escape",
|
|
702
|
+
"--classify",
|
|
703
|
+
"--file-type",
|
|
704
|
+
"--full-time",
|
|
705
|
+
"--no-group",
|
|
706
|
+
"--human-readable",
|
|
707
|
+
"--si",
|
|
708
|
+
"--dereference-command-line",
|
|
709
|
+
"--inode",
|
|
710
|
+
"--kibibytes",
|
|
711
|
+
"--dereference",
|
|
712
|
+
"--numeric-uid-gid",
|
|
713
|
+
"--literal",
|
|
714
|
+
"--hide-control-chars",
|
|
715
|
+
"--show-control-chars",
|
|
716
|
+
"--quote-name",
|
|
717
|
+
"--reverse",
|
|
718
|
+
"--recursive",
|
|
719
|
+
"--size",
|
|
720
|
+
"--directory",
|
|
721
|
+
"--group-directories-first",
|
|
722
|
+
"--context",
|
|
723
|
+
"--zero"
|
|
724
|
+
],
|
|
725
|
+
values: [
|
|
726
|
+
"-w",
|
|
727
|
+
"-I",
|
|
728
|
+
"-T",
|
|
729
|
+
"--width",
|
|
730
|
+
"--ignore",
|
|
731
|
+
"--hide",
|
|
732
|
+
"--block-size",
|
|
733
|
+
"--color",
|
|
734
|
+
"--format",
|
|
735
|
+
"--sort",
|
|
736
|
+
"--time",
|
|
737
|
+
"--time-style",
|
|
738
|
+
"--indicator-style",
|
|
739
|
+
"--quoting-style",
|
|
740
|
+
"--tabsize"
|
|
741
|
+
]
|
|
742
|
+
},
|
|
743
|
+
tree: {
|
|
744
|
+
cluster: true,
|
|
745
|
+
// `-o` is absent on purpose: it is tree's write-to-a-file flag, which the
|
|
746
|
+
// redirect check never sees because there is no redirect.
|
|
747
|
+
booleans: [
|
|
748
|
+
"-a",
|
|
749
|
+
"-d",
|
|
750
|
+
"-f",
|
|
751
|
+
"-i",
|
|
752
|
+
"-l",
|
|
753
|
+
"-x",
|
|
754
|
+
"-C",
|
|
755
|
+
"-n",
|
|
756
|
+
"-p",
|
|
757
|
+
"-s",
|
|
758
|
+
"-h",
|
|
759
|
+
"-u",
|
|
760
|
+
"-g",
|
|
761
|
+
"-D",
|
|
762
|
+
"-t",
|
|
763
|
+
"-c",
|
|
764
|
+
"-r",
|
|
765
|
+
"-F",
|
|
766
|
+
"-q",
|
|
767
|
+
"-N",
|
|
768
|
+
"-Q",
|
|
769
|
+
"-v",
|
|
770
|
+
"-J",
|
|
771
|
+
"-X",
|
|
772
|
+
"-Z",
|
|
773
|
+
"--noreport",
|
|
774
|
+
"--dirsfirst",
|
|
775
|
+
"--du",
|
|
776
|
+
"--prune",
|
|
777
|
+
"--matchdirs",
|
|
778
|
+
"--inodes",
|
|
779
|
+
"--device",
|
|
780
|
+
"--si",
|
|
781
|
+
"--gitignore",
|
|
782
|
+
"--ignore-case",
|
|
783
|
+
"--filesfirst",
|
|
784
|
+
"--nolinks",
|
|
785
|
+
"--hintro"
|
|
786
|
+
],
|
|
787
|
+
values: [
|
|
788
|
+
"-L",
|
|
789
|
+
"-P",
|
|
790
|
+
"-I",
|
|
791
|
+
"-H",
|
|
792
|
+
"--filelimit",
|
|
793
|
+
"--timefmt",
|
|
794
|
+
"--charset",
|
|
795
|
+
"--sort",
|
|
796
|
+
"--gitfile",
|
|
797
|
+
"--info",
|
|
798
|
+
"--prefix"
|
|
799
|
+
]
|
|
800
|
+
},
|
|
801
|
+
git: {
|
|
802
|
+
cluster: true,
|
|
803
|
+
// Absent on purpose, and each for a reason that was demonstrated rather than
|
|
804
|
+
// guessed at:
|
|
805
|
+
// -c, --config-env set configuration inline, and several configuration
|
|
806
|
+
// keys name a program to run (`core.pager`, `core.editor`,
|
|
807
|
+
// `diff.external`), so this is an execution flag with one
|
|
808
|
+
// indirection in front of it.
|
|
809
|
+
// --exec-path redirects where git finds its helper programs; a
|
|
810
|
+
// fabricated helper ran during an ordinary remote listing.
|
|
811
|
+
// -O, --open-files-in-pager runs an arbitrary named program over search hits.
|
|
812
|
+
// --upload-pack, --receive-pack name the program run on the other end.
|
|
813
|
+
// --textconv, --ext-diff run filters the repository itself configures.
|
|
814
|
+
// --git-dir, --work-tree repoint git at a tree whose configuration is not
|
|
815
|
+
// the one the developer is looking at.
|
|
816
|
+
// -o, --output write a file. `git ls-files -o` is a legitimate spelling
|
|
817
|
+
// of `--others`, but `git archive -o` writes, and the
|
|
818
|
+
// long spelling stays available for the read.
|
|
819
|
+
booleans: [
|
|
820
|
+
"-a",
|
|
821
|
+
"-b",
|
|
822
|
+
"-E",
|
|
823
|
+
"-F",
|
|
824
|
+
"-h",
|
|
825
|
+
"-H",
|
|
826
|
+
"-i",
|
|
827
|
+
"-I",
|
|
828
|
+
"-l",
|
|
829
|
+
"-p",
|
|
830
|
+
"-P",
|
|
831
|
+
"-q",
|
|
832
|
+
"-r",
|
|
833
|
+
"-R",
|
|
834
|
+
"-s",
|
|
835
|
+
"-t",
|
|
836
|
+
"-u",
|
|
837
|
+
"-v",
|
|
838
|
+
"-w",
|
|
839
|
+
"-z",
|
|
840
|
+
"--oneline",
|
|
841
|
+
"--graph",
|
|
842
|
+
"--stat",
|
|
843
|
+
"--shortstat",
|
|
844
|
+
"--numstat",
|
|
845
|
+
"--dirstat",
|
|
846
|
+
"--summary",
|
|
847
|
+
"--name-only",
|
|
848
|
+
"--name-status",
|
|
849
|
+
"--raw",
|
|
850
|
+
"--patch",
|
|
851
|
+
"--no-patch",
|
|
852
|
+
"--patch-with-stat",
|
|
853
|
+
"--cached",
|
|
854
|
+
"--staged",
|
|
855
|
+
"--porcelain",
|
|
856
|
+
"--short",
|
|
857
|
+
"--long",
|
|
858
|
+
"--branch",
|
|
859
|
+
"--show-stash",
|
|
860
|
+
"--show-current",
|
|
861
|
+
"--all",
|
|
862
|
+
"--tags",
|
|
863
|
+
"--heads",
|
|
864
|
+
"--remotes",
|
|
865
|
+
"--list",
|
|
866
|
+
"--verbose",
|
|
867
|
+
"--quiet",
|
|
868
|
+
"--abbrev-ref",
|
|
869
|
+
"--count",
|
|
870
|
+
"--reverse",
|
|
871
|
+
"--first-parent",
|
|
872
|
+
"--no-merges",
|
|
873
|
+
"--merges",
|
|
874
|
+
"--follow",
|
|
875
|
+
"--full-history",
|
|
876
|
+
"--simplify-merges",
|
|
877
|
+
"--sparse",
|
|
878
|
+
"--dense",
|
|
879
|
+
"--ancestry-path",
|
|
880
|
+
"--left-right",
|
|
881
|
+
"--cherry",
|
|
882
|
+
"--cherry-pick",
|
|
883
|
+
"--cherry-mark",
|
|
884
|
+
"--boundary",
|
|
885
|
+
"--topo-order",
|
|
886
|
+
"--date-order",
|
|
887
|
+
"--author-date-order",
|
|
888
|
+
"--relative-date",
|
|
889
|
+
"--parents",
|
|
890
|
+
"--children",
|
|
891
|
+
"--timestamp",
|
|
892
|
+
"--no-walk",
|
|
893
|
+
"--do-walk",
|
|
894
|
+
"--decorate",
|
|
895
|
+
"--no-decorate",
|
|
896
|
+
"--abbrev-commit",
|
|
897
|
+
"--no-abbrev-commit",
|
|
898
|
+
"--abbrev",
|
|
899
|
+
"--no-abbrev",
|
|
900
|
+
"--color",
|
|
901
|
+
"--no-color",
|
|
902
|
+
"--pretty",
|
|
903
|
+
"--format",
|
|
904
|
+
"--expand-tabs",
|
|
905
|
+
"--no-expand-tabs",
|
|
906
|
+
"--numbered",
|
|
907
|
+
"--email",
|
|
908
|
+
"--no-notes",
|
|
909
|
+
"--notes",
|
|
910
|
+
"--show-signature",
|
|
911
|
+
"--find-renames",
|
|
912
|
+
"--find-copies",
|
|
913
|
+
"--find-copies-harder",
|
|
914
|
+
"--break-rewrites",
|
|
915
|
+
"--no-renames",
|
|
916
|
+
"--irreversible-delete",
|
|
917
|
+
"--full-index",
|
|
918
|
+
"--binary",
|
|
919
|
+
"--text",
|
|
920
|
+
"--check",
|
|
921
|
+
"--exit-code",
|
|
922
|
+
"--word-diff",
|
|
923
|
+
"--function-context",
|
|
924
|
+
"--ignore-all-space",
|
|
925
|
+
"--ignore-space-change",
|
|
926
|
+
"--ignore-space-at-eol",
|
|
927
|
+
"--ignore-blank-lines",
|
|
928
|
+
"--ignore-cr-at-eol",
|
|
929
|
+
"--no-prefix",
|
|
930
|
+
"--default-prefix",
|
|
931
|
+
"--no-textconv",
|
|
932
|
+
"--no-ext-diff",
|
|
933
|
+
"--submodule",
|
|
934
|
+
"--ignore-submodules",
|
|
935
|
+
"--recurse-submodules",
|
|
936
|
+
"--no-recurse-submodules",
|
|
937
|
+
"--others",
|
|
938
|
+
"--deleted",
|
|
939
|
+
"--modified",
|
|
940
|
+
"--unmerged",
|
|
941
|
+
"--stage",
|
|
942
|
+
"--killed",
|
|
943
|
+
"--ignored",
|
|
944
|
+
"--exclude-standard",
|
|
945
|
+
"--error-unmatch",
|
|
946
|
+
"--full-name",
|
|
947
|
+
"--directory",
|
|
948
|
+
"--no-empty-directory",
|
|
949
|
+
"--untracked-files",
|
|
950
|
+
"--no-optional-locks",
|
|
951
|
+
"--no-pager",
|
|
952
|
+
"--paginate",
|
|
953
|
+
"--bare",
|
|
954
|
+
"--literal-pathspecs",
|
|
955
|
+
"--glob-pathspecs",
|
|
956
|
+
"--noglob-pathspecs",
|
|
957
|
+
"--icase-pathspecs",
|
|
958
|
+
"--line-number",
|
|
959
|
+
"--column",
|
|
960
|
+
"--heading",
|
|
961
|
+
"--no-index",
|
|
962
|
+
"--untracked",
|
|
963
|
+
"--no-recursive",
|
|
964
|
+
"--name-rev",
|
|
965
|
+
"--root",
|
|
966
|
+
"--dirty",
|
|
967
|
+
"--broken",
|
|
968
|
+
"--always",
|
|
969
|
+
"--merged",
|
|
970
|
+
"--no-merged",
|
|
971
|
+
"--show-toplevel",
|
|
972
|
+
"--show-cdup",
|
|
973
|
+
"--git-common-dir",
|
|
974
|
+
"--absolute-git-dir",
|
|
975
|
+
"--is-inside-work-tree",
|
|
976
|
+
"--is-inside-git-dir",
|
|
977
|
+
"--is-bare-repository",
|
|
978
|
+
"--symbolic",
|
|
979
|
+
"--symbolic-full-name",
|
|
980
|
+
"--verify",
|
|
981
|
+
"--short-hash",
|
|
982
|
+
"--textual",
|
|
983
|
+
"--batch",
|
|
984
|
+
"--batch-check",
|
|
985
|
+
"--batch-all-objects",
|
|
986
|
+
"--buffer",
|
|
987
|
+
"--follow-symlinks",
|
|
988
|
+
"--use-mailmap"
|
|
989
|
+
],
|
|
990
|
+
values: [
|
|
991
|
+
"-n",
|
|
992
|
+
"-C",
|
|
993
|
+
"-B",
|
|
994
|
+
"-M",
|
|
995
|
+
"-L",
|
|
996
|
+
"-S",
|
|
997
|
+
"-G",
|
|
998
|
+
"-U",
|
|
999
|
+
"-e",
|
|
1000
|
+
"-m",
|
|
1001
|
+
"-f",
|
|
1002
|
+
"-g",
|
|
1003
|
+
"-x",
|
|
1004
|
+
"-X",
|
|
1005
|
+
"-W",
|
|
1006
|
+
"--max-count",
|
|
1007
|
+
"--skip",
|
|
1008
|
+
"--since",
|
|
1009
|
+
"--after",
|
|
1010
|
+
"--until",
|
|
1011
|
+
"--before",
|
|
1012
|
+
"--author",
|
|
1013
|
+
"--grep",
|
|
1014
|
+
"--grep-reflog",
|
|
1015
|
+
"--committer",
|
|
1016
|
+
"--date",
|
|
1017
|
+
"--diff-filter",
|
|
1018
|
+
"--unified",
|
|
1019
|
+
"--stat-width",
|
|
1020
|
+
"--stat-name-width",
|
|
1021
|
+
"--stat-count",
|
|
1022
|
+
"--word-diff-regex",
|
|
1023
|
+
"--line-prefix",
|
|
1024
|
+
"--src-prefix",
|
|
1025
|
+
"--dst-prefix",
|
|
1026
|
+
"--pickaxe-regex",
|
|
1027
|
+
"--anchored",
|
|
1028
|
+
"--color-moved",
|
|
1029
|
+
"--color-words",
|
|
1030
|
+
"--inter-hunk-context",
|
|
1031
|
+
"--max-parents",
|
|
1032
|
+
"--min-parents",
|
|
1033
|
+
"--simplify-by-decoration",
|
|
1034
|
+
"--glob",
|
|
1035
|
+
"--exclude",
|
|
1036
|
+
"--sort",
|
|
1037
|
+
"--contains",
|
|
1038
|
+
"--points-at",
|
|
1039
|
+
"--decorate-refs",
|
|
1040
|
+
"--decorate-refs-exclude",
|
|
1041
|
+
"--encoding",
|
|
1042
|
+
"--context",
|
|
1043
|
+
"--max-depth",
|
|
1044
|
+
// `--filters` is absent from this list on purpose: it runs the clean and
|
|
1045
|
+
// smudge filters the repository under research configures, which is the
|
|
1046
|
+
// same execution vector as the configuration flag above with a different
|
|
1047
|
+
// spelling.
|
|
1048
|
+
"--and",
|
|
1049
|
+
"--or",
|
|
1050
|
+
"--not",
|
|
1051
|
+
"--all-match",
|
|
1052
|
+
"--threads",
|
|
1053
|
+
"--path"
|
|
1054
|
+
],
|
|
1055
|
+
patterns: [COUNT_FLAG]
|
|
1056
|
+
},
|
|
1057
|
+
wc: {
|
|
1058
|
+
cluster: true,
|
|
1059
|
+
booleans: [
|
|
1060
|
+
"-c",
|
|
1061
|
+
"-l",
|
|
1062
|
+
"-m",
|
|
1063
|
+
"-w",
|
|
1064
|
+
"-L",
|
|
1065
|
+
"--bytes",
|
|
1066
|
+
"--chars",
|
|
1067
|
+
"--lines",
|
|
1068
|
+
"--words",
|
|
1069
|
+
"--max-line-length",
|
|
1070
|
+
"--total"
|
|
1071
|
+
]
|
|
1072
|
+
},
|
|
1073
|
+
du: {
|
|
1074
|
+
cluster: true,
|
|
1075
|
+
booleans: [
|
|
1076
|
+
"-0",
|
|
1077
|
+
"-a",
|
|
1078
|
+
"-b",
|
|
1079
|
+
"-c",
|
|
1080
|
+
"-D",
|
|
1081
|
+
"-h",
|
|
1082
|
+
"-H",
|
|
1083
|
+
"-k",
|
|
1084
|
+
"-l",
|
|
1085
|
+
"-L",
|
|
1086
|
+
"-m",
|
|
1087
|
+
"-P",
|
|
1088
|
+
"-s",
|
|
1089
|
+
"-S",
|
|
1090
|
+
"-x",
|
|
1091
|
+
"--all",
|
|
1092
|
+
"--apparent-size",
|
|
1093
|
+
"--bytes",
|
|
1094
|
+
"--total",
|
|
1095
|
+
"--dereference-args",
|
|
1096
|
+
"--human-readable",
|
|
1097
|
+
"--si",
|
|
1098
|
+
"--inodes",
|
|
1099
|
+
"--count-links",
|
|
1100
|
+
"--dereference",
|
|
1101
|
+
"--no-dereference",
|
|
1102
|
+
"--separate-dirs",
|
|
1103
|
+
"--summarize",
|
|
1104
|
+
"--one-file-system",
|
|
1105
|
+
"--null"
|
|
1106
|
+
],
|
|
1107
|
+
values: [
|
|
1108
|
+
"-d",
|
|
1109
|
+
"-t",
|
|
1110
|
+
"-B",
|
|
1111
|
+
"-X",
|
|
1112
|
+
"--max-depth",
|
|
1113
|
+
"--threshold",
|
|
1114
|
+
"--block-size",
|
|
1115
|
+
"--exclude",
|
|
1116
|
+
"--exclude-from",
|
|
1117
|
+
"--time",
|
|
1118
|
+
"--time-style",
|
|
1119
|
+
"--files0-from"
|
|
1120
|
+
]
|
|
1121
|
+
},
|
|
1122
|
+
df: {
|
|
1123
|
+
cluster: true,
|
|
1124
|
+
booleans: [
|
|
1125
|
+
"-a",
|
|
1126
|
+
"-h",
|
|
1127
|
+
"-H",
|
|
1128
|
+
"-i",
|
|
1129
|
+
"-k",
|
|
1130
|
+
"-l",
|
|
1131
|
+
"-m",
|
|
1132
|
+
"-P",
|
|
1133
|
+
"-T",
|
|
1134
|
+
"-v",
|
|
1135
|
+
"--all",
|
|
1136
|
+
"--human-readable",
|
|
1137
|
+
"--si",
|
|
1138
|
+
"--inodes",
|
|
1139
|
+
"--local",
|
|
1140
|
+
"--portability",
|
|
1141
|
+
"--print-type",
|
|
1142
|
+
"--total",
|
|
1143
|
+
"--sync",
|
|
1144
|
+
"--no-sync"
|
|
1145
|
+
],
|
|
1146
|
+
values: ["-B", "-t", "-x", "--block-size", "--type", "--exclude-type"]
|
|
1147
|
+
},
|
|
1148
|
+
file: {
|
|
1149
|
+
cluster: true,
|
|
1150
|
+
// `-C`/`--compile` is absent: it writes a compiled magic file.
|
|
1151
|
+
booleans: [
|
|
1152
|
+
"-b",
|
|
1153
|
+
"-c",
|
|
1154
|
+
"-h",
|
|
1155
|
+
"-i",
|
|
1156
|
+
"-k",
|
|
1157
|
+
"-L",
|
|
1158
|
+
"-n",
|
|
1159
|
+
"-N",
|
|
1160
|
+
"-p",
|
|
1161
|
+
"-r",
|
|
1162
|
+
"-s",
|
|
1163
|
+
"-S",
|
|
1164
|
+
"-v",
|
|
1165
|
+
"-z",
|
|
1166
|
+
"-0",
|
|
1167
|
+
"--brief",
|
|
1168
|
+
"--mime",
|
|
1169
|
+
"--mime-type",
|
|
1170
|
+
"--mime-encoding",
|
|
1171
|
+
"--dereference",
|
|
1172
|
+
"--no-dereference",
|
|
1173
|
+
"--keep-going",
|
|
1174
|
+
"--no-buffer",
|
|
1175
|
+
"--no-pad",
|
|
1176
|
+
"--raw",
|
|
1177
|
+
"--special-files",
|
|
1178
|
+
"--uncompress",
|
|
1179
|
+
"--print0",
|
|
1180
|
+
"--checking-printout"
|
|
1181
|
+
],
|
|
1182
|
+
values: [
|
|
1183
|
+
"-e",
|
|
1184
|
+
"-m",
|
|
1185
|
+
"-P",
|
|
1186
|
+
"-F",
|
|
1187
|
+
"--exclude",
|
|
1188
|
+
"--magic-file",
|
|
1189
|
+
"--separator",
|
|
1190
|
+
"--parameter",
|
|
1191
|
+
"--extension",
|
|
1192
|
+
"--files-from"
|
|
1193
|
+
]
|
|
1194
|
+
},
|
|
1195
|
+
head: {
|
|
1196
|
+
cluster: true,
|
|
1197
|
+
booleans: ["-q", "-v", "-z", "--quiet", "--silent", "--verbose", "--zero-terminated"],
|
|
1198
|
+
values: ["-c", "-n", "--bytes", "--lines"],
|
|
1199
|
+
patterns: [COUNT_FLAG]
|
|
1200
|
+
},
|
|
1201
|
+
tail: {
|
|
1202
|
+
cluster: true,
|
|
1203
|
+
booleans: [
|
|
1204
|
+
"-f",
|
|
1205
|
+
"-F",
|
|
1206
|
+
"-q",
|
|
1207
|
+
"-v",
|
|
1208
|
+
"-z",
|
|
1209
|
+
"--follow",
|
|
1210
|
+
"--retry",
|
|
1211
|
+
"--quiet",
|
|
1212
|
+
"--silent",
|
|
1213
|
+
"--verbose",
|
|
1214
|
+
"--zero-terminated"
|
|
1215
|
+
],
|
|
1216
|
+
values: [
|
|
1217
|
+
"-c",
|
|
1218
|
+
"-n",
|
|
1219
|
+
"-s",
|
|
1220
|
+
"--bytes",
|
|
1221
|
+
"--lines",
|
|
1222
|
+
"--sleep-interval",
|
|
1223
|
+
"--pid",
|
|
1224
|
+
"--max-unchanged-stats"
|
|
1225
|
+
],
|
|
1226
|
+
patterns: [COUNT_FLAG]
|
|
1227
|
+
},
|
|
1228
|
+
cat: {
|
|
1229
|
+
cluster: true,
|
|
1230
|
+
booleans: [
|
|
1231
|
+
"-A",
|
|
1232
|
+
"-b",
|
|
1233
|
+
"-e",
|
|
1234
|
+
"-E",
|
|
1235
|
+
"-n",
|
|
1236
|
+
"-s",
|
|
1237
|
+
"-t",
|
|
1238
|
+
"-T",
|
|
1239
|
+
"-u",
|
|
1240
|
+
"-v",
|
|
1241
|
+
"--show-all",
|
|
1242
|
+
"--number-nonblank",
|
|
1243
|
+
"--show-ends",
|
|
1244
|
+
"--number",
|
|
1245
|
+
"--squeeze-blank",
|
|
1246
|
+
"--show-tabs",
|
|
1247
|
+
"--show-nonprinting"
|
|
1248
|
+
]
|
|
1249
|
+
},
|
|
1250
|
+
sort: {
|
|
1251
|
+
cluster: true,
|
|
1252
|
+
// Absent on purpose: `-o`/`--output` writes a file with no redirect for the
|
|
1253
|
+
// redirect check to see, and `--compress-program` names a program that sort
|
|
1254
|
+
// runs over its own temporary files — a permitted binary as an interpreter.
|
|
1255
|
+
booleans: [
|
|
1256
|
+
"-b",
|
|
1257
|
+
"-c",
|
|
1258
|
+
"-C",
|
|
1259
|
+
"-d",
|
|
1260
|
+
"-f",
|
|
1261
|
+
"-g",
|
|
1262
|
+
"-h",
|
|
1263
|
+
"-i",
|
|
1264
|
+
"-M",
|
|
1265
|
+
"-n",
|
|
1266
|
+
"-r",
|
|
1267
|
+
"-R",
|
|
1268
|
+
"-s",
|
|
1269
|
+
"-u",
|
|
1270
|
+
"-V",
|
|
1271
|
+
"-z",
|
|
1272
|
+
"--ignore-leading-blanks",
|
|
1273
|
+
"--check",
|
|
1274
|
+
"--dictionary-order",
|
|
1275
|
+
"--ignore-case",
|
|
1276
|
+
"--general-numeric-sort",
|
|
1277
|
+
"--human-numeric-sort",
|
|
1278
|
+
"--ignore-nonprinting",
|
|
1279
|
+
"--month-sort",
|
|
1280
|
+
"--numeric-sort",
|
|
1281
|
+
"--random-sort",
|
|
1282
|
+
"--reverse",
|
|
1283
|
+
"--stable",
|
|
1284
|
+
"--unique",
|
|
1285
|
+
"--version-sort",
|
|
1286
|
+
"--zero-terminated",
|
|
1287
|
+
"--debug"
|
|
1288
|
+
],
|
|
1289
|
+
values: [
|
|
1290
|
+
"-k",
|
|
1291
|
+
"-t",
|
|
1292
|
+
"-S",
|
|
1293
|
+
"-T",
|
|
1294
|
+
"--key",
|
|
1295
|
+
"--field-separator",
|
|
1296
|
+
"--buffer-size",
|
|
1297
|
+
"--temporary-directory",
|
|
1298
|
+
"--random-source",
|
|
1299
|
+
"--batch-size",
|
|
1300
|
+
"--parallel",
|
|
1301
|
+
"--sort"
|
|
1302
|
+
]
|
|
1303
|
+
},
|
|
1304
|
+
uniq: {
|
|
1305
|
+
cluster: true,
|
|
1306
|
+
booleans: [
|
|
1307
|
+
"-c",
|
|
1308
|
+
"-d",
|
|
1309
|
+
"-D",
|
|
1310
|
+
"-i",
|
|
1311
|
+
"-u",
|
|
1312
|
+
"-z",
|
|
1313
|
+
"--count",
|
|
1314
|
+
"--repeated",
|
|
1315
|
+
"--all-repeated",
|
|
1316
|
+
"--ignore-case",
|
|
1317
|
+
"--unique",
|
|
1318
|
+
"--zero-terminated"
|
|
1319
|
+
],
|
|
1320
|
+
values: ["-f", "-s", "-w", "--skip-fields", "--skip-chars", "--check-chars", "--group"]
|
|
1321
|
+
},
|
|
1322
|
+
// No flag on either runs a program or writes a file, and the planner's
|
|
1323
|
+
// commonest use of `echo` is a section marker — `echo --- files changed ---`
|
|
1324
|
+
// is three tokens that all begin with a dash and none of them is a flag.
|
|
1325
|
+
echo: { freeform: true },
|
|
1326
|
+
printf: { freeform: true },
|
|
1327
|
+
date: {
|
|
1328
|
+
cluster: true,
|
|
1329
|
+
// `-s`/`--set` is absent: it sets the system clock.
|
|
1330
|
+
booleans: [
|
|
1331
|
+
"-u",
|
|
1332
|
+
"-R",
|
|
1333
|
+
"-I",
|
|
1334
|
+
"--utc",
|
|
1335
|
+
"--universal",
|
|
1336
|
+
"--rfc-email",
|
|
1337
|
+
"--rfc-3339",
|
|
1338
|
+
"--iso-8601",
|
|
1339
|
+
"--debug"
|
|
1340
|
+
],
|
|
1341
|
+
values: ["-d", "-f", "-r", "--date", "--file", "--reference"]
|
|
1342
|
+
},
|
|
1343
|
+
stat: {
|
|
1344
|
+
cluster: true,
|
|
1345
|
+
booleans: ["-f", "-L", "-t", "--file-system", "--terse", "--dereference"],
|
|
1346
|
+
values: ["-c", "--format", "--printf", "--cached"]
|
|
1347
|
+
},
|
|
1348
|
+
basename: {
|
|
1349
|
+
cluster: true,
|
|
1350
|
+
booleans: ["-a", "-z", "--multiple", "--zero"],
|
|
1351
|
+
values: ["-s", "--suffix"]
|
|
1352
|
+
},
|
|
1353
|
+
dirname: {
|
|
1354
|
+
cluster: true,
|
|
1355
|
+
booleans: ["-z", "--zero"]
|
|
1356
|
+
},
|
|
1357
|
+
realpath: {
|
|
1358
|
+
cluster: true,
|
|
1359
|
+
booleans: [
|
|
1360
|
+
"-e",
|
|
1361
|
+
"-m",
|
|
1362
|
+
"-L",
|
|
1363
|
+
"-P",
|
|
1364
|
+
"-q",
|
|
1365
|
+
"-s",
|
|
1366
|
+
"-z",
|
|
1367
|
+
"--canonicalize-existing",
|
|
1368
|
+
"--canonicalize-missing",
|
|
1369
|
+
"--logical",
|
|
1370
|
+
"--physical",
|
|
1371
|
+
"--quiet",
|
|
1372
|
+
"--strip",
|
|
1373
|
+
"--no-symlinks",
|
|
1374
|
+
"--zero"
|
|
1375
|
+
],
|
|
1376
|
+
values: ["--relative-to", "--relative-base"]
|
|
1377
|
+
},
|
|
1378
|
+
pwd: { cluster: true, booleans: ["-L", "-P", "--logical", "--physical"] },
|
|
1379
|
+
which: { cluster: true, booleans: ["-a", "-s", "--all"] },
|
|
1380
|
+
type: { cluster: true, booleans: ["-a", "-f", "-p", "-P", "-t"] },
|
|
1381
|
+
uname: {
|
|
1382
|
+
cluster: true,
|
|
1383
|
+
booleans: [
|
|
1384
|
+
"-a",
|
|
1385
|
+
"-i",
|
|
1386
|
+
"-m",
|
|
1387
|
+
"-n",
|
|
1388
|
+
"-o",
|
|
1389
|
+
"-p",
|
|
1390
|
+
"-r",
|
|
1391
|
+
"-s",
|
|
1392
|
+
"-v",
|
|
1393
|
+
"--all",
|
|
1394
|
+
"--kernel-name",
|
|
1395
|
+
"--nodename",
|
|
1396
|
+
"--kernel-release",
|
|
1397
|
+
"--kernel-version",
|
|
1398
|
+
"--machine",
|
|
1399
|
+
"--processor",
|
|
1400
|
+
"--hardware-platform",
|
|
1401
|
+
"--operating-system"
|
|
1402
|
+
]
|
|
1403
|
+
},
|
|
1404
|
+
whoami: {},
|
|
1405
|
+
cut: {
|
|
1406
|
+
cluster: true,
|
|
1407
|
+
// `--output-delimiter` names a separator string, not a file.
|
|
1408
|
+
booleans: ["-n", "-s", "-z", "--only-delimited", "--complement", "--zero-terminated"],
|
|
1409
|
+
values: [
|
|
1410
|
+
"-b",
|
|
1411
|
+
"-c",
|
|
1412
|
+
"-d",
|
|
1413
|
+
"-f",
|
|
1414
|
+
"--bytes",
|
|
1415
|
+
"--characters",
|
|
1416
|
+
"--delimiter",
|
|
1417
|
+
"--fields",
|
|
1418
|
+
"--output-delimiter"
|
|
1419
|
+
]
|
|
1420
|
+
},
|
|
1421
|
+
nl: {
|
|
1422
|
+
cluster: true,
|
|
1423
|
+
booleans: ["-p", "--no-renumber"],
|
|
1424
|
+
values: [
|
|
1425
|
+
"-b",
|
|
1426
|
+
"-d",
|
|
1427
|
+
"-f",
|
|
1428
|
+
"-h",
|
|
1429
|
+
"-i",
|
|
1430
|
+
"-l",
|
|
1431
|
+
"-n",
|
|
1432
|
+
"-s",
|
|
1433
|
+
"-v",
|
|
1434
|
+
"-w",
|
|
1435
|
+
"--body-numbering",
|
|
1436
|
+
"--section-delimiter",
|
|
1437
|
+
"--footer-numbering",
|
|
1438
|
+
"--header-numbering",
|
|
1439
|
+
"--join-blank-lines",
|
|
1440
|
+
"--line-increment",
|
|
1441
|
+
"--number-format",
|
|
1442
|
+
"--number-separator",
|
|
1443
|
+
"--starting-line-number",
|
|
1444
|
+
"--number-width"
|
|
1445
|
+
]
|
|
1446
|
+
},
|
|
1447
|
+
rg: {
|
|
1448
|
+
cluster: true,
|
|
1449
|
+
// `--pre` runs a named preprocessor over every file searched and
|
|
1450
|
+
// `--hostname-bin` runs a named program to resolve the hostname; both are
|
|
1451
|
+
// absent, and `--pre-glob` with them.
|
|
1452
|
+
booleans: [
|
|
1453
|
+
"-a",
|
|
1454
|
+
"-c",
|
|
1455
|
+
"-F",
|
|
1456
|
+
"-h",
|
|
1457
|
+
"-H",
|
|
1458
|
+
"-i",
|
|
1459
|
+
"-I",
|
|
1460
|
+
"-l",
|
|
1461
|
+
"-L",
|
|
1462
|
+
"-n",
|
|
1463
|
+
"-N",
|
|
1464
|
+
"-o",
|
|
1465
|
+
"-p",
|
|
1466
|
+
"-P",
|
|
1467
|
+
"-q",
|
|
1468
|
+
"-s",
|
|
1469
|
+
"-S",
|
|
1470
|
+
"-u",
|
|
1471
|
+
"-uu",
|
|
1472
|
+
"-uuu",
|
|
1473
|
+
"-U",
|
|
1474
|
+
"-v",
|
|
1475
|
+
"-V",
|
|
1476
|
+
"-w",
|
|
1477
|
+
"-x",
|
|
1478
|
+
"-z",
|
|
1479
|
+
"--text",
|
|
1480
|
+
"--count",
|
|
1481
|
+
"--count-matches",
|
|
1482
|
+
"--fixed-strings",
|
|
1483
|
+
"--files",
|
|
1484
|
+
"--files-with-matches",
|
|
1485
|
+
"--files-without-match",
|
|
1486
|
+
"--follow",
|
|
1487
|
+
"--hidden",
|
|
1488
|
+
"--no-hidden",
|
|
1489
|
+
"--ignore-case",
|
|
1490
|
+
"--case-sensitive",
|
|
1491
|
+
"--smart-case",
|
|
1492
|
+
"--invert-match",
|
|
1493
|
+
"--line-number",
|
|
1494
|
+
"--no-line-number",
|
|
1495
|
+
"--with-filename",
|
|
1496
|
+
"--no-filename",
|
|
1497
|
+
"--heading",
|
|
1498
|
+
"--no-heading",
|
|
1499
|
+
"--column",
|
|
1500
|
+
"--no-column",
|
|
1501
|
+
"--byte-offset",
|
|
1502
|
+
"--vimgrep",
|
|
1503
|
+
"--json",
|
|
1504
|
+
"--stats",
|
|
1505
|
+
"--trim",
|
|
1506
|
+
"--pretty",
|
|
1507
|
+
"--passthru",
|
|
1508
|
+
"--word-regexp",
|
|
1509
|
+
"--line-regexp",
|
|
1510
|
+
"--multiline",
|
|
1511
|
+
"--multiline-dotall",
|
|
1512
|
+
"--no-multiline",
|
|
1513
|
+
"--crlf",
|
|
1514
|
+
"--null",
|
|
1515
|
+
"--null-data",
|
|
1516
|
+
"--binary",
|
|
1517
|
+
"--search-zip",
|
|
1518
|
+
"--no-ignore",
|
|
1519
|
+
"--no-ignore-dot",
|
|
1520
|
+
"--no-ignore-exclude",
|
|
1521
|
+
"--no-ignore-files",
|
|
1522
|
+
"--no-ignore-global",
|
|
1523
|
+
"--no-ignore-parent",
|
|
1524
|
+
"--no-ignore-vcs",
|
|
1525
|
+
"--no-ignore-messages",
|
|
1526
|
+
"--no-messages",
|
|
1527
|
+
"--no-require-git",
|
|
1528
|
+
"--require-git",
|
|
1529
|
+
"--one-file-system",
|
|
1530
|
+
"--pcre2",
|
|
1531
|
+
"--no-pcre2",
|
|
1532
|
+
"--auto-hybrid-regex",
|
|
1533
|
+
"--line-buffered",
|
|
1534
|
+
"--block-buffered",
|
|
1535
|
+
"--unicode",
|
|
1536
|
+
"--no-unicode",
|
|
1537
|
+
"--unrestricted",
|
|
1538
|
+
"--type-list",
|
|
1539
|
+
"--debug",
|
|
1540
|
+
"--trace",
|
|
1541
|
+
"--include-zero",
|
|
1542
|
+
"--no-context-separator",
|
|
1543
|
+
"--stop-on-nonmatch",
|
|
1544
|
+
"--quiet",
|
|
1545
|
+
"--no-binary"
|
|
1546
|
+
],
|
|
1547
|
+
values: [
|
|
1548
|
+
"-A",
|
|
1549
|
+
"-B",
|
|
1550
|
+
"-C",
|
|
1551
|
+
"-d",
|
|
1552
|
+
"-e",
|
|
1553
|
+
"-E",
|
|
1554
|
+
"-f",
|
|
1555
|
+
"-g",
|
|
1556
|
+
"-j",
|
|
1557
|
+
"-m",
|
|
1558
|
+
"-M",
|
|
1559
|
+
"-r",
|
|
1560
|
+
"-t",
|
|
1561
|
+
"-T",
|
|
1562
|
+
"--after-context",
|
|
1563
|
+
"--before-context",
|
|
1564
|
+
"--context",
|
|
1565
|
+
"--context-separator",
|
|
1566
|
+
"--field-context-separator",
|
|
1567
|
+
"--field-match-separator",
|
|
1568
|
+
"--regexp",
|
|
1569
|
+
"--file",
|
|
1570
|
+
"--glob",
|
|
1571
|
+
"--iglob",
|
|
1572
|
+
"--glob-case-insensitive",
|
|
1573
|
+
"--type",
|
|
1574
|
+
"--type-not",
|
|
1575
|
+
"--type-add",
|
|
1576
|
+
"--type-clear",
|
|
1577
|
+
"--max-count",
|
|
1578
|
+
"--max-columns",
|
|
1579
|
+
"--max-depth",
|
|
1580
|
+
"--maxdepth",
|
|
1581
|
+
"--max-filesize",
|
|
1582
|
+
"--replace",
|
|
1583
|
+
"--threads",
|
|
1584
|
+
"--encoding",
|
|
1585
|
+
"--engine",
|
|
1586
|
+
"--sort",
|
|
1587
|
+
"--sortr",
|
|
1588
|
+
"--color",
|
|
1589
|
+
"--colors",
|
|
1590
|
+
"--path-separator",
|
|
1591
|
+
"--ignore-file",
|
|
1592
|
+
"--dfa-size-limit",
|
|
1593
|
+
"--regex-size-limit",
|
|
1594
|
+
"--binary-files",
|
|
1595
|
+
"--max-columns-preview"
|
|
1596
|
+
]
|
|
1597
|
+
},
|
|
1598
|
+
grep: {
|
|
1599
|
+
cluster: true,
|
|
1600
|
+
// `-o` here is `--only-matching`, which writes nothing. It is the same two
|
|
1601
|
+
// characters as `sort -o`, which writes a file — the reason these sets are
|
|
1602
|
+
// per-binary rather than one shared list.
|
|
1603
|
+
booleans: [
|
|
1604
|
+
"-a",
|
|
1605
|
+
"-b",
|
|
1606
|
+
"-c",
|
|
1607
|
+
"-E",
|
|
1608
|
+
"-F",
|
|
1609
|
+
"-G",
|
|
1610
|
+
"-H",
|
|
1611
|
+
"-h",
|
|
1612
|
+
"-i",
|
|
1613
|
+
"-I",
|
|
1614
|
+
"-l",
|
|
1615
|
+
"-L",
|
|
1616
|
+
"-n",
|
|
1617
|
+
"-o",
|
|
1618
|
+
"-P",
|
|
1619
|
+
"-q",
|
|
1620
|
+
"-r",
|
|
1621
|
+
"-R",
|
|
1622
|
+
"-s",
|
|
1623
|
+
"-T",
|
|
1624
|
+
"-U",
|
|
1625
|
+
"-v",
|
|
1626
|
+
"-w",
|
|
1627
|
+
"-x",
|
|
1628
|
+
"-y",
|
|
1629
|
+
"-z",
|
|
1630
|
+
"-Z",
|
|
1631
|
+
"--extended-regexp",
|
|
1632
|
+
"--fixed-strings",
|
|
1633
|
+
"--basic-regexp",
|
|
1634
|
+
"--perl-regexp",
|
|
1635
|
+
"--ignore-case",
|
|
1636
|
+
"--no-ignore-case",
|
|
1637
|
+
"--invert-match",
|
|
1638
|
+
"--word-regexp",
|
|
1639
|
+
"--line-regexp",
|
|
1640
|
+
"--count",
|
|
1641
|
+
"--files-with-matches",
|
|
1642
|
+
"--files-without-match",
|
|
1643
|
+
"--line-number",
|
|
1644
|
+
"--with-filename",
|
|
1645
|
+
"--no-filename",
|
|
1646
|
+
"--only-matching",
|
|
1647
|
+
"--quiet",
|
|
1648
|
+
"--silent",
|
|
1649
|
+
"--no-messages",
|
|
1650
|
+
"--text",
|
|
1651
|
+
"--binary",
|
|
1652
|
+
"--line-buffered",
|
|
1653
|
+
"--null",
|
|
1654
|
+
"--null-data",
|
|
1655
|
+
"--byte-offset",
|
|
1656
|
+
"--recursive",
|
|
1657
|
+
"--dereference-recursive",
|
|
1658
|
+
"--initial-tab",
|
|
1659
|
+
"--unix-byte-offsets",
|
|
1660
|
+
"--no-group-separator",
|
|
1661
|
+
"--color",
|
|
1662
|
+
"--colour"
|
|
1663
|
+
],
|
|
1664
|
+
values: [
|
|
1665
|
+
"-A",
|
|
1666
|
+
"-B",
|
|
1667
|
+
"-C",
|
|
1668
|
+
"-d",
|
|
1669
|
+
"-D",
|
|
1670
|
+
"-e",
|
|
1671
|
+
"-f",
|
|
1672
|
+
"-m",
|
|
1673
|
+
"--after-context",
|
|
1674
|
+
"--before-context",
|
|
1675
|
+
"--context",
|
|
1676
|
+
"--regexp",
|
|
1677
|
+
"--file",
|
|
1678
|
+
"--max-count",
|
|
1679
|
+
"--devices",
|
|
1680
|
+
"--directories",
|
|
1681
|
+
"--binary-files",
|
|
1682
|
+
"--include",
|
|
1683
|
+
"--exclude",
|
|
1684
|
+
"--exclude-dir",
|
|
1685
|
+
"--exclude-from",
|
|
1686
|
+
"--label",
|
|
1687
|
+
"--group-separator"
|
|
1688
|
+
]
|
|
1689
|
+
},
|
|
1690
|
+
find: {
|
|
1691
|
+
// No clustering: find spells its flags as single-dash words, so walking the
|
|
1692
|
+
// characters of `-name` would read it as five short flags. And its arguments
|
|
1693
|
+
// are an expression, so `--` does not end them.
|
|
1694
|
+
expressionArgs: true,
|
|
1695
|
+
// Absent on purpose: `-exec`, `-execdir`, `-ok` and `-okdir` run an inner
|
|
1696
|
+
// command (also caught by the named guard below, which says so more
|
|
1697
|
+
// precisely), `-delete` removes files, and `-fprint`, `-fprint0`, `-fprintf`
|
|
1698
|
+
// and `-fls` are find's write family — the same write as `> file` with no
|
|
1699
|
+
// redirect for the redirect check to see.
|
|
1700
|
+
booleans: [
|
|
1701
|
+
"-H",
|
|
1702
|
+
"-L",
|
|
1703
|
+
"-P",
|
|
1704
|
+
"-a",
|
|
1705
|
+
"-and",
|
|
1706
|
+
"-o",
|
|
1707
|
+
"-or",
|
|
1708
|
+
"-not",
|
|
1709
|
+
"-d",
|
|
1710
|
+
"-depth",
|
|
1711
|
+
"-daystart",
|
|
1712
|
+
"-empty",
|
|
1713
|
+
"-executable",
|
|
1714
|
+
"-false",
|
|
1715
|
+
"-follow",
|
|
1716
|
+
"-help",
|
|
1717
|
+
"--help",
|
|
1718
|
+
"-ignore_readdir_race",
|
|
1719
|
+
"-noignore_readdir_race",
|
|
1720
|
+
"-ls",
|
|
1721
|
+
"-mount",
|
|
1722
|
+
"-nogroup",
|
|
1723
|
+
"-noleaf",
|
|
1724
|
+
"-nouser",
|
|
1725
|
+
"-print",
|
|
1726
|
+
"-print0",
|
|
1727
|
+
"-prune",
|
|
1728
|
+
"-quit",
|
|
1729
|
+
"-readable",
|
|
1730
|
+
"-true",
|
|
1731
|
+
"-version",
|
|
1732
|
+
"-writable",
|
|
1733
|
+
"-xdev"
|
|
1734
|
+
],
|
|
1735
|
+
values: [
|
|
1736
|
+
"-maxdepth",
|
|
1737
|
+
"-mindepth",
|
|
1738
|
+
"-name",
|
|
1739
|
+
"-iname",
|
|
1740
|
+
"-path",
|
|
1741
|
+
"-ipath",
|
|
1742
|
+
"-wholename",
|
|
1743
|
+
"-iwholename",
|
|
1744
|
+
"-regex",
|
|
1745
|
+
"-iregex",
|
|
1746
|
+
"-lname",
|
|
1747
|
+
"-ilname",
|
|
1748
|
+
"-type",
|
|
1749
|
+
"-xtype",
|
|
1750
|
+
"-size",
|
|
1751
|
+
"-newer",
|
|
1752
|
+
"-anewer",
|
|
1753
|
+
"-cnewer",
|
|
1754
|
+
"-mtime",
|
|
1755
|
+
"-mmin",
|
|
1756
|
+
"-ctime",
|
|
1757
|
+
"-cmin",
|
|
1758
|
+
"-atime",
|
|
1759
|
+
"-amin",
|
|
1760
|
+
"-used",
|
|
1761
|
+
"-uid",
|
|
1762
|
+
"-gid",
|
|
1763
|
+
"-user",
|
|
1764
|
+
"-group",
|
|
1765
|
+
"-perm",
|
|
1766
|
+
"-links",
|
|
1767
|
+
"-inum",
|
|
1768
|
+
"-samefile",
|
|
1769
|
+
"-fstype",
|
|
1770
|
+
"-regextype",
|
|
1771
|
+
"-files0-from",
|
|
1772
|
+
"-printf",
|
|
1773
|
+
"-context",
|
|
1774
|
+
"-D"
|
|
1775
|
+
],
|
|
1776
|
+
// `find -O2` glues the optimisation level on, and `-newermt`/`-newerat` and
|
|
1777
|
+
// friends spell the two comparison fields into the flag name.
|
|
1778
|
+
patterns: [/^-O\d$/, /^-newer[acmBt][acmBt]$/]
|
|
1779
|
+
},
|
|
1780
|
+
jq: {
|
|
1781
|
+
cluster: true,
|
|
1782
|
+
booleans: [
|
|
1783
|
+
"-a",
|
|
1784
|
+
"-c",
|
|
1785
|
+
"-C",
|
|
1786
|
+
"-e",
|
|
1787
|
+
"-h",
|
|
1788
|
+
"-j",
|
|
1789
|
+
"-M",
|
|
1790
|
+
"-n",
|
|
1791
|
+
"-r",
|
|
1792
|
+
"-R",
|
|
1793
|
+
"-s",
|
|
1794
|
+
"-S",
|
|
1795
|
+
"--ascii-output",
|
|
1796
|
+
"--compact-output",
|
|
1797
|
+
"--color-output",
|
|
1798
|
+
"--monochrome-output",
|
|
1799
|
+
"--exit-status",
|
|
1800
|
+
"--join-output",
|
|
1801
|
+
"--null-input",
|
|
1802
|
+
"--raw-input",
|
|
1803
|
+
"--raw-output",
|
|
1804
|
+
"--raw-output0",
|
|
1805
|
+
"--slurp",
|
|
1806
|
+
"--sort-keys",
|
|
1807
|
+
"--seq",
|
|
1808
|
+
"--stream",
|
|
1809
|
+
"--stream-errors",
|
|
1810
|
+
"--tab",
|
|
1811
|
+
"--unbuffered",
|
|
1812
|
+
"--args",
|
|
1813
|
+
"--jsonargs"
|
|
1814
|
+
],
|
|
1815
|
+
values: [
|
|
1816
|
+
"-f",
|
|
1817
|
+
"-L",
|
|
1818
|
+
"--arg",
|
|
1819
|
+
"--argjson",
|
|
1820
|
+
"--slurpfile",
|
|
1821
|
+
"--rawfile",
|
|
1822
|
+
"--indent",
|
|
1823
|
+
"--from-file"
|
|
1824
|
+
]
|
|
1825
|
+
},
|
|
1826
|
+
yq: {
|
|
1827
|
+
cluster: true,
|
|
1828
|
+
// `-i`/`--inplace`/`--in-place` rewrite the file, and `-s`/`--split-exp`
|
|
1829
|
+
// writes one file per document. All absent — and all bare or near-bare
|
|
1830
|
+
// booleans, which is why exempting booleans was not an option.
|
|
1831
|
+
booleans: [
|
|
1832
|
+
"-C",
|
|
1833
|
+
"-e",
|
|
1834
|
+
"-h",
|
|
1835
|
+
"-j",
|
|
1836
|
+
"-M",
|
|
1837
|
+
"-n",
|
|
1838
|
+
"-N",
|
|
1839
|
+
"-P",
|
|
1840
|
+
"-r",
|
|
1841
|
+
"-S",
|
|
1842
|
+
"-v",
|
|
1843
|
+
"-y",
|
|
1844
|
+
"-Y",
|
|
1845
|
+
"--colors",
|
|
1846
|
+
"--no-colors",
|
|
1847
|
+
"--exit-status",
|
|
1848
|
+
"--no-doc",
|
|
1849
|
+
"--null-input",
|
|
1850
|
+
"--prettyPrint",
|
|
1851
|
+
"--raw-output",
|
|
1852
|
+
"--sort-keys",
|
|
1853
|
+
"--tojson",
|
|
1854
|
+
"--yaml-output",
|
|
1855
|
+
"--yaml-roundtrip",
|
|
1856
|
+
"--unwrapScalar",
|
|
1857
|
+
"--header-preprocess",
|
|
1858
|
+
"--no-exit-status",
|
|
1859
|
+
"--verbose",
|
|
1860
|
+
"--xml-strict-mode"
|
|
1861
|
+
],
|
|
1862
|
+
values: [
|
|
1863
|
+
"-I",
|
|
1864
|
+
"-o",
|
|
1865
|
+
"-p",
|
|
1866
|
+
"-L",
|
|
1867
|
+
"--indent",
|
|
1868
|
+
"--output-format",
|
|
1869
|
+
"--input-format",
|
|
1870
|
+
"--expression",
|
|
1871
|
+
"--width",
|
|
1872
|
+
"--xml-attribute-prefix",
|
|
1873
|
+
"--xml-content-name"
|
|
1874
|
+
]
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
645
1877
|
var GIT_READONLY_SUBCOMMANDS = [
|
|
646
1878
|
"log",
|
|
647
1879
|
"status",
|
|
648
1880
|
"diff",
|
|
649
1881
|
"show",
|
|
650
1882
|
"ls-files",
|
|
651
|
-
"ls-remote",
|
|
652
1883
|
"ls-tree",
|
|
653
1884
|
"branch",
|
|
654
1885
|
"tag",
|
|
@@ -763,6 +1994,26 @@ var REFUSED_SUBCOMMANDS = {
|
|
|
763
1994
|
az: ["login", "logout"],
|
|
764
1995
|
terraform: ["apply", "destroy"]
|
|
765
1996
|
};
|
|
1997
|
+
var SHELL_KEYWORDS = [
|
|
1998
|
+
"{",
|
|
1999
|
+
"}",
|
|
2000
|
+
"if",
|
|
2001
|
+
"then",
|
|
2002
|
+
"elif",
|
|
2003
|
+
"else",
|
|
2004
|
+
"fi",
|
|
2005
|
+
"for",
|
|
2006
|
+
"while",
|
|
2007
|
+
"until",
|
|
2008
|
+
"do",
|
|
2009
|
+
"done",
|
|
2010
|
+
"case",
|
|
2011
|
+
"esac",
|
|
2012
|
+
"select",
|
|
2013
|
+
"function",
|
|
2014
|
+
"time",
|
|
2015
|
+
"export"
|
|
2016
|
+
];
|
|
766
2017
|
var INTERPRETERS = [
|
|
767
2018
|
"sh",
|
|
768
2019
|
"bash",
|
|
@@ -1206,16 +2457,79 @@ function pathLikeArgs(command, opts = {}) {
|
|
|
1206
2457
|
return !a.startsWith("-") && looksLikePath(a) ? [a] : [];
|
|
1207
2458
|
}));
|
|
1208
2459
|
}
|
|
2460
|
+
function matchFlag(spec, booleans, values, token) {
|
|
2461
|
+
if (token.startsWith("--")) {
|
|
2462
|
+
const eq = token.indexOf("=");
|
|
2463
|
+
const name = eq > 0 ? token.slice(0, eq) : token;
|
|
2464
|
+
if (booleans.has(name)) return "boolean";
|
|
2465
|
+
if (values.has(name)) return eq > 0 ? "boolean" : "value";
|
|
2466
|
+
return void 0;
|
|
2467
|
+
}
|
|
2468
|
+
if (booleans.has(token)) return "boolean";
|
|
2469
|
+
if (values.has(token)) return "value";
|
|
2470
|
+
if (spec.patterns?.some((p) => p.test(token))) return "boolean";
|
|
2471
|
+
if (!spec.cluster) return void 0;
|
|
2472
|
+
for (let i = 1; i < token.length; i++) {
|
|
2473
|
+
const short = `-${token[i]}`;
|
|
2474
|
+
if (booleans.has(short)) continue;
|
|
2475
|
+
if (values.has(short)) return i === token.length - 1 ? "value" : "boolean";
|
|
2476
|
+
return void 0;
|
|
2477
|
+
}
|
|
2478
|
+
return "boolean";
|
|
2479
|
+
}
|
|
2480
|
+
function takesNextToken(next) {
|
|
2481
|
+
if (next === void 0) return false;
|
|
2482
|
+
return !/^-./.test(next) || /^-\d/.test(next);
|
|
2483
|
+
}
|
|
2484
|
+
function flagLabel(token) {
|
|
2485
|
+
const eq = token.startsWith("--") ? token.indexOf("=") : -1;
|
|
2486
|
+
return eq > 0 ? token.slice(0, eq) : token;
|
|
2487
|
+
}
|
|
2488
|
+
function scanFlags(seg) {
|
|
2489
|
+
const spec = FLAG_POLICY[seg.binary];
|
|
2490
|
+
if (!spec || spec.freeform) return void 0;
|
|
2491
|
+
const booleans = /* @__PURE__ */ new Set([...spec.booleans ?? [], ...UNIVERSAL_FLAGS]);
|
|
2492
|
+
const values = new Set(spec.values ?? []);
|
|
2493
|
+
const operands = [];
|
|
2494
|
+
let i = 0;
|
|
2495
|
+
while (i < seg.args.length) {
|
|
2496
|
+
const token = seg.args[i];
|
|
2497
|
+
if (token === "--" && !spec.expressionArgs) {
|
|
2498
|
+
operands.push(...seg.args.slice(i + 1));
|
|
2499
|
+
break;
|
|
2500
|
+
}
|
|
2501
|
+
if (!/^-./.test(token)) {
|
|
2502
|
+
operands.push(token);
|
|
2503
|
+
i++;
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
const shape = matchFlag(spec, booleans, values, token);
|
|
2507
|
+
if (shape === void 0) return { unknown: flagLabel(token), operands };
|
|
2508
|
+
i += shape === "value" && takesNextToken(seg.args[i + 1]) ? 2 : 1;
|
|
2509
|
+
}
|
|
2510
|
+
return { operands };
|
|
2511
|
+
}
|
|
2512
|
+
function subcommandOf(seg) {
|
|
2513
|
+
const scan = scanFlags(seg);
|
|
2514
|
+
if (scan) return scan.operands[0];
|
|
2515
|
+
return seg.args.find((a) => !a.startsWith("-"));
|
|
2516
|
+
}
|
|
2517
|
+
var SCOPE_LEAD_ARGS = 2;
|
|
1209
2518
|
function scopeFor(seg) {
|
|
1210
2519
|
if (!MULTIPLEXERS.includes(seg.binary)) return seg.binary;
|
|
1211
|
-
const
|
|
1212
|
-
|
|
2520
|
+
const lead = [];
|
|
2521
|
+
for (const arg of seg.args) {
|
|
2522
|
+
if (arg.startsWith("-")) break;
|
|
2523
|
+
lead.push(arg);
|
|
2524
|
+
if (lead.length === SCOPE_LEAD_ARGS) break;
|
|
2525
|
+
}
|
|
2526
|
+
return [seg.binary, ...lead].join(" ");
|
|
1213
2527
|
}
|
|
1214
2528
|
function isAuto(seg) {
|
|
1215
2529
|
if (!AUTO_COMMANDS.includes(seg.binary)) return false;
|
|
1216
2530
|
if (seg.expandable) return false;
|
|
1217
2531
|
if (seg.binary !== "git") return true;
|
|
1218
|
-
const sub = seg
|
|
2532
|
+
const sub = subcommandOf(seg);
|
|
1219
2533
|
return sub !== void 0 && GIT_READONLY_SUBCOMMANDS.includes(sub);
|
|
1220
2534
|
}
|
|
1221
2535
|
function refusalFor(seg) {
|
|
@@ -1225,29 +2539,31 @@ function refusalFor(seg) {
|
|
|
1225
2539
|
if (seg.binary === "eval" || seg.binary === "exec") {
|
|
1226
2540
|
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.`;
|
|
1227
2541
|
}
|
|
2542
|
+
if (seg.binary === "source" || seg.binary === ".") {
|
|
2543
|
+
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.`;
|
|
2544
|
+
}
|
|
2545
|
+
if (SHELL_KEYWORDS.includes(seg.binary)) {
|
|
2546
|
+
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.`;
|
|
2547
|
+
}
|
|
1228
2548
|
const subs = REFUSED_SUBCOMMANDS[seg.binary];
|
|
1229
2549
|
if (subs) {
|
|
1230
|
-
const sub = seg
|
|
2550
|
+
const sub = subcommandOf(seg);
|
|
1231
2551
|
if (sub && subs.includes(sub)) {
|
|
1232
2552
|
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.`;
|
|
1233
2553
|
}
|
|
1234
2554
|
}
|
|
1235
2555
|
if (seg.binary === "git") {
|
|
1236
|
-
const sub = seg
|
|
2556
|
+
const sub = subcommandOf(seg);
|
|
1237
2557
|
if (sub === "branch" || sub === "tag") {
|
|
1238
2558
|
const flag = seg.args.find((a) => ["-D", "-d", "-m", "-M", "--delete", "--move"].includes(a));
|
|
1239
2559
|
if (flag) {
|
|
1240
2560
|
return `"git ${sub} ${flag}" mutates refs. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
1241
2561
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
if (sub === "grep") {
|
|
1248
|
-
const pagerFlag = seg.args.find((a) => a === "-O" || a === "--open-files-in-pager" || a.startsWith("--open-files-in-pager="));
|
|
1249
|
-
if (pagerFlag) {
|
|
1250
|
-
return `"git grep ${pagerFlag}" opens the results in an arbitrary named program. Use the read-only research tools, or describe the change as a task.`;
|
|
2562
|
+
const listing = seg.args.includes("-l") || seg.args.includes("--list");
|
|
2563
|
+
const scan2 = scanFlags(seg);
|
|
2564
|
+
const target = scan2?.operands[1];
|
|
2565
|
+
if (!listing && target) {
|
|
2566
|
+
return `"git ${sub} ${target}" creates or moves a ref. You are a read-only planner \u2014 describe the change as a task instead.`;
|
|
1251
2567
|
}
|
|
1252
2568
|
}
|
|
1253
2569
|
}
|
|
@@ -1275,6 +2591,13 @@ function refusalFor(seg) {
|
|
|
1275
2591
|
if (seg.assignments.length > 0) {
|
|
1276
2592
|
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.`;
|
|
1277
2593
|
}
|
|
2594
|
+
const scan = scanFlags(seg);
|
|
2595
|
+
if (scan?.unknown) {
|
|
2596
|
+
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.`;
|
|
2597
|
+
}
|
|
2598
|
+
if (seg.binary === "uniq" && scan && scan.operands.length > 1) {
|
|
2599
|
+
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.`;
|
|
2600
|
+
}
|
|
1278
2601
|
return void 0;
|
|
1279
2602
|
}
|
|
1280
2603
|
function withWrapperNote(reason, wrappers) {
|
|
@@ -1661,6 +2984,139 @@ It is outside the auto-allowed read-only set (${scope}). Continue with the read-
|
|
|
1661
2984
|
}
|
|
1662
2985
|
};
|
|
1663
2986
|
|
|
2987
|
+
// src/utils/redactSecrets.ts
|
|
2988
|
+
var REDACTION_MARKER = "REDACTED";
|
|
2989
|
+
function marker(kind, length) {
|
|
2990
|
+
return `[${REDACTION_MARKER} ${kind}: ${length} chars]`;
|
|
2991
|
+
}
|
|
2992
|
+
var LINE_PREFIX = String.raw`(?:[ \t]*\d+\||[^\s:]*:\d+:|)[ \t]*`;
|
|
2993
|
+
var LINE_PREFIX_ANCHORED = new RegExp("^" + LINE_PREFIX);
|
|
2994
|
+
var CREDENTIAL_WORD = String.raw`(?:api[_-]?keys?|secrets?|passwords?|passwd|passphrase|tokens?|access[_-]?keys?|private[_-]?keys?|client[_-]?secrets?|credentials?|auth)(?![a-z])`;
|
|
2995
|
+
var NAMED_CREDENTIAL = new RegExp(
|
|
2996
|
+
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`,
|
|
2997
|
+
"gi"
|
|
2998
|
+
);
|
|
2999
|
+
var DOTENV_CREDENTIAL = new RegExp(
|
|
3000
|
+
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]*$`,
|
|
3001
|
+
"gm"
|
|
3002
|
+
);
|
|
3003
|
+
var PROVIDER_KEY_PATTERNS = [
|
|
3004
|
+
// OpenAI / Anthropic / OpenRouter and the many `sk-` derivatives
|
|
3005
|
+
/(?<![A-Za-z0-9])sk-(?:ant-|proj-|or-v1-|live-|test-)?[A-Za-z0-9_-]{20,}/g,
|
|
3006
|
+
// GitHub personal-access, OAuth, app and refresh tokens
|
|
3007
|
+
/(?<![A-Za-z0-9])(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{22,}/g,
|
|
3008
|
+
// Slack bot / user / app tokens
|
|
3009
|
+
/(?<![A-Za-z0-9])xox[abprse]-[A-Za-z0-9-]{10,}/g,
|
|
3010
|
+
// Stripe secret and restricted keys
|
|
3011
|
+
/(?<![A-Za-z0-9])[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g,
|
|
3012
|
+
// Google API keys and OAuth access tokens
|
|
3013
|
+
/(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{30,}/g,
|
|
3014
|
+
/(?<![A-Za-z0-9])ya29\.[A-Za-z0-9_-]{20,}/g,
|
|
3015
|
+
// npm, Hugging Face, Groq, GitLab, DigitalOcean, Shopify
|
|
3016
|
+
/(?<![A-Za-z0-9])(?:npm|hf|gsk|glpat|dop_v1|shpat|shpss)[_-][A-Za-z0-9]{20,}/g,
|
|
3017
|
+
/(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}/g,
|
|
3018
|
+
// Three-segment JWTs, whose payload segment is base64 for `{"`
|
|
3019
|
+
/(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g
|
|
3020
|
+
];
|
|
3021
|
+
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;
|
|
3022
|
+
var PEM_BEGIN = /-----BEGIN ((?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?)-----/;
|
|
3023
|
+
var PEM_BODY_LINE = /^[A-Za-z0-9+/=:,.\- \t]*$/;
|
|
3024
|
+
function redactPrivateKeyBlocks(text) {
|
|
3025
|
+
if (!PEM_BEGIN.test(text)) return text;
|
|
3026
|
+
const out = [];
|
|
3027
|
+
let label = null;
|
|
3028
|
+
let bodyChars = 0;
|
|
3029
|
+
const closeBlock = () => {
|
|
3030
|
+
if (label === null) return;
|
|
3031
|
+
out.push(marker("private key", bodyChars));
|
|
3032
|
+
label = null;
|
|
3033
|
+
bodyChars = 0;
|
|
3034
|
+
};
|
|
3035
|
+
for (const line of text.split("\n")) {
|
|
3036
|
+
if (label === null) {
|
|
3037
|
+
out.push(line);
|
|
3038
|
+
const begun = PEM_BEGIN.exec(line);
|
|
3039
|
+
if (begun) label = begun[1];
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3042
|
+
const content = line.replace(LINE_PREFIX_ANCHORED, "");
|
|
3043
|
+
if (content.includes(`-----END ${label}-----`)) {
|
|
3044
|
+
closeBlock();
|
|
3045
|
+
out.push(line);
|
|
3046
|
+
} else if (PEM_BODY_LINE.test(content)) {
|
|
3047
|
+
bodyChars += content.trim().length;
|
|
3048
|
+
} else {
|
|
3049
|
+
closeBlock();
|
|
3050
|
+
out.push(line);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
closeBlock();
|
|
3054
|
+
return out.join("\n");
|
|
3055
|
+
}
|
|
3056
|
+
var NUMERIC = /^[-+]?\d+(?:\.\d+)?$/;
|
|
3057
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
3058
|
+
var MEMBER_EXPRESSION = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)+$/;
|
|
3059
|
+
var SLUG = /^[a-z]+(?:[-_][a-z]+)+$/;
|
|
3060
|
+
var PATHISH = /^(?:\.{0,2}\/|~\/)/;
|
|
3061
|
+
var URLISH = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
3062
|
+
var CODE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
3063
|
+
"true",
|
|
3064
|
+
"false",
|
|
3065
|
+
"null",
|
|
3066
|
+
"none",
|
|
3067
|
+
"nil",
|
|
3068
|
+
"undefined",
|
|
3069
|
+
"void",
|
|
3070
|
+
"string",
|
|
3071
|
+
"number",
|
|
3072
|
+
"boolean",
|
|
3073
|
+
"object",
|
|
3074
|
+
"any",
|
|
3075
|
+
"unknown",
|
|
3076
|
+
"bool",
|
|
3077
|
+
"str",
|
|
3078
|
+
"int"
|
|
3079
|
+
]);
|
|
3080
|
+
var UNAMBIGUOUS_CREDENTIAL_NAME = /(?:password|passwd|passphrase|secret|credentials?|private[_-]?key)/i;
|
|
3081
|
+
function looksLikeKeyMaterial(value) {
|
|
3082
|
+
const hasDigit = /\d/.test(value);
|
|
3083
|
+
const hasAlpha = /[A-Za-z]/.test(value);
|
|
3084
|
+
const mixedCase = /[a-z]/.test(value) && /[A-Z]/.test(value);
|
|
3085
|
+
return hasDigit && hasAlpha || mixedCase || value.length >= 24;
|
|
3086
|
+
}
|
|
3087
|
+
function looksLikeCode(value) {
|
|
3088
|
+
return NUMERIC.test(value) || CODE_KEYWORDS.has(value.toLowerCase()) || MEMBER_EXPRESSION.test(value) || PATHISH.test(value) || URLISH.test(value);
|
|
3089
|
+
}
|
|
3090
|
+
function trimTrailingPunctuation(value) {
|
|
3091
|
+
const match = /[.:!]+$/.exec(value);
|
|
3092
|
+
if (!match || match.index === 0) return { value, tail: "" };
|
|
3093
|
+
return { value: value.slice(0, match.index), tail: match[0] };
|
|
3094
|
+
}
|
|
3095
|
+
function redactSecrets(text) {
|
|
3096
|
+
if (!text) return text;
|
|
3097
|
+
let out = redactPrivateKeyBlocks(text);
|
|
3098
|
+
out = out.replace(DOTENV_CREDENTIAL, (full, head, quote, value) => {
|
|
3099
|
+
if (looksLikeCode(value) || value.startsWith("$")) return full;
|
|
3100
|
+
return `${head}${quote}${marker("credential", value.length)}${quote}`;
|
|
3101
|
+
});
|
|
3102
|
+
out = out.replace(NAMED_CREDENTIAL, (full, head, quote, raw) => {
|
|
3103
|
+
const { value, tail } = trimTrailingPunctuation(raw);
|
|
3104
|
+
if (value.length < 8) return full;
|
|
3105
|
+
if (looksLikeCode(value) || value.startsWith("$")) return full;
|
|
3106
|
+
if (UNAMBIGUOUS_CREDENTIAL_NAME.test(head) && /\d/.test(value)) {
|
|
3107
|
+
return `${head}${quote}${marker("credential", value.length)}${quote}${tail}`;
|
|
3108
|
+
}
|
|
3109
|
+
if (IDENTIFIER.test(value) || SLUG.test(value)) return full;
|
|
3110
|
+
if (!UNAMBIGUOUS_CREDENTIAL_NAME.test(head) && !looksLikeKeyMaterial(value)) return full;
|
|
3111
|
+
return `${head}${quote}${marker("credential", value.length)}${quote}${tail}`;
|
|
3112
|
+
});
|
|
3113
|
+
for (const pattern of PROVIDER_KEY_PATTERNS) {
|
|
3114
|
+
out = out.replace(pattern, (hit) => marker("api key", hit.length));
|
|
3115
|
+
}
|
|
3116
|
+
out = out.replace(CLOUD_ACCESS_KEY_ID, (hit) => marker("access key id", hit.length));
|
|
3117
|
+
return out;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
1664
3120
|
// src/services/executeTool.ts
|
|
1665
3121
|
var GREP_OUTPUT_MODES = ["content", "files", "count"];
|
|
1666
3122
|
function num(value) {
|
|
@@ -1687,6 +3143,10 @@ function grepOptions(args) {
|
|
|
1687
3143
|
}
|
|
1688
3144
|
var STOPPED_TOOL_RESULT = "Stopped by the user \u2014 this call was not executed, and no further tool calls will run this turn.";
|
|
1689
3145
|
async function executeTool(name, args, fs10, fetcher, signal) {
|
|
3146
|
+
const outcome = await runTool(name, args, fs10, fetcher, signal);
|
|
3147
|
+
return { ...outcome, output: redactSecrets(outcome.output) };
|
|
3148
|
+
}
|
|
3149
|
+
async function runTool(name, args, fs10, fetcher, signal) {
|
|
1690
3150
|
if (signal?.aborted) return { success: false, output: STOPPED_TOOL_RESULT, truncated: false };
|
|
1691
3151
|
switch (name) {
|
|
1692
3152
|
case "read_file": {
|
|
@@ -2787,7 +4247,7 @@ function formatError(err) {
|
|
|
2787
4247
|
var defaultLogger = new ConsoleLogger();
|
|
2788
4248
|
|
|
2789
4249
|
// src/services/BaseAiService.ts
|
|
2790
|
-
var
|
|
4250
|
+
var import_uuid3 = require("uuid");
|
|
2791
4251
|
|
|
2792
4252
|
// src/services/ModeResolver.ts
|
|
2793
4253
|
function runnerModesFrom(registry, runners) {
|
|
@@ -2910,61 +4370,376 @@ function modesFor(scope, modes) {
|
|
|
2910
4370
|
return scoped;
|
|
2911
4371
|
}
|
|
2912
4372
|
|
|
2913
|
-
// src/services/
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
return lines.join("\n");
|
|
2940
|
-
}
|
|
2941
|
-
function buildSubagentSystemPrompt() {
|
|
2942
|
-
return [
|
|
2943
|
-
"You are a read-only research subagent working for a project planner. You receive one self-contained research task, explore the workspace with your tools, and reply with a single digest. You never modify files, never fetch URLs, and cannot spawn further agents.",
|
|
2944
|
-
"",
|
|
2945
|
-
"Work efficiently: batch independent tool calls in one reply; prefer glob/grep to locate, then read only what matters. Your tool budget is small \u2014 stop exploring when you can answer the task.",
|
|
2946
|
-
"",
|
|
2947
|
-
"Your final reply IS the digest \u2014 it goes straight back to the planner, not to a human. It must contain:",
|
|
2948
|
-
"- The exact file paths (and key line references) relevant to the task",
|
|
2949
|
-
"- The key symbols/functions involved and how they connect",
|
|
2950
|
-
"- Constraints, gotchas, or conventions the planner must respect",
|
|
2951
|
-
"- Direct answers to every question the task asked",
|
|
2952
|
-
"Plain prose/markdown only. No questions, no task plans, no JSON. Be dense and specific; vague summaries are useless to the planner."
|
|
2953
|
-
].join("\n");
|
|
2954
|
-
}
|
|
2955
|
-
function buildModeGuideForRunners(runners, _autonomousDefault) {
|
|
2956
|
-
const lines = ['Assign a "taskMode" to each AI task based on the runner:'];
|
|
2957
|
-
for (const r of runners) {
|
|
2958
|
-
if (r === "claude-code") {
|
|
2959
|
-
lines.push('- For claude-code: "acceptEdits" (edit automatically, recommended), "default" (ask before edits), "plan" (read-only analysis), "bypassPermissions" (skip all prompts, CI only).');
|
|
2960
|
-
} else if (r === "opencode") {
|
|
2961
|
-
lines.push('- For opencode: "build" (full access agent), "plan" (read-only analysis).');
|
|
2962
|
-
} else {
|
|
2963
|
-
lines.push(`- For ${r}: use the modes from the runner's manifest.`);
|
|
2964
|
-
}
|
|
4373
|
+
// src/services/JsonExtractor.ts
|
|
4374
|
+
var PLAN_ENVELOPE_KEY = "tasks";
|
|
4375
|
+
var TASK_OPS_ENVELOPE_KEY = "taskOps";
|
|
4376
|
+
var TASK_QUERY_ENVELOPE_KEY = "taskQuery";
|
|
4377
|
+
var PlanParseError = class extends Error {
|
|
4378
|
+
raw;
|
|
4379
|
+
/**
|
|
4380
|
+
* True when the reply was cut off mid-object (output-token limit) rather
|
|
4381
|
+
* than malformed — the two need different repairs: a truncated emission will
|
|
4382
|
+
* be cut at the same point again unless the retry frees context or asks for
|
|
4383
|
+
* terser output.
|
|
4384
|
+
*/
|
|
4385
|
+
truncated;
|
|
4386
|
+
/**
|
|
4387
|
+
* True when the JSON parsed fine and a *shape* rule rejected it. The generic
|
|
4388
|
+
* "could not be parsed as JSON" corrective is a lie for these, so the model
|
|
4389
|
+
* re-sends the same object; a semantic rejection must be repaired by naming
|
|
4390
|
+
* the rule it broke.
|
|
4391
|
+
*/
|
|
4392
|
+
semantic;
|
|
4393
|
+
constructor(message, raw, opts) {
|
|
4394
|
+
super(message);
|
|
4395
|
+
this.name = "PlanParseError";
|
|
4396
|
+
this.raw = raw;
|
|
4397
|
+
this.truncated = opts?.truncated ?? false;
|
|
4398
|
+
this.semantic = opts?.semantic ?? false;
|
|
2965
4399
|
}
|
|
2966
|
-
|
|
2967
|
-
|
|
4400
|
+
};
|
|
4401
|
+
function stripModelNoise(raw) {
|
|
4402
|
+
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
|
|
4403
|
+
}
|
|
4404
|
+
function balancedObjectAt(text, start) {
|
|
4405
|
+
let depth = 0;
|
|
4406
|
+
let inString = false;
|
|
4407
|
+
let escaped = false;
|
|
4408
|
+
for (let i = start; i < text.length; i++) {
|
|
4409
|
+
const ch = text[i];
|
|
4410
|
+
if (inString) {
|
|
4411
|
+
if (escaped) escaped = false;
|
|
4412
|
+
else if (ch === "\\") escaped = true;
|
|
4413
|
+
else if (ch === '"') inString = false;
|
|
4414
|
+
continue;
|
|
4415
|
+
}
|
|
4416
|
+
if (ch === '"') inString = true;
|
|
4417
|
+
else if (ch === "{") depth++;
|
|
4418
|
+
else if (ch === "}") {
|
|
4419
|
+
depth--;
|
|
4420
|
+
if (depth === 0) return { json: text.slice(start, i + 1), balanced: true };
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
return { json: text.slice(start), balanced: false };
|
|
4424
|
+
}
|
|
4425
|
+
function extractObjectsWithKey(raw, key) {
|
|
4426
|
+
const stripped = stripModelNoise(raw);
|
|
4427
|
+
const keyRe = new RegExp(`"${key}"\\s*:`);
|
|
4428
|
+
const matches = [];
|
|
4429
|
+
let firstObject = null;
|
|
4430
|
+
let sawBrace = false;
|
|
4431
|
+
let sawUnbalanced = false;
|
|
4432
|
+
let inString = false;
|
|
4433
|
+
let escaped = false;
|
|
4434
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
4435
|
+
const ch = stripped[i];
|
|
4436
|
+
if (inString) {
|
|
4437
|
+
if (escaped) escaped = false;
|
|
4438
|
+
else if (ch === "\\") escaped = true;
|
|
4439
|
+
else if (ch === '"') inString = false;
|
|
4440
|
+
continue;
|
|
4441
|
+
}
|
|
4442
|
+
if (ch === '"') {
|
|
4443
|
+
inString = true;
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4446
|
+
if (ch !== "{") continue;
|
|
4447
|
+
sawBrace = true;
|
|
4448
|
+
const obj = balancedObjectAt(stripped, i);
|
|
4449
|
+
if (obj.balanced && keyRe.test(obj.json)) {
|
|
4450
|
+
matches.push(obj.json);
|
|
4451
|
+
i += obj.json.length - 1;
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
if (!obj.balanced) sawUnbalanced = true;
|
|
4455
|
+
if (!firstObject && obj.balanced) firstObject = obj.json;
|
|
4456
|
+
}
|
|
4457
|
+
return {
|
|
4458
|
+
matches,
|
|
4459
|
+
fallback: firstObject !== null ? { json: firstObject, balanced: true } : { json: stripped, balanced: false },
|
|
4460
|
+
sawBrace,
|
|
4461
|
+
sawUnbalanced
|
|
4462
|
+
};
|
|
4463
|
+
}
|
|
4464
|
+
function extractObjectWithBalance(raw) {
|
|
4465
|
+
const { matches, fallback, sawBrace } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
4466
|
+
if (matches.length > 0) return { json: matches[0], balanced: true, sawBrace };
|
|
4467
|
+
return { ...fallback, sawBrace };
|
|
4468
|
+
}
|
|
4469
|
+
function extractJsonObject(raw) {
|
|
4470
|
+
return extractObjectWithBalance(raw).json;
|
|
4471
|
+
}
|
|
4472
|
+
function stripTrailingCommas(json) {
|
|
4473
|
+
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
4474
|
+
}
|
|
4475
|
+
function escapeControlCharsInStrings(json) {
|
|
4476
|
+
let out = "";
|
|
4477
|
+
let inString = false;
|
|
4478
|
+
let escaped = false;
|
|
4479
|
+
for (const ch of json) {
|
|
4480
|
+
if (inString) {
|
|
4481
|
+
if (escaped) {
|
|
4482
|
+
out += ch;
|
|
4483
|
+
escaped = false;
|
|
4484
|
+
continue;
|
|
4485
|
+
}
|
|
4486
|
+
if (ch === "\\") {
|
|
4487
|
+
out += ch;
|
|
4488
|
+
escaped = true;
|
|
4489
|
+
continue;
|
|
4490
|
+
}
|
|
4491
|
+
if (ch === '"') {
|
|
4492
|
+
out += ch;
|
|
4493
|
+
inString = false;
|
|
4494
|
+
continue;
|
|
4495
|
+
}
|
|
4496
|
+
if (ch === "\n") {
|
|
4497
|
+
out += "\\n";
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
if (ch === "\r") {
|
|
4501
|
+
out += "\\r";
|
|
4502
|
+
continue;
|
|
4503
|
+
}
|
|
4504
|
+
if (ch === " ") {
|
|
4505
|
+
out += "\\t";
|
|
4506
|
+
continue;
|
|
4507
|
+
}
|
|
4508
|
+
out += ch;
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
if (ch === '"') inString = true;
|
|
4512
|
+
out += ch;
|
|
4513
|
+
}
|
|
4514
|
+
return out;
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
// src/services/TaskQuery.ts
|
|
4518
|
+
var TASK_QUERY_FIELDS = [
|
|
4519
|
+
"description",
|
|
4520
|
+
"prompt",
|
|
4521
|
+
"userSteps",
|
|
4522
|
+
"verdict",
|
|
4523
|
+
"outputSummary",
|
|
4524
|
+
"userStoriesCovered"
|
|
4525
|
+
];
|
|
4526
|
+
function textHasTaskQuery(text) {
|
|
4527
|
+
return text.includes(`"${TASK_QUERY_ENVELOPE_KEY}"`);
|
|
4528
|
+
}
|
|
4529
|
+
function parseTaskQueryJson(text) {
|
|
4530
|
+
const { matches, fallback } = extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY);
|
|
4531
|
+
const candidates = matches.length > 0 ? [...matches].reverse() : [fallback.json];
|
|
4532
|
+
let lastError = null;
|
|
4533
|
+
for (const json of candidates) {
|
|
4534
|
+
try {
|
|
4535
|
+
return parseTaskQueryObject(json, text);
|
|
4536
|
+
} catch (err) {
|
|
4537
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
4538
|
+
if (!lastError) lastError = err;
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
throw lastError ?? new PlanParseError("No JSON object found in task-query reply", text);
|
|
4542
|
+
}
|
|
4543
|
+
function parseTaskQueryObject(json, text) {
|
|
4544
|
+
let parsed;
|
|
4545
|
+
try {
|
|
4546
|
+
parsed = JSON.parse(escapeControlCharsInStrings(stripTrailingCommas(json)));
|
|
4547
|
+
} catch (err) {
|
|
4548
|
+
throw new PlanParseError(`Task-query JSON is invalid: ${err instanceof Error ? err.message : String(err)}`, text);
|
|
4549
|
+
}
|
|
4550
|
+
const raw = parsed.taskQuery;
|
|
4551
|
+
const body = Array.isArray(raw) ? { tasks: raw } : raw;
|
|
4552
|
+
if (typeof body !== "object" || body === null) {
|
|
4553
|
+
throw new PlanParseError('"taskQuery" must be an object (or an array of task references)', text);
|
|
4554
|
+
}
|
|
4555
|
+
const { tasks: rawTasks, fields: rawFields, catalog: rawCatalog } = body;
|
|
4556
|
+
const tasks = [];
|
|
4557
|
+
if (rawTasks !== void 0) {
|
|
4558
|
+
if (!Array.isArray(rawTasks)) throw new PlanParseError('"tasks" in a taskQuery must be an array of task references', text);
|
|
4559
|
+
for (const ref of rawTasks) {
|
|
4560
|
+
if (typeof ref !== "string" && typeof ref !== "number") {
|
|
4561
|
+
throw new PlanParseError(`Invalid task reference "${String(ref)}" in taskQuery`, text);
|
|
4562
|
+
}
|
|
4563
|
+
tasks.push(String(ref).trim());
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
let fields;
|
|
4567
|
+
if (rawFields !== void 0) {
|
|
4568
|
+
if (!Array.isArray(rawFields)) throw new PlanParseError('"fields" in a taskQuery must be an array of field names', text);
|
|
4569
|
+
fields = [];
|
|
4570
|
+
for (const f of rawFields) {
|
|
4571
|
+
if (typeof f !== "string" || !TASK_QUERY_FIELDS.includes(f)) {
|
|
4572
|
+
throw new PlanParseError(`Unknown taskQuery field "${String(f)}" \u2014 choose from ${TASK_QUERY_FIELDS.join(", ")}`, text);
|
|
4573
|
+
}
|
|
4574
|
+
fields.push(f);
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
const catalog = rawCatalog === true;
|
|
4578
|
+
if (tasks.length === 0 && !catalog) {
|
|
4579
|
+
throw new PlanParseError('A taskQuery must name at least one task or set "catalog": true', text);
|
|
4580
|
+
}
|
|
4581
|
+
return { tasks, fields, catalog };
|
|
4582
|
+
}
|
|
4583
|
+
function taskQuerySignature(query) {
|
|
4584
|
+
return JSON.stringify([query.tasks, query.fields ?? null, query.catalog]);
|
|
4585
|
+
}
|
|
4586
|
+
var TASK_QUERY_ANSWER_OR_OPS = "You have now read everything you asked for. Do not send another taskQuery this turn: answer the user in prose, or emit the taskOps JSON for the change you came to make.";
|
|
4587
|
+
var TASK_QUERY_PROTOCOL = [
|
|
4588
|
+
"READING A TASK BEFORE YOU EDIT IT:",
|
|
4589
|
+
"The plan block you are shown each turn carries only short fields \u2014 it never contains a task's prompt, its user steps, or a completed task's verdict. Never rewrite a field you have not read. To read one, reply with ONLY this JSON object:",
|
|
4590
|
+
` {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>", "..."],"fields":[${TASK_QUERY_FIELDS.map((f) => `"${f}"`).join(", ")}],"catalog":true}}`,
|
|
4591
|
+
'- "tasks": one or more task references. Ask for everything you need in ONE query \u2014 each query costs a round-trip.',
|
|
4592
|
+
'- "fields": optional. Omit it to get every field above.',
|
|
4593
|
+
'- "catalog": optional. Set it to true for the full runner and model catalog \u2014 every model id with its label and thinking-effort variants, and every task mode with its description. It works before any task exists, and may be used on its own.',
|
|
4594
|
+
"Ordewell answers immediately with the detail and changes nothing. Then reply again with your taskOps JSON, or with prose for the user.",
|
|
4595
|
+
"Three queries per user message; after that every answer also tells you to land the turn. Do not ask the same question twice."
|
|
4596
|
+
];
|
|
4597
|
+
var TASK_QUERY_REMINDER = `- To READ a task in full (prompt, user steps, verdict, output, user stories) or the whole model/mode catalog before editing, reply with ONLY {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"catalog":true}} \u2014 it changes nothing, and you then reply again with your ops.`;
|
|
4598
|
+
function findTask(tasks, ref) {
|
|
4599
|
+
const byId = tasks.find((t) => t.id === ref);
|
|
4600
|
+
if (byId) return byId;
|
|
4601
|
+
const orderStr = ref.startsWith("#") ? ref.slice(1) : ref;
|
|
4602
|
+
if (/^\d+$/.test(orderStr)) {
|
|
4603
|
+
const byOrder = tasks.filter((t) => t.order === Number(orderStr));
|
|
4604
|
+
if (byOrder.length === 1) return byOrder[0];
|
|
4605
|
+
}
|
|
4606
|
+
return tasks.find((t) => t.title === ref);
|
|
4607
|
+
}
|
|
4608
|
+
function renderField(task, field) {
|
|
4609
|
+
switch (field) {
|
|
4610
|
+
case "description":
|
|
4611
|
+
return [`description: ${task.description || "(none)"}`];
|
|
4612
|
+
case "prompt":
|
|
4613
|
+
return task.prompt ? ["prompt:", task.prompt] : ["prompt: (none)"];
|
|
4614
|
+
case "userSteps": {
|
|
4615
|
+
const steps = task.userSteps ?? [];
|
|
4616
|
+
if (steps.length === 0) return ["userSteps: (none)"];
|
|
4617
|
+
return ["userSteps:", ...steps.map((s) => ` ${s.order}. [${s.completed ? "x" : " "}] ${s.instruction}`)];
|
|
4618
|
+
}
|
|
4619
|
+
case "verdict": {
|
|
4620
|
+
const v = task.verdict;
|
|
4621
|
+
if (!v) return ["verdict: (none)"];
|
|
4622
|
+
const checks = v.checks.map((c) => `${c.name}=${c.skipped ? "skipped" : c.passed ? "pass" : "fail"}`).join(", ");
|
|
4623
|
+
return [`verdict: ${v.outcome.toUpperCase()} \u2014 ${v.reason}${checks ? ` [${checks}]` : ""}`];
|
|
4624
|
+
}
|
|
4625
|
+
case "outputSummary": {
|
|
4626
|
+
const o = task.outputSummary;
|
|
4627
|
+
if (!o) return ["outputSummary: (none)"];
|
|
4628
|
+
return [`outputSummary: ${o.reviewReason}`, ...o.logTail ? ["log tail:", o.logTail] : []];
|
|
4629
|
+
}
|
|
4630
|
+
case "userStoriesCovered": {
|
|
4631
|
+
const stories = task.userStoriesCovered ?? [];
|
|
4632
|
+
if (stories.length === 0) return ["userStoriesCovered: (none)"];
|
|
4633
|
+
return ["userStoriesCovered:", ...stories.map((s) => ` - ${s}`)];
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
function renderTask(task, fields) {
|
|
4638
|
+
return [
|
|
4639
|
+
`#${task.order} id=${task.id} "${task.title}" [${task.status}] type:${task.type === "user" ? "MAN" : "AI"}`,
|
|
4640
|
+
...fields.flatMap((f) => renderField(task, f)),
|
|
4641
|
+
""
|
|
4642
|
+
];
|
|
4643
|
+
}
|
|
4644
|
+
function renderCatalog(catalog) {
|
|
4645
|
+
const lines = [];
|
|
4646
|
+
for (const runner of catalog.runners) {
|
|
4647
|
+
const models = catalog.models[runner] ?? [];
|
|
4648
|
+
lines.push(`${runner} models:`);
|
|
4649
|
+
if (models.length === 0) lines.push(" (none discovered)");
|
|
4650
|
+
for (const m of models) {
|
|
4651
|
+
const variants = m.variants?.length ? ` variants: ${m.variants.map((v) => `${v.id}${v.label && v.label !== v.id ? ` (${v.label})` : ""}`).join(", ")}` : " variants: none";
|
|
4652
|
+
lines.push(` - ${m.modelId} \u2014 ${m.modelLabel}${variants}`);
|
|
4653
|
+
}
|
|
4654
|
+
const modes = catalog.modes[runner] ?? [];
|
|
4655
|
+
lines.push(`${runner} task modes:`);
|
|
4656
|
+
if (modes.length === 0) lines.push(" (none declared)");
|
|
4657
|
+
const defaultId = resolveDefaultMode(modes, catalog.autonomousDefault);
|
|
4658
|
+
for (const mode of modes) {
|
|
4659
|
+
lines.push(` - ${mode.id} \u2014 ${mode.label}: ${mode.description}${mode.id === defaultId ? " (default)" : ""}`);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
return lines;
|
|
4663
|
+
}
|
|
4664
|
+
function renderTaskQueryAnswer(query, tasks, catalog) {
|
|
4665
|
+
const fields = query.fields ?? TASK_QUERY_FIELDS;
|
|
4666
|
+
const blocks = [];
|
|
4667
|
+
if (query.tasks.length > 0) {
|
|
4668
|
+
const body = [];
|
|
4669
|
+
for (const ref of query.tasks) {
|
|
4670
|
+
const task = findTask(tasks, ref);
|
|
4671
|
+
if (!task) {
|
|
4672
|
+
body.push(`${ref}: no task matches this reference in the current plan.`, "");
|
|
4673
|
+
continue;
|
|
4674
|
+
}
|
|
4675
|
+
body.push(...renderTask(task, fields));
|
|
4676
|
+
}
|
|
4677
|
+
blocks.push(["<task_detail>", ...body, "</task_detail>"].join("\n"));
|
|
4678
|
+
}
|
|
4679
|
+
if (query.catalog) {
|
|
4680
|
+
blocks.push(["<runner_catalog>", ...renderCatalog(catalog), "</runner_catalog>"].join("\n"));
|
|
4681
|
+
}
|
|
4682
|
+
blocks.push(
|
|
4683
|
+
"That is everything you asked for. Nothing has changed. Reply now with the taskOps JSON for the edit you intended, or answer the user in prose."
|
|
4684
|
+
);
|
|
4685
|
+
return blocks.join("\n\n");
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
// src/services/PlanPrompts.ts
|
|
4689
|
+
function buildResearchToolsPrompt(subagentsEnabled = false) {
|
|
4690
|
+
const lines = [
|
|
4691
|
+
"You have access to the following tools to explore the workspace:",
|
|
4692
|
+
"",
|
|
4693
|
+
"read_file(path: string, offset?: number, limit?: number) - Read file contents with line numbers and optional pagination. offset is a 0-based line number (default 0). limit is max lines (default 2000). Output lines are prefixed with their line numbers. When a file has more lines than requested, a hint shows the offset to use next. Files larger than ~1 MB are rejected \u2014 use grep instead.",
|
|
4694
|
+
"read_files(paths: string[]) - Read multiple files at once. Batch reads when you have several known paths \u2014 this is faster than calling read_file repeatedly.",
|
|
4695
|
+
'glob(pattern: string) - Find files matching a glob pattern (e.g. "src/**/*.ts"). Use for finding files by name/path. Results sorted by modification time, newest first.',
|
|
4696
|
+
'grep(pattern: string, include?: string) - Search for a regex pattern inside file contents. Use for finding code patterns, function definitions, imports, etc. Use "include" to filter by file type (e.g. "*.ts").',
|
|
4697
|
+
"list_dir(path: string, depth?: number) - List directory contents as a tree. Use for orientation \u2014 to see what directories and files exist. Use depth=2 or depth=3 for a tree view.",
|
|
4698
|
+
"bash(command: string) - Run a read-only shell command (ls, tree, git log/status/diff, cat, head, wc, etc.). Do NOT use bash for file search \u2014 use the dedicated glob and grep tools instead.",
|
|
4699
|
+
"",
|
|
4700
|
+
"TOOL CHOICE GUIDE:",
|
|
4701
|
+
"- list_dir: for browsing/orientation and understanding project structure",
|
|
4702
|
+
"- glob: for finding files by name/path pattern",
|
|
4703
|
+
"- grep: for searching inside file contents",
|
|
4704
|
+
"",
|
|
4705
|
+
"When multiple independent tool calls are needed, batch them in a single response to save round-trips. For example, if you need to read README.md and AGENTS.md, call read_files with both paths at once."
|
|
4706
|
+
];
|
|
4707
|
+
if (subagentsEnabled) {
|
|
4708
|
+
lines.push(
|
|
4709
|
+
"",
|
|
4710
|
+
'spawn_research_agent(prompt: string) - Launch a stateless read-only research agent that explores the workspace with its own tools and returns a digest. Use it to delegate an open-ended exploration thread \u2014 a subsystem to map, or a "how does X work here" question \u2014 whenever one emerges during research. To explore several independent areas, launch the agents CONCURRENTLY: put multiple spawn_research_agent calls in one reply and they run in parallel.',
|
|
4711
|
+
"The agent sees nothing of this conversation, so its prompt must be self-contained: the area or question, the paths/symbols to start from, and exactly what the digest must report back. Do NOT use it on small or single-subsystem repos, or to read one known file \u2014 direct tools are faster there."
|
|
4712
|
+
);
|
|
4713
|
+
}
|
|
4714
|
+
return lines.join("\n");
|
|
4715
|
+
}
|
|
4716
|
+
function buildSubagentSystemPrompt() {
|
|
4717
|
+
return [
|
|
4718
|
+
"You are a read-only research subagent working for a project planner. You receive one self-contained research task, explore the workspace with your tools, and reply with a single digest. You never modify files, never fetch URLs, and cannot spawn further agents.",
|
|
4719
|
+
"",
|
|
4720
|
+
"Work efficiently: batch independent tool calls in one reply; prefer glob/grep to locate, then read only what matters. Your tool budget is small \u2014 stop exploring when you can answer the task.",
|
|
4721
|
+
"",
|
|
4722
|
+
"Your final reply IS the digest \u2014 it goes straight back to the planner, not to a human. It must contain:",
|
|
4723
|
+
"- The exact file paths (and key line references) relevant to the task",
|
|
4724
|
+
"- The key symbols/functions involved and how they connect",
|
|
4725
|
+
"- Constraints, gotchas, or conventions the planner must respect",
|
|
4726
|
+
"- Direct answers to every question the task asked",
|
|
4727
|
+
"Plain prose/markdown only. No questions, no task plans, no JSON. Be dense and specific; vague summaries are useless to the planner."
|
|
4728
|
+
].join("\n");
|
|
4729
|
+
}
|
|
4730
|
+
function buildModeGuideForRunners(runners, _autonomousDefault) {
|
|
4731
|
+
const lines = ['Assign a "taskMode" to each AI task based on the runner:'];
|
|
4732
|
+
for (const r of runners) {
|
|
4733
|
+
if (r === "claude-code") {
|
|
4734
|
+
lines.push('- For claude-code: "acceptEdits" (edit automatically, recommended), "default" (ask before edits), "plan" (read-only analysis), "bypassPermissions" (skip all prompts, CI only).');
|
|
4735
|
+
} else if (r === "opencode") {
|
|
4736
|
+
lines.push('- For opencode: "build" (full access agent), "plan" (read-only analysis).');
|
|
4737
|
+
} else {
|
|
4738
|
+
lines.push(`- For ${r}: use the modes from the runner's manifest.`);
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
lines.push('Use the recommended mode for implementation. Use "plan" for analysis-only requests.');
|
|
4742
|
+
return lines.join("\n");
|
|
2968
4743
|
}
|
|
2969
4744
|
function buildModeExamplesForRunners(runners) {
|
|
2970
4745
|
return runners.map((r) => {
|
|
@@ -2999,7 +4774,8 @@ function researchPhaseBlock(harnessMode) {
|
|
|
2999
4774
|
];
|
|
3000
4775
|
const toolSpecific = harnessMode ? [
|
|
3001
4776
|
"- Explore the repository with your own tools until you understand its architecture",
|
|
3002
|
-
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace"
|
|
4777
|
+
"- You are planning, not implementing: do NOT edit, create, or delete any file, and do not run commands that change the workspace",
|
|
4778
|
+
"- If you delegate exploration to your own agents, WAIT for their results inside this reply. Do NOT launch them in the background or async and end your turn saying you will report back later: your turn ending is what hands the conversation back to the user, and anything you say after it never reaches them."
|
|
3003
4779
|
] : [
|
|
3004
4780
|
"- Use list_dir with depth=2-3 for a tree overview",
|
|
3005
4781
|
"- Use glob and grep to find relevant source files",
|
|
@@ -3164,6 +4940,11 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
3164
4940
|
"TASK MODE:",
|
|
3165
4941
|
modeGuide,
|
|
3166
4942
|
"",
|
|
4943
|
+
// Shared by both variants on purpose: the read channel is a text envelope
|
|
4944
|
+
// exactly so a harness planner, which Ordewell cannot hand tools to, speaks
|
|
4945
|
+
// the same protocol as an API-backed one (ADR-0009).
|
|
4946
|
+
...TASK_QUERY_PROTOCOL,
|
|
4947
|
+
"",
|
|
3167
4948
|
context ? `PROJECT CONTEXT:
|
|
3168
4949
|
${context}
|
|
3169
4950
|
` : "",
|
|
@@ -3471,7 +5252,8 @@ function buildMergePrompt(taskIds, tasks) {
|
|
|
3471
5252
|
"The merged task takes the union of their dependencies (excluding the merged tasks themselves) and preserves their user stories.",
|
|
3472
5253
|
'Set "assignedRunner" and "assignedModel" on the merged task \u2014 use one of the runners and models listed in <available_models> above. Prefer the strongest model if the merged work is complex.',
|
|
3473
5254
|
'Reply with ONLY a taskOps JSON object using a single "merge" op:',
|
|
3474
|
-
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}
|
|
5255
|
+
` {"taskOps":[{"op":"merge","taskIds":[${selected.map((t) => `"${t.id}"`).join(", ")}],"merged":{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}}}]}`,
|
|
5256
|
+
`If this merge needs a companion op in the same batch (e.g. an added task that should depend on the merge result), give the merge op a "handle" (any unused name) and reference it from the later op's taskId/dependencies.`
|
|
3475
5257
|
].join("\n");
|
|
3476
5258
|
}
|
|
3477
5259
|
function buildSplitPrompt(taskId, tasks) {
|
|
@@ -3483,153 +5265,11 @@ function buildSplitPrompt(taskId, tasks) {
|
|
|
3483
5265
|
"The first part inherits the original task's dependencies. Each later part depends on the previous part. Tasks that depended on the original now depend on the LAST part.",
|
|
3484
5266
|
'Set "assignedRunner" and "assignedModel" on each part \u2014 use the runners and models listed in <available_models> above. You may assign different models to different parts (e.g. a stronger model for a complex part, a faster one for a simple part).',
|
|
3485
5267
|
'Reply with ONLY a taskOps JSON object using a single "split" op:',
|
|
3486
|
-
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}
|
|
5268
|
+
` {"taskOps":[{"op":"split","taskId":"${task.id}","parts":[{"title":"...","description":"...","prompt":"...","assignedRunner":"...","assignedModel":{"modelId":"...","modelLabel":"..."}},...]}]}`,
|
|
5269
|
+
`If this split needs a companion op in the same batch (e.g. another task that should depend on the last part), give the split op a "handle" (any unused name) \u2014 it names the last part \u2014 and reference it from the later op's taskId/dependencies.`
|
|
3487
5270
|
].join("\n");
|
|
3488
5271
|
}
|
|
3489
5272
|
|
|
3490
|
-
// src/services/JsonExtractor.ts
|
|
3491
|
-
var PLAN_ENVELOPE_KEY = "tasks";
|
|
3492
|
-
var TASK_OPS_ENVELOPE_KEY = "taskOps";
|
|
3493
|
-
var PlanParseError = class extends Error {
|
|
3494
|
-
raw;
|
|
3495
|
-
/**
|
|
3496
|
-
* True when the reply was cut off mid-object (output-token limit) rather
|
|
3497
|
-
* than malformed — the two need different repairs: a truncated emission will
|
|
3498
|
-
* be cut at the same point again unless the retry frees context or asks for
|
|
3499
|
-
* terser output.
|
|
3500
|
-
*/
|
|
3501
|
-
truncated;
|
|
3502
|
-
/**
|
|
3503
|
-
* True when the JSON parsed fine and a *shape* rule rejected it. The generic
|
|
3504
|
-
* "could not be parsed as JSON" corrective is a lie for these, so the model
|
|
3505
|
-
* re-sends the same object; a semantic rejection must be repaired by naming
|
|
3506
|
-
* the rule it broke.
|
|
3507
|
-
*/
|
|
3508
|
-
semantic;
|
|
3509
|
-
constructor(message, raw, opts) {
|
|
3510
|
-
super(message);
|
|
3511
|
-
this.name = "PlanParseError";
|
|
3512
|
-
this.raw = raw;
|
|
3513
|
-
this.truncated = opts?.truncated ?? false;
|
|
3514
|
-
this.semantic = opts?.semantic ?? false;
|
|
3515
|
-
}
|
|
3516
|
-
};
|
|
3517
|
-
function stripModelNoise(raw) {
|
|
3518
|
-
return raw.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/```json\s*/gi, "").replace(/```\s*/g, "").trim();
|
|
3519
|
-
}
|
|
3520
|
-
function balancedObjectAt(text, start) {
|
|
3521
|
-
let depth = 0;
|
|
3522
|
-
let inString = false;
|
|
3523
|
-
let escaped = false;
|
|
3524
|
-
for (let i = start; i < text.length; i++) {
|
|
3525
|
-
const ch = text[i];
|
|
3526
|
-
if (inString) {
|
|
3527
|
-
if (escaped) escaped = false;
|
|
3528
|
-
else if (ch === "\\") escaped = true;
|
|
3529
|
-
else if (ch === '"') inString = false;
|
|
3530
|
-
continue;
|
|
3531
|
-
}
|
|
3532
|
-
if (ch === '"') inString = true;
|
|
3533
|
-
else if (ch === "{") depth++;
|
|
3534
|
-
else if (ch === "}") {
|
|
3535
|
-
depth--;
|
|
3536
|
-
if (depth === 0) return { json: text.slice(start, i + 1), balanced: true };
|
|
3537
|
-
}
|
|
3538
|
-
}
|
|
3539
|
-
return { json: text.slice(start), balanced: false };
|
|
3540
|
-
}
|
|
3541
|
-
function extractObjectsWithKey(raw, key) {
|
|
3542
|
-
const stripped = stripModelNoise(raw);
|
|
3543
|
-
const keyRe = new RegExp(`"${key}"\\s*:`);
|
|
3544
|
-
const matches = [];
|
|
3545
|
-
let firstObject = null;
|
|
3546
|
-
let sawBrace = false;
|
|
3547
|
-
let sawUnbalanced = false;
|
|
3548
|
-
let inString = false;
|
|
3549
|
-
let escaped = false;
|
|
3550
|
-
for (let i = 0; i < stripped.length; i++) {
|
|
3551
|
-
const ch = stripped[i];
|
|
3552
|
-
if (inString) {
|
|
3553
|
-
if (escaped) escaped = false;
|
|
3554
|
-
else if (ch === "\\") escaped = true;
|
|
3555
|
-
else if (ch === '"') inString = false;
|
|
3556
|
-
continue;
|
|
3557
|
-
}
|
|
3558
|
-
if (ch === '"') {
|
|
3559
|
-
inString = true;
|
|
3560
|
-
continue;
|
|
3561
|
-
}
|
|
3562
|
-
if (ch !== "{") continue;
|
|
3563
|
-
sawBrace = true;
|
|
3564
|
-
const obj = balancedObjectAt(stripped, i);
|
|
3565
|
-
if (obj.balanced && keyRe.test(obj.json)) {
|
|
3566
|
-
matches.push(obj.json);
|
|
3567
|
-
i += obj.json.length - 1;
|
|
3568
|
-
continue;
|
|
3569
|
-
}
|
|
3570
|
-
if (!obj.balanced) sawUnbalanced = true;
|
|
3571
|
-
if (!firstObject && obj.balanced) firstObject = obj.json;
|
|
3572
|
-
}
|
|
3573
|
-
return {
|
|
3574
|
-
matches,
|
|
3575
|
-
fallback: firstObject !== null ? { json: firstObject, balanced: true } : { json: stripped, balanced: false },
|
|
3576
|
-
sawBrace,
|
|
3577
|
-
sawUnbalanced
|
|
3578
|
-
};
|
|
3579
|
-
}
|
|
3580
|
-
function extractObjectWithBalance(raw) {
|
|
3581
|
-
const { matches, fallback, sawBrace } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
3582
|
-
if (matches.length > 0) return { json: matches[0], balanced: true, sawBrace };
|
|
3583
|
-
return { ...fallback, sawBrace };
|
|
3584
|
-
}
|
|
3585
|
-
function extractJsonObject(raw) {
|
|
3586
|
-
return extractObjectWithBalance(raw).json;
|
|
3587
|
-
}
|
|
3588
|
-
function stripTrailingCommas(json) {
|
|
3589
|
-
return json.replace(/,(\s*[}\]])/g, "$1");
|
|
3590
|
-
}
|
|
3591
|
-
function escapeControlCharsInStrings(json) {
|
|
3592
|
-
let out = "";
|
|
3593
|
-
let inString = false;
|
|
3594
|
-
let escaped = false;
|
|
3595
|
-
for (const ch of json) {
|
|
3596
|
-
if (inString) {
|
|
3597
|
-
if (escaped) {
|
|
3598
|
-
out += ch;
|
|
3599
|
-
escaped = false;
|
|
3600
|
-
continue;
|
|
3601
|
-
}
|
|
3602
|
-
if (ch === "\\") {
|
|
3603
|
-
out += ch;
|
|
3604
|
-
escaped = true;
|
|
3605
|
-
continue;
|
|
3606
|
-
}
|
|
3607
|
-
if (ch === '"') {
|
|
3608
|
-
out += ch;
|
|
3609
|
-
inString = false;
|
|
3610
|
-
continue;
|
|
3611
|
-
}
|
|
3612
|
-
if (ch === "\n") {
|
|
3613
|
-
out += "\\n";
|
|
3614
|
-
continue;
|
|
3615
|
-
}
|
|
3616
|
-
if (ch === "\r") {
|
|
3617
|
-
out += "\\r";
|
|
3618
|
-
continue;
|
|
3619
|
-
}
|
|
3620
|
-
if (ch === " ") {
|
|
3621
|
-
out += "\\t";
|
|
3622
|
-
continue;
|
|
3623
|
-
}
|
|
3624
|
-
out += ch;
|
|
3625
|
-
continue;
|
|
3626
|
-
}
|
|
3627
|
-
if (ch === '"') inString = true;
|
|
3628
|
-
out += ch;
|
|
3629
|
-
}
|
|
3630
|
-
return out;
|
|
3631
|
-
}
|
|
3632
|
-
|
|
3633
5273
|
// src/services/PlanValidator.ts
|
|
3634
5274
|
function parsePlanJson(raw, runners, runnerModes, autonomousDefault = true) {
|
|
3635
5275
|
const { matches, fallback, sawUnbalanced } = extractObjectsWithKey(raw, PLAN_ENVELOPE_KEY);
|
|
@@ -3870,6 +5510,171 @@ function validatePlanModification(ctx) {
|
|
|
3870
5510
|
return { valid: allErrors.length === 0, errors: allErrors };
|
|
3871
5511
|
}
|
|
3872
5512
|
|
|
5513
|
+
// src/services/TaskOps.ts
|
|
5514
|
+
var import_uuid2 = require("uuid");
|
|
5515
|
+
|
|
5516
|
+
// src/services/ModelAllowlistResolver.ts
|
|
5517
|
+
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
5518
|
+
if (!allowlist || allowlist.length === 0) return void 0;
|
|
5519
|
+
if (!modelsByRunner) return allowlist;
|
|
5520
|
+
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
5521
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
5522
|
+
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
5523
|
+
if (other === runner) continue;
|
|
5524
|
+
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
5525
|
+
}
|
|
5526
|
+
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
5527
|
+
return kept.length > 0 ? kept : void 0;
|
|
5528
|
+
}
|
|
5529
|
+
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
5530
|
+
const result = {};
|
|
5531
|
+
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
5532
|
+
if (!models) continue;
|
|
5533
|
+
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
5534
|
+
if (!allowed) {
|
|
5535
|
+
result[runner] = models;
|
|
5536
|
+
continue;
|
|
5537
|
+
}
|
|
5538
|
+
const allowedSet = new Set(allowed);
|
|
5539
|
+
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
5540
|
+
const covered = new Set(intersection.map((m) => m.modelId));
|
|
5541
|
+
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
5542
|
+
result[runner] = [...intersection, ...missing];
|
|
5543
|
+
}
|
|
5544
|
+
return result;
|
|
5545
|
+
}
|
|
5546
|
+
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
5547
|
+
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
5548
|
+
function clampThinkingEffort(effort, variants) {
|
|
5549
|
+
if (!effort) return void 0;
|
|
5550
|
+
if (variants.length === 0) return void 0;
|
|
5551
|
+
const ids = new Set(variants.map((v) => v.id));
|
|
5552
|
+
if (ids.has(effort)) return effort;
|
|
5553
|
+
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
5554
|
+
if (ids.has(canon)) return canon;
|
|
5555
|
+
const idx = EFFORT_LADDER.indexOf(canon);
|
|
5556
|
+
if (idx < 0) return void 0;
|
|
5557
|
+
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
5558
|
+
const lower = EFFORT_LADDER[idx - d];
|
|
5559
|
+
if (lower && ids.has(lower)) return lower;
|
|
5560
|
+
const higher = EFFORT_LADDER[idx + d];
|
|
5561
|
+
if (higher && ids.has(higher)) return higher;
|
|
5562
|
+
}
|
|
5563
|
+
return void 0;
|
|
5564
|
+
}
|
|
5565
|
+
function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
|
|
5566
|
+
const clampVariant = (task) => {
|
|
5567
|
+
if (!task.assignedModel || !modelsByRunner) return task;
|
|
5568
|
+
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
5569
|
+
(m) => m.modelId === task.assignedModel.modelId
|
|
5570
|
+
);
|
|
5571
|
+
if (!model) return task;
|
|
5572
|
+
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
5573
|
+
return {
|
|
5574
|
+
...task,
|
|
5575
|
+
assignedModel: {
|
|
5576
|
+
...task.assignedModel,
|
|
5577
|
+
thinkingEffort: clamped,
|
|
5578
|
+
availableVariants: model.variants.map((v) => v.id)
|
|
5579
|
+
}
|
|
5580
|
+
};
|
|
5581
|
+
};
|
|
5582
|
+
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
5583
|
+
const assignmentFor = (runner, modelId) => {
|
|
5584
|
+
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
5585
|
+
return {
|
|
5586
|
+
modelId,
|
|
5587
|
+
modelLabel: known?.modelLabel ?? modelId,
|
|
5588
|
+
thinkingEffort: void 0,
|
|
5589
|
+
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
5590
|
+
};
|
|
5591
|
+
};
|
|
5592
|
+
return tasks.map((task) => {
|
|
5593
|
+
if (task.type === "user") return task;
|
|
5594
|
+
if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
|
|
5595
|
+
const fallbackRunner = allowedRunners[0];
|
|
5596
|
+
const fallbackModels = allowedFor(fallbackRunner);
|
|
5597
|
+
const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
|
|
5598
|
+
return clampVariant({
|
|
5599
|
+
...task,
|
|
5600
|
+
assignedRunner: fallbackRunner,
|
|
5601
|
+
assignedModel: fallbackModel
|
|
5602
|
+
});
|
|
5603
|
+
}
|
|
5604
|
+
const allowlist = allowedFor(task.assignedRunner);
|
|
5605
|
+
if (!task.assignedModel) {
|
|
5606
|
+
if (allowlist && allowlist.length > 0) {
|
|
5607
|
+
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
|
|
5608
|
+
}
|
|
5609
|
+
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
5610
|
+
if (discovered) {
|
|
5611
|
+
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
5612
|
+
}
|
|
5613
|
+
return task;
|
|
5614
|
+
}
|
|
5615
|
+
if (!allowlist || allowlist.length === 0) return clampVariant(task);
|
|
5616
|
+
if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
|
|
5617
|
+
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
5618
|
+
});
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
// src/services/TaskEditValidator.ts
|
|
5622
|
+
var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy"];
|
|
5623
|
+
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
5624
|
+
if (!catalog) return { ok: true };
|
|
5625
|
+
if (assignedModel) {
|
|
5626
|
+
const models = catalog.modelsByRunner[runner];
|
|
5627
|
+
if (models) {
|
|
5628
|
+
const modelId = assignedModel.modelId;
|
|
5629
|
+
if (!models.some((m) => m.modelId === modelId)) {
|
|
5630
|
+
return { ok: false, error: `Runner "${runner}" does not offer model "${modelId}"` };
|
|
5631
|
+
}
|
|
5632
|
+
const allowed = effectiveAllowlist(catalog.perRunnerAllowlist?.[runner], runner, catalog.modelsByRunner);
|
|
5633
|
+
if (allowed && !allowed.includes(modelId)) {
|
|
5634
|
+
return { ok: false, error: `Model "${modelId}" is excluded by the allowlist for runner "${runner}"` };
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
if (taskMode !== void 0) {
|
|
5639
|
+
const modes = catalog.runnerModes[runner];
|
|
5640
|
+
if (modes && modes.length > 0 && !modes.some((m) => m.id === taskMode)) {
|
|
5641
|
+
return { ok: false, error: `Runner "${runner}" has no task mode "${taskMode}" \u2014 valid modes: ${modes.map((m) => m.id).join(", ")}` };
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
return { ok: true };
|
|
5645
|
+
}
|
|
5646
|
+
function typeCoherenceCheck(target, changes) {
|
|
5647
|
+
if (!changes.type || changes.type === target.type) return null;
|
|
5648
|
+
if (changes.type === "user") {
|
|
5649
|
+
if (!Array.isArray(changes.userSteps) || changes.userSteps.length === 0) {
|
|
5650
|
+
return { ok: false, error: `"${target.title}" cannot become a manual task without user steps \u2014 include "userSteps" in this edit` };
|
|
5651
|
+
}
|
|
5652
|
+
return { ok: true, clear: AI_ONLY_FIELDS.filter((f) => target[f] !== void 0) };
|
|
5653
|
+
}
|
|
5654
|
+
if (!changes.prompt || !changes.prompt.trim()) {
|
|
5655
|
+
return { ok: false, error: `"${target.title}" cannot become an AI task without a prompt \u2014 include "prompt" in this edit` };
|
|
5656
|
+
}
|
|
5657
|
+
return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
|
|
5658
|
+
}
|
|
5659
|
+
function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
5660
|
+
const target = tasks.find((t) => t.id === taskId);
|
|
5661
|
+
if (!target) return { ok: false, error: "Task not found" };
|
|
5662
|
+
if (actor === "planner") {
|
|
5663
|
+
if (target.status === "in_progress") return { ok: false, error: `"${target.title}" is running and cannot be modified` };
|
|
5664
|
+
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be modified` };
|
|
5665
|
+
}
|
|
5666
|
+
if (changes.dependencies) {
|
|
5667
|
+
const depCheck = canSetDependencies(tasks, taskId, changes.dependencies);
|
|
5668
|
+
if (!depCheck.ok) return depCheck;
|
|
5669
|
+
}
|
|
5670
|
+
const coherence = typeCoherenceCheck(target, changes);
|
|
5671
|
+
if (coherence && !coherence.ok) return coherence;
|
|
5672
|
+
const runner = changes.assignedRunner ?? target.assignedRunner;
|
|
5673
|
+
const validity = checkModelAndModeValidity(runner, changes.assignedModel, changes.taskMode, catalog);
|
|
5674
|
+
if (!validity.ok) return validity;
|
|
5675
|
+
return coherence ?? { ok: true };
|
|
5676
|
+
}
|
|
5677
|
+
|
|
3873
5678
|
// src/services/TaskOps.ts
|
|
3874
5679
|
var UPDATABLE_FIELDS = [
|
|
3875
5680
|
"title",
|
|
@@ -3882,7 +5687,8 @@ var UPDATABLE_FIELDS = [
|
|
|
3882
5687
|
"thinkingEffort",
|
|
3883
5688
|
"type",
|
|
3884
5689
|
"userSteps",
|
|
3885
|
-
"autonomy"
|
|
5690
|
+
"autonomy",
|
|
5691
|
+
"sliceType"
|
|
3886
5692
|
];
|
|
3887
5693
|
function textHasTaskOps(text) {
|
|
3888
5694
|
return text.includes(`"${TASK_OPS_ENVELOPE_KEY}"`);
|
|
@@ -3919,18 +5725,41 @@ function parseTaskOpsObject(json, text) {
|
|
|
3919
5725
|
}
|
|
3920
5726
|
return ops;
|
|
3921
5727
|
}
|
|
3922
|
-
function resolveRef(ref,
|
|
3923
|
-
if (typeof ref !== "string" && typeof ref !== "number") return
|
|
5728
|
+
function resolveRef(ref, originalScope, liveScope, handleOwner, handleId, opIndex) {
|
|
5729
|
+
if (typeof ref !== "string" && typeof ref !== "number") return { error: `invalid reference "${String(ref)}"` };
|
|
3924
5730
|
const s = String(ref).trim();
|
|
3925
|
-
const
|
|
3926
|
-
if (
|
|
5731
|
+
const ownerIdx = handleOwner.get(s);
|
|
5732
|
+
if (ownerIdx !== void 0) {
|
|
5733
|
+
if (ownerIdx >= opIndex) {
|
|
5734
|
+
return { error: `handle "${s}" is defined later in this batch (op ${ownerIdx + 1}) and cannot be referenced yet` };
|
|
5735
|
+
}
|
|
5736
|
+
const id = handleId.get(s);
|
|
5737
|
+
if (!id) return { error: `handle "${s}" was never resolved` };
|
|
5738
|
+
return { id };
|
|
5739
|
+
}
|
|
5740
|
+
const byLiveId = liveScope.find((t) => t.id === s);
|
|
5741
|
+
if (byLiveId) return { id: byLiveId.id };
|
|
3927
5742
|
const orderStr = s.startsWith("#") ? s.slice(1) : s;
|
|
3928
5743
|
if (/^\d+$/.test(orderStr)) {
|
|
3929
5744
|
const order = Number(orderStr);
|
|
3930
|
-
const byOrder =
|
|
3931
|
-
if (byOrder.length === 1) return byOrder[0];
|
|
5745
|
+
const byOrder = originalScope.filter((t) => t.order === order);
|
|
5746
|
+
if (byOrder.length === 1) return { id: byOrder[0].id };
|
|
3932
5747
|
}
|
|
3933
|
-
|
|
5748
|
+
const byTitle = originalScope.find((t) => t.title === s);
|
|
5749
|
+
if (byTitle) return { id: byTitle.id };
|
|
5750
|
+
return { error: `task "${s}" not found` };
|
|
5751
|
+
}
|
|
5752
|
+
function refCollidesWithExisting(name, originalFlatAll) {
|
|
5753
|
+
if (originalFlatAll.some((t) => t.id === name)) return true;
|
|
5754
|
+
const orderStr = name.startsWith("#") ? name.slice(1) : name;
|
|
5755
|
+
if (/^\d+$/.test(orderStr) && originalFlatAll.some((t) => t.order === Number(orderStr))) return true;
|
|
5756
|
+
return originalFlatAll.some((t) => t.title === name);
|
|
5757
|
+
}
|
|
5758
|
+
function findInTasks(tasks, id) {
|
|
5759
|
+
return flattenTasks(tasks).find((t) => t.id === id);
|
|
5760
|
+
}
|
|
5761
|
+
function findTopLevel(tasks, id) {
|
|
5762
|
+
return tasks.find((t) => t.id === id);
|
|
3934
5763
|
}
|
|
3935
5764
|
function detectCycle(tasks) {
|
|
3936
5765
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
@@ -3954,6 +5783,55 @@ function detectCycle(tasks) {
|
|
|
3954
5783
|
}
|
|
3955
5784
|
return null;
|
|
3956
5785
|
}
|
|
5786
|
+
function isPinned(t) {
|
|
5787
|
+
return t.status === "in_progress" || t.status === "completed";
|
|
5788
|
+
}
|
|
5789
|
+
function repairOrder(tasks) {
|
|
5790
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
5791
|
+
const placed = /* @__PURE__ */ new Set();
|
|
5792
|
+
const movable = tasks.filter((t) => !isPinned(t));
|
|
5793
|
+
const result = [];
|
|
5794
|
+
const blockerOf = (t) => {
|
|
5795
|
+
const id = t.dependencies.find((d) => byId.has(d) && !placed.has(d));
|
|
5796
|
+
return id ? byId.get(id) : void 0;
|
|
5797
|
+
};
|
|
5798
|
+
const place = (t) => {
|
|
5799
|
+
result.push(t);
|
|
5800
|
+
placed.add(t.id);
|
|
5801
|
+
};
|
|
5802
|
+
for (const occupant of tasks) {
|
|
5803
|
+
if (isPinned(occupant)) {
|
|
5804
|
+
const blocker = blockerOf(occupant);
|
|
5805
|
+
if (blocker) return { error: pinnedRefusal(blocker, occupant, "before") };
|
|
5806
|
+
place(occupant);
|
|
5807
|
+
continue;
|
|
5808
|
+
}
|
|
5809
|
+
const next = movable.findIndex((t) => !blockerOf(t));
|
|
5810
|
+
if (next < 0) {
|
|
5811
|
+
const [stuck, pin] = trailToPin(movable[0], blockerOf);
|
|
5812
|
+
return { error: pinnedRefusal(stuck, pin, "after") };
|
|
5813
|
+
}
|
|
5814
|
+
place(movable.splice(next, 1)[0]);
|
|
5815
|
+
}
|
|
5816
|
+
return { tasks: result };
|
|
5817
|
+
}
|
|
5818
|
+
function trailToPin(task, blockerOf) {
|
|
5819
|
+
let at = task;
|
|
5820
|
+
for (; ; ) {
|
|
5821
|
+
const blocker = blockerOf(at);
|
|
5822
|
+
if (!blocker || isPinned(blocker)) return [at, blocker ?? at];
|
|
5823
|
+
at = blocker;
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
function describeMoves(before, after) {
|
|
5827
|
+
const wasAt = new Map(before.map((t, i) => [t.id, i + 1]));
|
|
5828
|
+
const moved = after.map((t, i) => ({ t, from: wasAt.get(t.id), to: i + 1 })).filter((m) => m.from !== m.to);
|
|
5829
|
+
return moved.length ? moved.map((m) => `"${m.t.title}" #${m.from}\u2192#${m.to}`).join(", ") : null;
|
|
5830
|
+
}
|
|
5831
|
+
function pinnedRefusal(other, pin, side) {
|
|
5832
|
+
const word = pin.status === "in_progress" ? "running" : "completed";
|
|
5833
|
+
return `"${other.title}" would have to run ${side} "${pin.title}", which is ${word} and cannot be moved`;
|
|
5834
|
+
}
|
|
3957
5835
|
function canMergeTasks(tasks, selectedIds) {
|
|
3958
5836
|
if (selectedIds.length < 2) return { ok: false, error: "Select at least two tasks to merge" };
|
|
3959
5837
|
const idSet = new Set(selectedIds);
|
|
@@ -3970,15 +5848,6 @@ function canMergeTasks(tasks, selectedIds) {
|
|
|
3970
5848
|
sim.push({ ...ordered[0], id: survivorId, title: "__merge_sim__", order: ordered[0].order, dependencies: unionDeps });
|
|
3971
5849
|
const cycle = detectCycle(sim);
|
|
3972
5850
|
if (cycle) return { ok: false, error: `Merging would create a dependency cycle involving "${cycle}"` };
|
|
3973
|
-
const orderById = new Map(sim.map((t) => [t.id, t.order]));
|
|
3974
|
-
for (const t of sim) {
|
|
3975
|
-
for (const dep of t.dependencies) {
|
|
3976
|
-
const depOrder = orderById.get(dep);
|
|
3977
|
-
if (depOrder !== void 0 && depOrder >= t.order) {
|
|
3978
|
-
return { ok: false, error: "Merging would break task ordering \u2014 a dependency of the merged set comes after it" };
|
|
3979
|
-
}
|
|
3980
|
-
}
|
|
3981
|
-
}
|
|
3982
5851
|
return { ok: true };
|
|
3983
5852
|
}
|
|
3984
5853
|
function dependentsOf(tasks, taskId) {
|
|
@@ -4010,17 +5879,46 @@ function canSplitTask(tasks, taskId) {
|
|
|
4010
5879
|
if (target.status === "completed") return { ok: false, error: `"${target.title}" is completed and cannot be split` };
|
|
4011
5880
|
return { ok: true };
|
|
4012
5881
|
}
|
|
4013
|
-
function applyTaskOps(currentTasks, ops, runners) {
|
|
5882
|
+
function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
4014
5883
|
let tasks = JSON.parse(JSON.stringify(currentTasks));
|
|
4015
5884
|
const errors = [];
|
|
4016
5885
|
const summary = [];
|
|
4017
|
-
const
|
|
5886
|
+
const originalFlatAll = flattenTasks(currentTasks);
|
|
5887
|
+
const handleOwner = /* @__PURE__ */ new Map();
|
|
5888
|
+
const handleId = /* @__PURE__ */ new Map();
|
|
5889
|
+
for (const [i, op] of ops.entries()) {
|
|
5890
|
+
const handle = op.handle;
|
|
5891
|
+
if (handle === void 0) continue;
|
|
5892
|
+
if (typeof handle !== "string" || !handle.trim()) {
|
|
5893
|
+
errors.push(`op ${i + 1} (${op.op}): handle must be a non-empty string`);
|
|
5894
|
+
continue;
|
|
5895
|
+
}
|
|
5896
|
+
if (handleOwner.has(handle)) {
|
|
5897
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle}" is already used earlier in this batch`);
|
|
5898
|
+
continue;
|
|
5899
|
+
}
|
|
5900
|
+
if (refCollidesWithExisting(handle, originalFlatAll)) {
|
|
5901
|
+
errors.push(`op ${i + 1} (${op.op}): handle "${handle}" collides with an existing task reference`);
|
|
5902
|
+
continue;
|
|
5903
|
+
}
|
|
5904
|
+
handleOwner.set(handle, i);
|
|
5905
|
+
}
|
|
5906
|
+
if (errors.length) return { ok: false, tasks: currentTasks, errors, summary: [] };
|
|
5907
|
+
const resolveDeps = (deps, opIndex) => {
|
|
4018
5908
|
const ids = [];
|
|
4019
5909
|
const bad = [];
|
|
5910
|
+
const liveFlat = flattenTasks(tasks);
|
|
4020
5911
|
for (const d of Array.isArray(deps) ? deps : []) {
|
|
4021
|
-
const
|
|
4022
|
-
if (
|
|
4023
|
-
|
|
5912
|
+
const r = resolveRef(d, originalFlatAll, liveFlat, handleOwner, handleId, opIndex);
|
|
5913
|
+
if ("error" in r) {
|
|
5914
|
+
bad.push(`${String(d)} (${r.error})`);
|
|
5915
|
+
continue;
|
|
5916
|
+
}
|
|
5917
|
+
if (!liveFlat.some((t) => t.id === r.id)) {
|
|
5918
|
+
bad.push(`${String(d)} (no longer exists in this batch)`);
|
|
5919
|
+
continue;
|
|
5920
|
+
}
|
|
5921
|
+
ids.push(r.id);
|
|
4024
5922
|
}
|
|
4025
5923
|
return { ids, bad };
|
|
4026
5924
|
};
|
|
@@ -4028,41 +5926,47 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4028
5926
|
const label = `op ${i + 1} (${op.op})`;
|
|
4029
5927
|
switch (op.op) {
|
|
4030
5928
|
case "update": {
|
|
4031
|
-
const
|
|
4032
|
-
if (
|
|
4033
|
-
errors.push(`${label}:
|
|
4034
|
-
break;
|
|
4035
|
-
}
|
|
4036
|
-
if (target.status === "in_progress") {
|
|
4037
|
-
errors.push(`${label}: "${target.title}" is running and cannot be modified`);
|
|
5929
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
5930
|
+
if ("error" in ref) {
|
|
5931
|
+
errors.push(`${label}: ${ref.error}`);
|
|
4038
5932
|
break;
|
|
4039
5933
|
}
|
|
4040
|
-
|
|
4041
|
-
|
|
5934
|
+
const target = findInTasks(tasks, ref.id);
|
|
5935
|
+
if (!target) {
|
|
5936
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
4042
5937
|
break;
|
|
4043
5938
|
}
|
|
4044
5939
|
const changes = {};
|
|
4045
5940
|
for (const key of UPDATABLE_FIELDS) {
|
|
4046
5941
|
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
4047
5942
|
}
|
|
5943
|
+
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
5944
|
+
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
5945
|
+
break;
|
|
5946
|
+
}
|
|
5947
|
+
const changesForCheck = { ...changes };
|
|
5948
|
+
delete changesForCheck.dependencies;
|
|
5949
|
+
const check = validateTaskEdit("planner", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
5950
|
+
if (!check.ok) {
|
|
5951
|
+
errors.push(`${label}: ${check.error}`);
|
|
5952
|
+
break;
|
|
5953
|
+
}
|
|
5954
|
+
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
4048
5955
|
if ("dependencies" in changes) {
|
|
4049
|
-
const { ids, bad } = resolveDeps(changes.dependencies);
|
|
5956
|
+
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
4050
5957
|
if (bad.length) {
|
|
4051
5958
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
4052
5959
|
break;
|
|
4053
5960
|
}
|
|
4054
5961
|
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
4055
5962
|
}
|
|
4056
|
-
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
4057
|
-
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
4058
|
-
break;
|
|
4059
|
-
}
|
|
4060
5963
|
if (Object.keys(changes).length === 0) {
|
|
4061
5964
|
errors.push(`${label}: no valid changes provided`);
|
|
4062
5965
|
break;
|
|
4063
5966
|
}
|
|
4064
5967
|
tasks = updateTaskInPlan(tasks, target.id, changes);
|
|
4065
|
-
|
|
5968
|
+
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
5969
|
+
summary.push(`Updated "${target.title}" (${Object.keys(changes).join(", ")})${clearedNote}`);
|
|
4066
5970
|
break;
|
|
4067
5971
|
}
|
|
4068
5972
|
case "add": {
|
|
@@ -4071,27 +5975,39 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4071
5975
|
errors.push(`${label}: new task needs a title`);
|
|
4072
5976
|
break;
|
|
4073
5977
|
}
|
|
4074
|
-
const { ids, bad } = resolveDeps(spec.dependencies);
|
|
5978
|
+
const { ids, bad } = resolveDeps(spec.dependencies, i);
|
|
4075
5979
|
if (bad.length) {
|
|
4076
5980
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
4077
5981
|
break;
|
|
4078
5982
|
}
|
|
4079
5983
|
const runner = spec.assignedRunner && runners.includes(spec.assignedRunner) ? spec.assignedRunner : runners[0];
|
|
5984
|
+
const validity = checkModelAndModeValidity(runner, spec.assignedModel, spec.taskMode, catalog);
|
|
5985
|
+
if (!validity.ok) {
|
|
5986
|
+
errors.push(`${label}: ${validity.error}`);
|
|
5987
|
+
break;
|
|
5988
|
+
}
|
|
5989
|
+
const newId = (0, import_uuid2.v4)();
|
|
4080
5990
|
tasks = addTaskToPlan(tasks, {
|
|
4081
5991
|
...spec,
|
|
4082
|
-
id:
|
|
5992
|
+
id: newId,
|
|
4083
5993
|
dependencies: ids,
|
|
4084
5994
|
assignedRunner: runner,
|
|
4085
5995
|
description: spec.description ?? spec.title,
|
|
4086
5996
|
prompt: spec.prompt ?? spec.description ?? spec.title
|
|
4087
5997
|
});
|
|
5998
|
+
if (op.handle) handleId.set(op.handle, newId);
|
|
4088
5999
|
summary.push(`Added "${spec.title}"`);
|
|
4089
6000
|
break;
|
|
4090
6001
|
}
|
|
4091
6002
|
case "remove": {
|
|
4092
|
-
const
|
|
6003
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
6004
|
+
if ("error" in ref) {
|
|
6005
|
+
errors.push(`${label}: ${ref.error}`);
|
|
6006
|
+
break;
|
|
6007
|
+
}
|
|
6008
|
+
const target = findInTasks(tasks, ref.id);
|
|
4093
6009
|
if (!target) {
|
|
4094
|
-
errors.push(`${label}: task "${op.taskId}"
|
|
6010
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
4095
6011
|
break;
|
|
4096
6012
|
}
|
|
4097
6013
|
if (target.status === "in_progress") {
|
|
@@ -4107,7 +6023,13 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4107
6023
|
break;
|
|
4108
6024
|
}
|
|
4109
6025
|
case "reorder": {
|
|
4110
|
-
const
|
|
6026
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
6027
|
+
const badRef = refs.find((r) => "error" in r);
|
|
6028
|
+
if (badRef) {
|
|
6029
|
+
errors.push(`${label}: ${badRef.error}`);
|
|
6030
|
+
break;
|
|
6031
|
+
}
|
|
6032
|
+
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
4111
6033
|
if (resolved.some((t) => !t)) {
|
|
4112
6034
|
errors.push(`${label}: unknown task in reorder list`);
|
|
4113
6035
|
break;
|
|
@@ -4123,9 +6045,15 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4123
6045
|
break;
|
|
4124
6046
|
}
|
|
4125
6047
|
case "merge": {
|
|
4126
|
-
const
|
|
6048
|
+
const refs = (op.taskIds ?? []).map((r) => resolveRef(r, currentTasks, tasks, handleOwner, handleId, i));
|
|
6049
|
+
const badRef = refs.find((r) => "error" in r);
|
|
6050
|
+
if (badRef) {
|
|
6051
|
+
errors.push(`${label}: ${badRef.error}`);
|
|
6052
|
+
break;
|
|
6053
|
+
}
|
|
6054
|
+
const resolved = refs.map((r) => findTopLevel(tasks, r.id));
|
|
4127
6055
|
if (resolved.some((t) => !t)) {
|
|
4128
|
-
errors.push(`${label}: unknown task in merge list`);
|
|
6056
|
+
errors.push(`${label}: unknown task in merge list (already removed, merged, or split by an earlier op)`);
|
|
4129
6057
|
break;
|
|
4130
6058
|
}
|
|
4131
6059
|
const toMerge = resolved.map((t) => t).sort((a, b) => a.order - b.order);
|
|
@@ -4140,7 +6068,7 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4140
6068
|
}
|
|
4141
6069
|
const idSet = new Set(toMerge.map((t) => t.id));
|
|
4142
6070
|
const unionDepsRaw = [...new Set(toMerge.flatMap((t) => t.dependencies))].filter((d) => !idSet.has(d));
|
|
4143
|
-
const { ids, bad } = resolveDeps(unionDepsRaw);
|
|
6071
|
+
const { ids, bad } = resolveDeps(unionDepsRaw, i);
|
|
4144
6072
|
if (bad.length) {
|
|
4145
6073
|
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
4146
6074
|
break;
|
|
@@ -4181,11 +6109,17 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4181
6109
|
}
|
|
4182
6110
|
if (!inserted) result.push(mergedTask);
|
|
4183
6111
|
tasks = renumberTasks(result);
|
|
6112
|
+
if (op.handle) handleId.set(op.handle, mergedId);
|
|
4184
6113
|
summary.push(`Merged ${toMerge.length} tasks into "${spec.title}"`);
|
|
4185
6114
|
break;
|
|
4186
6115
|
}
|
|
4187
6116
|
case "split": {
|
|
4188
|
-
const
|
|
6117
|
+
const ref = resolveRef(op.taskId, currentTasks, tasks, handleOwner, handleId, i);
|
|
6118
|
+
if ("error" in ref) {
|
|
6119
|
+
errors.push(`${label}: ${ref.error}`);
|
|
6120
|
+
break;
|
|
6121
|
+
}
|
|
6122
|
+
const target = findTopLevel(tasks, ref.id);
|
|
4189
6123
|
if (!target) {
|
|
4190
6124
|
errors.push(`${label}: task "${op.taskId}" not found`);
|
|
4191
6125
|
break;
|
|
@@ -4206,16 +6140,16 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4206
6140
|
const newTasks = [];
|
|
4207
6141
|
let prevId;
|
|
4208
6142
|
let buildError = false;
|
|
4209
|
-
for (let
|
|
4210
|
-
const spec = parts[
|
|
6143
|
+
for (let p = 0; p < parts.length; p++) {
|
|
6144
|
+
const spec = parts[p];
|
|
4211
6145
|
if (!spec || !spec.title) {
|
|
4212
|
-
errors.push(`${label}: split part ${
|
|
6146
|
+
errors.push(`${label}: split part ${p + 1} needs a title`);
|
|
4213
6147
|
buildError = true;
|
|
4214
6148
|
break;
|
|
4215
6149
|
}
|
|
4216
6150
|
let deps;
|
|
4217
|
-
if (
|
|
4218
|
-
const { ids, bad } = resolveDeps(target.dependencies);
|
|
6151
|
+
if (p === 0) {
|
|
6152
|
+
const { ids, bad } = resolveDeps(target.dependencies, i);
|
|
4219
6153
|
if (bad.length) {
|
|
4220
6154
|
errors.push(`${label}: split part 1 unknown dependencies: ${bad.join(", ")}`);
|
|
4221
6155
|
buildError = true;
|
|
@@ -4253,9 +6187,60 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4253
6187
|
result.push({ ...t, dependencies: t.dependencies.map((d) => d === target.id ? tailId : d) });
|
|
4254
6188
|
}
|
|
4255
6189
|
tasks = renumberTasks(result);
|
|
6190
|
+
if (op.handle) handleId.set(op.handle, tailId);
|
|
4256
6191
|
summary.push(`Split "${target.title}" into ${newTasks.length} tasks`);
|
|
4257
6192
|
break;
|
|
4258
6193
|
}
|
|
6194
|
+
case "rearm": {
|
|
6195
|
+
const ref = resolveRef(op.taskId, originalFlatAll, flattenTasks(tasks), handleOwner, handleId, i);
|
|
6196
|
+
if ("error" in ref) {
|
|
6197
|
+
errors.push(`${label}: ${ref.error}`);
|
|
6198
|
+
break;
|
|
6199
|
+
}
|
|
6200
|
+
const target = findInTasks(tasks, ref.id);
|
|
6201
|
+
if (!target) {
|
|
6202
|
+
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
6203
|
+
break;
|
|
6204
|
+
}
|
|
6205
|
+
if (target.status === "in_progress") {
|
|
6206
|
+
errors.push(`${label}: "${target.title}" is running and cannot be re-armed`);
|
|
6207
|
+
break;
|
|
6208
|
+
}
|
|
6209
|
+
const changes = {};
|
|
6210
|
+
for (const key of UPDATABLE_FIELDS) {
|
|
6211
|
+
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
6212
|
+
}
|
|
6213
|
+
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
6214
|
+
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
6215
|
+
break;
|
|
6216
|
+
}
|
|
6217
|
+
const changesForCheck = { ...changes };
|
|
6218
|
+
delete changesForCheck.dependencies;
|
|
6219
|
+
const check = validateTaskEdit("direct", flattenTasks(tasks), target.id, changesForCheck, catalog);
|
|
6220
|
+
if (!check.ok) {
|
|
6221
|
+
errors.push(`${label}: ${check.error}`);
|
|
6222
|
+
break;
|
|
6223
|
+
}
|
|
6224
|
+
for (const field of check.clear ?? []) changes[field] = void 0;
|
|
6225
|
+
if ("dependencies" in changes) {
|
|
6226
|
+
const { ids, bad } = resolveDeps(changes.dependencies, i);
|
|
6227
|
+
if (bad.length) {
|
|
6228
|
+
errors.push(`${label}: unknown dependencies: ${bad.join(", ")}`);
|
|
6229
|
+
break;
|
|
6230
|
+
}
|
|
6231
|
+
changes.dependencies = ids.filter((id) => id !== target.id);
|
|
6232
|
+
}
|
|
6233
|
+
tasks = updateTaskInPlan(tasks, target.id, { ...changes, status: "pending", verdict: void 0, outputSummary: void 0 });
|
|
6234
|
+
for (const dep of flattenTasks(tasks)) {
|
|
6235
|
+
if (dep.status === "blocked" && dep.dependencies.includes(target.id)) {
|
|
6236
|
+
tasks = updateTaskInPlan(tasks, dep.id, { status: "pending" });
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
const clearedNote = check.clear?.length ? `; cleared ${check.clear.join(", ")}` : "";
|
|
6240
|
+
const changedNote = Object.keys(changes).length ? ` (${Object.keys(changes).join(", ")})` : "";
|
|
6241
|
+
summary.push(`Re-armed "${target.title}"${changedNote}${clearedNote}`);
|
|
6242
|
+
break;
|
|
6243
|
+
}
|
|
4259
6244
|
default:
|
|
4260
6245
|
errors.push(`${label}: unknown op "${op.op}"`);
|
|
4261
6246
|
}
|
|
@@ -4271,13 +6256,14 @@ function applyTaskOps(currentTasks, ops, runners) {
|
|
|
4271
6256
|
}
|
|
4272
6257
|
const cycleAt = detectCycle(flat);
|
|
4273
6258
|
if (cycleAt) errors.push(`Dependency cycle detected involving "${cycleAt}"`);
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
}
|
|
6259
|
+
if (!errors.length) {
|
|
6260
|
+
const repaired = repairOrder(tasks);
|
|
6261
|
+
if ("error" in repaired) {
|
|
6262
|
+
errors.push(repaired.error);
|
|
6263
|
+
} else {
|
|
6264
|
+
const moves = describeMoves(tasks, repaired.tasks);
|
|
6265
|
+
if (moves) summary.push(`Reordered to keep dependencies first: ${moves}`);
|
|
6266
|
+
tasks = renumberTasks(repaired.tasks);
|
|
4281
6267
|
}
|
|
4282
6268
|
}
|
|
4283
6269
|
}
|
|
@@ -4307,6 +6293,9 @@ function truncatedPlanReEmitPrompt(historyCompacted) {
|
|
|
4307
6293
|
function reEmitTaskOpsPrompt(detail) {
|
|
4308
6294
|
return `Your task edits could not be applied: ${detail}. Re-emit ONLY a corrected {"${TASK_OPS_ENVELOPE_KEY}":[...]} JSON object \u2014 no prose, no code fences \u2014 or reply in prose if you did not intend to edit tasks.`;
|
|
4309
6295
|
}
|
|
6296
|
+
function reEmitTaskQueryPrompt(detail) {
|
|
6297
|
+
return `Your task-detail request could not be read: ${detail}. Re-emit ONLY a corrected {"${TASK_QUERY_ENVELOPE_KEY}":{"tasks":["<id or #order>"],"fields"?:[...],"catalog"?:true}} JSON object \u2014 no prose, no code fences \u2014 or reply in prose if you did not mean to look anything up.`;
|
|
6298
|
+
}
|
|
4310
6299
|
function taskOpsRejectedPrompt(errors) {
|
|
4311
6300
|
return `Those task edits were rejected:
|
|
4312
6301
|
- ${errors.join("\n- ")}
|
|
@@ -4333,6 +6322,16 @@ function classifyPlannerReply(text, opts) {
|
|
|
4333
6322
|
}
|
|
4334
6323
|
}
|
|
4335
6324
|
}
|
|
6325
|
+
if (textHasTaskQuery(text)) {
|
|
6326
|
+
try {
|
|
6327
|
+
return { kind: "task_query", query: parseTaskQueryJson(text) };
|
|
6328
|
+
} catch (err) {
|
|
6329
|
+
if (!(err instanceof PlanParseError)) throw err;
|
|
6330
|
+
if (extractObjectsWithKey(text, TASK_QUERY_ENVELOPE_KEY).matches.length > 0) {
|
|
6331
|
+
return { kind: "broken_task_query", error: err };
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
4336
6335
|
if (text.includes(`"${PLAN_ENVELOPE_KEY}"`)) {
|
|
4337
6336
|
try {
|
|
4338
6337
|
return { kind: "plan", tasks: parsePlanJson(text, opts.runners, opts.runnerModes, opts.autonomousDefault) };
|
|
@@ -4976,7 +6975,7 @@ var BaseAiService = class _BaseAiService {
|
|
|
4976
6975
|
const parallelIndexes = toolCalls.flatMap((tc, i) => PARALLEL_SAFE_TOOLS.has(tc.name) ? [i] : []);
|
|
4977
6976
|
await Promise.all([
|
|
4978
6977
|
spawnIndexes.length > 0 ? mapWithConcurrency(spawnIndexes, SUBAGENT_LIMITS.maxConcurrent, async (i) => {
|
|
4979
|
-
const subagentId = (0,
|
|
6978
|
+
const subagentId = (0, import_uuid3.v4)();
|
|
4980
6979
|
spawnSubagentIds.set(i, subagentId);
|
|
4981
6980
|
onProgress({ type: "tool_call", tool: toolCalls[i].name, toolArgs: JSON.stringify(toolCalls[i].args), subagentId, toolCallId: toolCalls[i].id });
|
|
4982
6981
|
precomputed.set(i, await this.executeSpawnAgent({ ...toolCalls[i].args }, fs10, onProgress, signal, subagentId));
|
|
@@ -5131,6 +7130,10 @@ ${llmOutput}
|
|
|
5131
7130
|
switch (reply.kind) {
|
|
5132
7131
|
case "task_ops":
|
|
5133
7132
|
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
7133
|
+
// A read is settled by the Session (it owns the plan and the catalog),
|
|
7134
|
+
// so it leaves this loop the same way a plan or an edit does.
|
|
7135
|
+
case "task_query":
|
|
7136
|
+
return { kind: "task_query", query: reply.query, text: turn.text, researchLog };
|
|
5134
7137
|
case "plan":
|
|
5135
7138
|
if (ctx.prdEnabled && !ctx.prdCaptured && !ctx.prdNudgeSent && !signal?.aborted) {
|
|
5136
7139
|
ctx.prdNudgeSent = true;
|
|
@@ -5153,6 +7156,13 @@ ${llmOutput}
|
|
|
5153
7156
|
continue;
|
|
5154
7157
|
}
|
|
5155
7158
|
break;
|
|
7159
|
+
case "broken_task_query":
|
|
7160
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
7161
|
+
jsonRepairAttempts++;
|
|
7162
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
7163
|
+
continue;
|
|
7164
|
+
}
|
|
7165
|
+
break;
|
|
5156
7166
|
case "broken_plan":
|
|
5157
7167
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS2 && !signal?.aborted) {
|
|
5158
7168
|
jsonRepairAttempts++;
|
|
@@ -5955,11 +7965,11 @@ function composeAugmentedPrompt(task, allTasks, opts) {
|
|
|
5955
7965
|
if (isHitlTask(task)) {
|
|
5956
7966
|
blocks.push(renderCheckpointInstruction());
|
|
5957
7967
|
}
|
|
5958
|
-
const
|
|
5959
|
-
if (blocks.length === 0) return basePrompt +
|
|
7968
|
+
const marker2 = renderCompletionMarker(task);
|
|
7969
|
+
if (blocks.length === 0) return basePrompt + marker2;
|
|
5960
7970
|
return `${blocks.join("\n\n")}
|
|
5961
7971
|
|
|
5962
|
-
${basePrompt}${
|
|
7972
|
+
${basePrompt}${marker2}`;
|
|
5963
7973
|
}
|
|
5964
7974
|
|
|
5965
7975
|
// src/services/VerdictEngine.ts
|
|
@@ -7132,111 +9142,6 @@ ${output.slice(-3e3)}`);
|
|
|
7132
9142
|
}
|
|
7133
9143
|
};
|
|
7134
9144
|
|
|
7135
|
-
// src/services/ModelAllowlistResolver.ts
|
|
7136
|
-
function effectiveAllowlist(allowlist, runner, modelsByRunner) {
|
|
7137
|
-
if (!allowlist || allowlist.length === 0) return void 0;
|
|
7138
|
-
if (!modelsByRunner) return allowlist;
|
|
7139
|
-
const own = new Set((modelsByRunner[runner] ?? []).map((m) => m.modelId));
|
|
7140
|
-
const elsewhere = /* @__PURE__ */ new Set();
|
|
7141
|
-
for (const [other, models] of Object.entries(modelsByRunner)) {
|
|
7142
|
-
if (other === runner) continue;
|
|
7143
|
-
for (const m of models ?? []) elsewhere.add(m.modelId);
|
|
7144
|
-
}
|
|
7145
|
-
const kept = allowlist.filter((id) => own.has(id) || !elsewhere.has(id));
|
|
7146
|
-
return kept.length > 0 ? kept : void 0;
|
|
7147
|
-
}
|
|
7148
|
-
function filterModelsForPrompt(modelsByRunner, perRunnerAllowlist) {
|
|
7149
|
-
const result = {};
|
|
7150
|
-
for (const [runner, models] of Object.entries(modelsByRunner)) {
|
|
7151
|
-
if (!models) continue;
|
|
7152
|
-
const allowed = effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
7153
|
-
if (!allowed) {
|
|
7154
|
-
result[runner] = models;
|
|
7155
|
-
continue;
|
|
7156
|
-
}
|
|
7157
|
-
const allowedSet = new Set(allowed);
|
|
7158
|
-
const intersection = models.filter((m) => allowedSet.has(m.modelId));
|
|
7159
|
-
const covered = new Set(intersection.map((m) => m.modelId));
|
|
7160
|
-
const missing = allowed.filter((id) => !covered.has(id)).map((id) => ({ modelId: id, modelLabel: id.split("/").pop() || id, variants: [] }));
|
|
7161
|
-
result[runner] = [...intersection, ...missing];
|
|
7162
|
-
}
|
|
7163
|
-
return result;
|
|
7164
|
-
}
|
|
7165
|
-
var EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
7166
|
-
var EFFORT_ALIASES = { disabled: "none", adaptive: "medium" };
|
|
7167
|
-
function clampThinkingEffort(effort, variants) {
|
|
7168
|
-
if (!effort) return void 0;
|
|
7169
|
-
if (variants.length === 0) return void 0;
|
|
7170
|
-
const ids = new Set(variants.map((v) => v.id));
|
|
7171
|
-
if (ids.has(effort)) return effort;
|
|
7172
|
-
const canon = EFFORT_ALIASES[effort] ?? effort;
|
|
7173
|
-
if (ids.has(canon)) return canon;
|
|
7174
|
-
const idx = EFFORT_LADDER.indexOf(canon);
|
|
7175
|
-
if (idx < 0) return void 0;
|
|
7176
|
-
for (let d = 1; d < EFFORT_LADDER.length; d++) {
|
|
7177
|
-
const lower = EFFORT_LADDER[idx - d];
|
|
7178
|
-
if (lower && ids.has(lower)) return lower;
|
|
7179
|
-
const higher = EFFORT_LADDER[idx + d];
|
|
7180
|
-
if (higher && ids.has(higher)) return higher;
|
|
7181
|
-
}
|
|
7182
|
-
return void 0;
|
|
7183
|
-
}
|
|
7184
|
-
function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRunner) {
|
|
7185
|
-
const clampVariant = (task) => {
|
|
7186
|
-
if (!task.assignedModel || !modelsByRunner) return task;
|
|
7187
|
-
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
7188
|
-
(m) => m.modelId === task.assignedModel.modelId
|
|
7189
|
-
);
|
|
7190
|
-
if (!model) return task;
|
|
7191
|
-
const clamped = clampThinkingEffort(task.assignedModel.thinkingEffort, model.variants);
|
|
7192
|
-
return {
|
|
7193
|
-
...task,
|
|
7194
|
-
assignedModel: {
|
|
7195
|
-
...task.assignedModel,
|
|
7196
|
-
thinkingEffort: clamped,
|
|
7197
|
-
availableVariants: model.variants.map((v) => v.id)
|
|
7198
|
-
}
|
|
7199
|
-
};
|
|
7200
|
-
};
|
|
7201
|
-
const allowedFor = (runner) => effectiveAllowlist(perRunnerAllowlist[runner], runner, modelsByRunner);
|
|
7202
|
-
const assignmentFor = (runner, modelId) => {
|
|
7203
|
-
const known = modelsByRunner?.[runner]?.find((m) => m.modelId === modelId);
|
|
7204
|
-
return {
|
|
7205
|
-
modelId,
|
|
7206
|
-
modelLabel: known?.modelLabel ?? modelId,
|
|
7207
|
-
thinkingEffort: void 0,
|
|
7208
|
-
...known ? { availableVariants: known.variants.map((v) => v.id) } : {}
|
|
7209
|
-
};
|
|
7210
|
-
};
|
|
7211
|
-
return tasks.map((task) => {
|
|
7212
|
-
if (task.type === "user") return task;
|
|
7213
|
-
if (allowedRunners && !allowedRunners.includes(task.assignedRunner)) {
|
|
7214
|
-
const fallbackRunner = allowedRunners[0];
|
|
7215
|
-
const fallbackModels = allowedFor(fallbackRunner);
|
|
7216
|
-
const fallbackModel = fallbackModels?.length ? assignmentFor(fallbackRunner, fallbackModels[0]) : modelsByRunner?.[fallbackRunner]?.[0] ? assignmentFor(fallbackRunner, modelsByRunner[fallbackRunner][0].modelId) : task.assignedModel;
|
|
7217
|
-
return clampVariant({
|
|
7218
|
-
...task,
|
|
7219
|
-
assignedRunner: fallbackRunner,
|
|
7220
|
-
assignedModel: fallbackModel
|
|
7221
|
-
});
|
|
7222
|
-
}
|
|
7223
|
-
const allowlist = allowedFor(task.assignedRunner);
|
|
7224
|
-
if (!task.assignedModel) {
|
|
7225
|
-
if (allowlist && allowlist.length > 0) {
|
|
7226
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) });
|
|
7227
|
-
}
|
|
7228
|
-
const discovered = modelsByRunner?.[task.assignedRunner]?.[0];
|
|
7229
|
-
if (discovered) {
|
|
7230
|
-
return clampVariant({ ...task, assignedModel: assignmentFor(task.assignedRunner, discovered.modelId) });
|
|
7231
|
-
}
|
|
7232
|
-
return task;
|
|
7233
|
-
}
|
|
7234
|
-
if (!allowlist || allowlist.length === 0) return clampVariant(task);
|
|
7235
|
-
if (allowlist.includes(task.assignedModel.modelId)) return clampVariant(task);
|
|
7236
|
-
return { ...task, assignedModel: assignmentFor(task.assignedRunner, allowlist[0]) };
|
|
7237
|
-
});
|
|
7238
|
-
}
|
|
7239
|
-
|
|
7240
9145
|
// src/services/Planner.ts
|
|
7241
9146
|
function plannerModesWith(req) {
|
|
7242
9147
|
const modes = req.modes ?? DEFAULT_PLANNER_MODES;
|
|
@@ -8906,7 +10811,7 @@ function assertWorkspaceIsProject(workspace, deps = {}) {
|
|
|
8906
10811
|
throw new WorkspaceNotAProjectError(workspace);
|
|
8907
10812
|
}
|
|
8908
10813
|
const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
|
|
8909
|
-
(
|
|
10814
|
+
(marker2) => exists(path9.join(resolved, marker2))
|
|
8910
10815
|
);
|
|
8911
10816
|
if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
|
|
8912
10817
|
}
|
|
@@ -8948,12 +10853,19 @@ var StdioAgentAdapter = class {
|
|
|
8948
10853
|
/** Set for the duration of a turn. Outside one, events are buffered rather than dropped. */
|
|
8949
10854
|
turnEmit = null;
|
|
8950
10855
|
/**
|
|
8951
|
-
* Events the agent produced
|
|
8952
|
-
* that arrive during the handshake. Dropping them hid the one
|
|
8953
|
-
* that explains a planner which cannot read the workspace, so they
|
|
8954
|
-
* until a turn exists to show them in.
|
|
10856
|
+
* Events the agent produced before its first turn — in practice the startup
|
|
10857
|
+
* warnings that arrive during the handshake. Dropping them hid the one
|
|
10858
|
+
* diagnostic that explains a planner which cannot read the workspace, so they
|
|
10859
|
+
* are held until a turn exists to show them in.
|
|
10860
|
+
*
|
|
10861
|
+
* Only before the *first* turn. Once one has run, anything arriving outside a
|
|
10862
|
+
* turn is that closed turn's straggling work — a backgrounded subagent
|
|
10863
|
+
* finishing, a late read — and replaying it into the next turn presents it as
|
|
10864
|
+
* research done for the user's new message.
|
|
8955
10865
|
*/
|
|
8956
10866
|
betweenTurns = [];
|
|
10867
|
+
/** Set once a turn has ended: after that, out-of-turn events are stale, not startup. */
|
|
10868
|
+
hadTurn = false;
|
|
8957
10869
|
disposed = false;
|
|
8958
10870
|
/** Resolves when the process ends, so a handshake can lose the race instead of waiting out its timeout. */
|
|
8959
10871
|
processEnded;
|
|
@@ -8988,7 +10900,7 @@ var StdioAgentAdapter = class {
|
|
|
8988
10900
|
this.process.stdout?.on("data", (chunk) => {
|
|
8989
10901
|
this.stdout.push(chunk.toString(), (line) => this.handleLine(line, (event) => {
|
|
8990
10902
|
if (this.turnEmit) this.turnEmit(event);
|
|
8991
|
-
else if (!TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
10903
|
+
else if (!this.hadTurn && !TURN_SCOPED_EVENTS.has(event.type) && this.betweenTurns.length < BETWEEN_TURN_EVENT_CAP) {
|
|
8992
10904
|
this.betweenTurns.push(event);
|
|
8993
10905
|
}
|
|
8994
10906
|
}));
|
|
@@ -9020,6 +10932,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
|
|
|
9020
10932
|
if (settled) return;
|
|
9021
10933
|
settled = true;
|
|
9022
10934
|
this.turnEmit = null;
|
|
10935
|
+
this.hadTurn = true;
|
|
9023
10936
|
signal?.removeEventListener("abort", onAbort);
|
|
9024
10937
|
this.process?.removeListener("exit", onExit);
|
|
9025
10938
|
this.process?.removeListener("error", onExit);
|
|
@@ -9098,6 +11011,7 @@ ${tail}` : ""}`;
|
|
|
9098
11011
|
|
|
9099
11012
|
// src/services/harness/ClaudeCodeAdapter.ts
|
|
9100
11013
|
var DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "KillShell"];
|
|
11014
|
+
var ASYNC_LAUNCH_MARKER = "Async agent launched successfully";
|
|
9101
11015
|
function flattenContent(content) {
|
|
9102
11016
|
if (typeof content === "string") return content;
|
|
9103
11017
|
if (Array.isArray(content)) {
|
|
@@ -9107,6 +11021,8 @@ function flattenContent(content) {
|
|
|
9107
11021
|
}
|
|
9108
11022
|
var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
9109
11023
|
agentId = "claude-code";
|
|
11024
|
+
/** Whether this turn has already emitted reply text — see {@link handleLine}. */
|
|
11025
|
+
turnHasText = false;
|
|
9110
11026
|
spawnSpec(opts) {
|
|
9111
11027
|
const args = [
|
|
9112
11028
|
"-p",
|
|
@@ -9131,6 +11047,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9131
11047
|
return { command: "claude", args };
|
|
9132
11048
|
}
|
|
9133
11049
|
turnPayload(message) {
|
|
11050
|
+
this.turnHasText = false;
|
|
9134
11051
|
return `${JSON.stringify({
|
|
9135
11052
|
type: "user",
|
|
9136
11053
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
@@ -9141,6 +11058,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9141
11058
|
const msg = StdioAgentAdapter.parse(line);
|
|
9142
11059
|
if (!msg) return;
|
|
9143
11060
|
if (msg.session_id) this.sessionId = msg.session_id;
|
|
11061
|
+
if (msg.parent_tool_use_id) return;
|
|
9144
11062
|
switch (msg.type) {
|
|
9145
11063
|
// The control channel: Claude asks whether a tool may run when its mode
|
|
9146
11064
|
// cannot decide alone. A read-only planner answers "deny", every time —
|
|
@@ -9165,8 +11083,12 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9165
11083
|
}
|
|
9166
11084
|
case "assistant":
|
|
9167
11085
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
9168
|
-
if (block.type === "text" && block.text)
|
|
9169
|
-
|
|
11086
|
+
if (block.type === "text" && block.text) {
|
|
11087
|
+
emit({ type: "assistant_text", text: this.turnHasText ? `
|
|
11088
|
+
|
|
11089
|
+
${block.text}` : block.text });
|
|
11090
|
+
this.turnHasText = true;
|
|
11091
|
+
} else if (block.type === "thinking" && block.thinking) emit({ type: "thinking", text: block.thinking });
|
|
9170
11092
|
else if (block.type === "tool_use" && block.name) {
|
|
9171
11093
|
emit({ type: "tool_call", id: block.id ?? block.name, name: block.name, args: block.input ?? {} });
|
|
9172
11094
|
}
|
|
@@ -9175,11 +11097,15 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9175
11097
|
case "user":
|
|
9176
11098
|
for (const block of Array.isArray(msg.message?.content) ? msg.message.content : []) {
|
|
9177
11099
|
if (block.type !== "tool_result") continue;
|
|
11100
|
+
const output = flattenContent(block.content);
|
|
11101
|
+
if (output.includes(ASYNC_LAUNCH_MARKER)) {
|
|
11102
|
+
emit({ type: "background_agent", id: block.tool_use_id ?? "" });
|
|
11103
|
+
}
|
|
9178
11104
|
emit({
|
|
9179
11105
|
type: "tool_result",
|
|
9180
11106
|
id: block.tool_use_id ?? "",
|
|
9181
11107
|
name: "",
|
|
9182
|
-
output
|
|
11108
|
+
output,
|
|
9183
11109
|
success: block.is_error !== true
|
|
9184
11110
|
});
|
|
9185
11111
|
}
|
|
@@ -9599,6 +11525,8 @@ var OpenCodeAdapter = class {
|
|
|
9599
11525
|
exited = false;
|
|
9600
11526
|
disposed = false;
|
|
9601
11527
|
opts = null;
|
|
11528
|
+
/** Whether this turn has already emitted reply text — see {@link emitPart}. */
|
|
11529
|
+
turnHasText = false;
|
|
9602
11530
|
async start(opts) {
|
|
9603
11531
|
this.opts = opts;
|
|
9604
11532
|
assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
|
|
@@ -9666,6 +11594,7 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
9666
11594
|
return;
|
|
9667
11595
|
}
|
|
9668
11596
|
const seen = /* @__PURE__ */ new Set();
|
|
11597
|
+
this.turnHasText = false;
|
|
9669
11598
|
const streamAbort = new AbortController();
|
|
9670
11599
|
let connected = () => {
|
|
9671
11600
|
};
|
|
@@ -9734,7 +11663,10 @@ ${this.stderrTail.trim()}` : ""}`);
|
|
|
9734
11663
|
if (part.type === "text" && part.text) {
|
|
9735
11664
|
if (seen.has(`text:${id}`)) return;
|
|
9736
11665
|
seen.add(`text:${id}`);
|
|
9737
|
-
onEvent({ type: "assistant_text", text:
|
|
11666
|
+
onEvent({ type: "assistant_text", text: this.turnHasText ? `
|
|
11667
|
+
|
|
11668
|
+
${part.text}` : part.text });
|
|
11669
|
+
this.turnHasText = true;
|
|
9738
11670
|
return;
|
|
9739
11671
|
}
|
|
9740
11672
|
if (part.type === "reasoning" && part.text) {
|
|
@@ -9901,6 +11833,7 @@ function normalizeAgentArgs(tool, args) {
|
|
|
9901
11833
|
|
|
9902
11834
|
// src/services/harness/CliAgentAiService.ts
|
|
9903
11835
|
var MAX_JSON_REPAIRS = 2;
|
|
11836
|
+
var MAX_AGENT_WAITS = 2;
|
|
9904
11837
|
var LOG_MAX_CHARS = 1e4;
|
|
9905
11838
|
function defaultAdapter(runner, deps) {
|
|
9906
11839
|
switch (runner) {
|
|
@@ -10041,13 +11974,16 @@ var CliAgentAiService = class {
|
|
|
10041
11974
|
let pending = message;
|
|
10042
11975
|
let emptyNudgeSent = false;
|
|
10043
11976
|
let jsonRepairAttempts = 0;
|
|
11977
|
+
let agentWaits = 0;
|
|
11978
|
+
const carried = [];
|
|
11979
|
+
const replyText = (text) => [...carried, text].filter((part) => part.trim()).join("\n\n");
|
|
10044
11980
|
try {
|
|
10045
11981
|
for (; ; ) {
|
|
10046
11982
|
const turn = await this.runTurn(pending, onProgress, combined);
|
|
10047
11983
|
researchLog.push(...turn.researchLog);
|
|
10048
11984
|
if (turn.aborted || combined?.aborted) {
|
|
10049
11985
|
onProgress({ type: "interrupted" });
|
|
10050
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
11986
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
10051
11987
|
}
|
|
10052
11988
|
if (turn.error) {
|
|
10053
11989
|
return { kind: "message", text: turn.error, researchLog };
|
|
@@ -10065,6 +12001,18 @@ var CliAgentAiService = class {
|
|
|
10065
12001
|
researchLog
|
|
10066
12002
|
};
|
|
10067
12003
|
}
|
|
12004
|
+
if (turn.backgroundAgents > 0 && agentWaits < MAX_AGENT_WAITS && !combined?.aborted) {
|
|
12005
|
+
agentWaits++;
|
|
12006
|
+
carried.push(turn.text);
|
|
12007
|
+
pending = [
|
|
12008
|
+
`You ended your turn with ${turn.backgroundAgents} subagent(s) still running in the background.`,
|
|
12009
|
+
"Ordewell hands the conversation back to the user when your turn ends, so anything you say after it never reaches them \u2014",
|
|
12010
|
+
"the results you promised to report would be lost.",
|
|
12011
|
+
"Wait for those agents to finish NOW, in this reply, and do not end your turn until you have their results.",
|
|
12012
|
+
"Then give the user your synthesis. In future replies, await your agents inside the turn rather than backgrounding them."
|
|
12013
|
+
].join(" ");
|
|
12014
|
+
continue;
|
|
12015
|
+
}
|
|
10068
12016
|
if (extractPrdBlock(turn.text)) conversation.prdCaptured = true;
|
|
10069
12017
|
const reply = classifyPlannerReply(turn.text, {
|
|
10070
12018
|
runners: conversation.runners,
|
|
@@ -10073,7 +12021,11 @@ var CliAgentAiService = class {
|
|
|
10073
12021
|
});
|
|
10074
12022
|
switch (reply.kind) {
|
|
10075
12023
|
case "task_ops":
|
|
10076
|
-
return { kind: "task_ops", ops: reply.ops, text: turn.text, researchLog };
|
|
12024
|
+
return { kind: "task_ops", ops: reply.ops, text: replyText(turn.text), researchLog };
|
|
12025
|
+
// The read channel is a text envelope precisely so it reaches here
|
|
12026
|
+
// too: a harness planner has no Ordewell tool loop to call into.
|
|
12027
|
+
case "task_query":
|
|
12028
|
+
return { kind: "task_query", query: reply.query, text: replyText(turn.text), researchLog };
|
|
10077
12029
|
case "plan":
|
|
10078
12030
|
if (conversation.prdEnabled && !conversation.prdCaptured && !conversation.prdNudgeSent && !combined?.aborted) {
|
|
10079
12031
|
conversation.prdNudgeSent = true;
|
|
@@ -10086,7 +12038,7 @@ var CliAgentAiService = class {
|
|
|
10086
12038
|
continue;
|
|
10087
12039
|
}
|
|
10088
12040
|
this.conversation = null;
|
|
10089
|
-
return { kind: "plan", tasks: reply.tasks, text: turn.text, researchLog };
|
|
12041
|
+
return { kind: "plan", tasks: reply.tasks, text: replyText(turn.text), researchLog };
|
|
10090
12042
|
case "broken_task_ops":
|
|
10091
12043
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
10092
12044
|
jsonRepairAttempts++;
|
|
@@ -10094,6 +12046,13 @@ var CliAgentAiService = class {
|
|
|
10094
12046
|
continue;
|
|
10095
12047
|
}
|
|
10096
12048
|
break;
|
|
12049
|
+
case "broken_task_query":
|
|
12050
|
+
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
12051
|
+
jsonRepairAttempts++;
|
|
12052
|
+
pending = reEmitTaskQueryPrompt(reply.error.message);
|
|
12053
|
+
continue;
|
|
12054
|
+
}
|
|
12055
|
+
break;
|
|
10097
12056
|
case "broken_plan":
|
|
10098
12057
|
if (jsonRepairAttempts < MAX_JSON_REPAIRS && !combined?.aborted) {
|
|
10099
12058
|
jsonRepairAttempts++;
|
|
@@ -10104,7 +12063,7 @@ var CliAgentAiService = class {
|
|
|
10104
12063
|
case "prose":
|
|
10105
12064
|
break;
|
|
10106
12065
|
}
|
|
10107
|
-
return { kind: "message", text: turn.text, researchLog };
|
|
12066
|
+
return { kind: "message", text: replyText(turn.text), researchLog };
|
|
10108
12067
|
}
|
|
10109
12068
|
} finally {
|
|
10110
12069
|
this.activeAbort = null;
|
|
@@ -10124,9 +12083,11 @@ var CliAgentAiService = class {
|
|
|
10124
12083
|
let text = "";
|
|
10125
12084
|
let error;
|
|
10126
12085
|
let stepIndex = 0;
|
|
12086
|
+
let backgroundAgents = 0;
|
|
10127
12087
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
10128
12088
|
[... truncated, total ${value.length} chars]` : value;
|
|
10129
|
-
const settle = (id,
|
|
12089
|
+
const settle = (id, rawOutput, success, outcome) => {
|
|
12090
|
+
const output = redactSecrets(rawOutput);
|
|
10130
12091
|
const call = pendingCalls.get(id);
|
|
10131
12092
|
pendingCalls.delete(id);
|
|
10132
12093
|
const step = {
|
|
@@ -10162,6 +12123,9 @@ var CliAgentAiService = class {
|
|
|
10162
12123
|
case "tool_result":
|
|
10163
12124
|
settle(event.id, event.output, event.success, event.success ? "success" : "failure");
|
|
10164
12125
|
return;
|
|
12126
|
+
case "background_agent":
|
|
12127
|
+
backgroundAgents++;
|
|
12128
|
+
return;
|
|
10165
12129
|
case "permission_request": {
|
|
10166
12130
|
const mapped = mapAgentTool(event.name);
|
|
10167
12131
|
const args = JSON.stringify({ detail: event.detail });
|
|
@@ -10192,7 +12156,7 @@ var CliAgentAiService = class {
|
|
|
10192
12156
|
adapter.dispose();
|
|
10193
12157
|
this.adapter = null;
|
|
10194
12158
|
}
|
|
10195
|
-
return { text, researchLog, error, aborted: signal?.aborted };
|
|
12159
|
+
return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
10196
12160
|
}
|
|
10197
12161
|
// --- Process lifecycle ---
|
|
10198
12162
|
async startAdapter(opts) {
|
|
@@ -10657,6 +12621,16 @@ function ensureDir(dir) {
|
|
|
10657
12621
|
function getStateDir(baseDir) {
|
|
10658
12622
|
return path10.join(baseDir ?? process.cwd(), STATE_DIR);
|
|
10659
12623
|
}
|
|
12624
|
+
var IGNORE_FILE = ".gitignore";
|
|
12625
|
+
function ensureStateDirIgnored(baseDir) {
|
|
12626
|
+
const dir = getStateDir(baseDir);
|
|
12627
|
+
ensureDir(dir);
|
|
12628
|
+
try {
|
|
12629
|
+
fs4.writeFileSync(path10.join(dir, IGNORE_FILE), "*\n", { flag: "wx" });
|
|
12630
|
+
} catch (err) {
|
|
12631
|
+
if (err.code !== "EEXIST") throw err;
|
|
12632
|
+
}
|
|
12633
|
+
}
|
|
10660
12634
|
|
|
10661
12635
|
// src/utils/sessionId.ts
|
|
10662
12636
|
var import_crypto = require("crypto");
|
|
@@ -10686,6 +12660,7 @@ function sessionFilename(session) {
|
|
|
10686
12660
|
function saveSession(plan, goal, baseDir, id) {
|
|
10687
12661
|
const sessionsDir = getSessionsDir(baseDir);
|
|
10688
12662
|
ensureDir(sessionsDir);
|
|
12663
|
+
ensureStateDirIgnored(baseDir);
|
|
10689
12664
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10690
12665
|
const meta = {
|
|
10691
12666
|
id: id || mintSessionId(),
|
|
@@ -10788,6 +12763,12 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
|
|
|
10788
12763
|
}
|
|
10789
12764
|
|
|
10790
12765
|
// src/services/createSession.ts
|
|
12766
|
+
var CATALOG_MODEL_CAP = 100;
|
|
12767
|
+
var MAX_TASK_QUERIES = 3;
|
|
12768
|
+
var MAX_TASK_QUERIES_HARD = 6;
|
|
12769
|
+
function freshReadBudget() {
|
|
12770
|
+
return { answered: 0, seen: /* @__PURE__ */ new Set() };
|
|
12771
|
+
}
|
|
10791
12772
|
var PlanEditError = class extends Error {
|
|
10792
12773
|
constructor(message) {
|
|
10793
12774
|
super(message);
|
|
@@ -11024,6 +13005,21 @@ var Session = class {
|
|
|
11024
13005
|
runnerModesFor(runners) {
|
|
11025
13006
|
return runnerModesFrom(this.registry, runners);
|
|
11026
13007
|
}
|
|
13008
|
+
/**
|
|
13009
|
+
* The catalog a model/task-mode edit is checked against — the same
|
|
13010
|
+
* discovered models and manifest modes the per-turn catalog block shows the
|
|
13011
|
+
* planner, so a refusal here can never name something invalid that the
|
|
13012
|
+
* planner was never told about. `modelsCache` is read live, not filtered by
|
|
13013
|
+
* the allowlist here — {@link checkModelAndModeValidity} narrows by
|
|
13014
|
+
* allowlist itself, the same way `coerceAssignments` does.
|
|
13015
|
+
*/
|
|
13016
|
+
editCatalog() {
|
|
13017
|
+
return {
|
|
13018
|
+
modelsByRunner: this.modelsCache,
|
|
13019
|
+
runnerModes: this.runnerModesFor(this.plan?.runners ?? []),
|
|
13020
|
+
perRunnerAllowlist: this.settingsFn().modelAllowlist ?? this.currentAllowlist
|
|
13021
|
+
};
|
|
13022
|
+
}
|
|
11027
13023
|
get planState() {
|
|
11028
13024
|
return this.plan;
|
|
11029
13025
|
}
|
|
@@ -11203,10 +13199,12 @@ var Session = class {
|
|
|
11203
13199
|
async continueConversation(userMessage, options) {
|
|
11204
13200
|
if (!this.plan) throw new Error("No planning conversation to continue");
|
|
11205
13201
|
const priorHistory = this.plan.conversationHistory ?? [];
|
|
13202
|
+
const priorResearchLog = this.plan.researchLog ?? [];
|
|
11206
13203
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11207
13204
|
this.appendTranscript("user", userMessage, now);
|
|
13205
|
+
const historyAfterOwnAppend = this.plan.conversationHistory;
|
|
11208
13206
|
this.plan.researchLog = [...this.plan.researchLog ?? [], { id: `up-${Date.now()}`, type: "user_prompt", content: userMessage, timestamp: now }];
|
|
11209
|
-
const contextBlock = this.planContextBlock();
|
|
13207
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock()].filter(Boolean).join("\n\n");
|
|
11210
13208
|
const outgoing = contextBlock ? `${contextBlock}
|
|
11211
13209
|
|
|
11212
13210
|
${userMessage}` : userMessage;
|
|
@@ -11219,6 +13217,8 @@ ${userMessage}` : userMessage;
|
|
|
11219
13217
|
(p) => this.translateProgress(p),
|
|
11220
13218
|
options?.signal
|
|
11221
13219
|
) : await this.resumeConversation(outgoing, priorHistory, options);
|
|
13220
|
+
const reads = freshReadBudget();
|
|
13221
|
+
turn = await this.drainTaskQueries(turn, reads, options);
|
|
11222
13222
|
if ((turn.kind === "task_ops" || turn.kind === "plan") && this.hasLiveWork) {
|
|
11223
13223
|
this.queueMessage(userMessage);
|
|
11224
13224
|
this.plan.queuedMessages = this.getQueuedMessages();
|
|
@@ -11228,11 +13228,74 @@ ${userMessage}` : userMessage;
|
|
|
11228
13228
|
researchLog: turn.researchLog
|
|
11229
13229
|
};
|
|
11230
13230
|
}
|
|
11231
|
-
return await this.settleTurn(turn, options);
|
|
13231
|
+
return await this.settleTurn(turn, options, reads);
|
|
13232
|
+
} catch (err) {
|
|
13233
|
+
if (this.plan && this.plan.conversationHistory === historyAfterOwnAppend) {
|
|
13234
|
+
this.plan.conversationHistory = priorHistory;
|
|
13235
|
+
this.plan.researchLog = priorResearchLog;
|
|
13236
|
+
}
|
|
13237
|
+
throw err;
|
|
11232
13238
|
} finally {
|
|
11233
13239
|
releaseAbort();
|
|
11234
13240
|
}
|
|
11235
13241
|
}
|
|
13242
|
+
/**
|
|
13243
|
+
* Answer every read the planner emits until it says something else.
|
|
13244
|
+
*
|
|
13245
|
+
* The channel is a text envelope rather than a registered tool because the
|
|
13246
|
+
* protocol has to be identical on both planner backends (ADR-0009): Ordewell
|
|
13247
|
+
* owns a tool loop only in the API case, and a harness planner running as a
|
|
13248
|
+
* coding-agent subprocess can only be reached this way.
|
|
13249
|
+
*
|
|
13250
|
+
* Draining here — outside {@link repairLoop} — is what keeps reads free of
|
|
13251
|
+
* the repair budget. A planner that looks a task up and *then* fumbles its
|
|
13252
|
+
* ops JSON still gets its two corrective retries; charging it for the read
|
|
13253
|
+
* would cost it the chance to fix the edit.
|
|
13254
|
+
*/
|
|
13255
|
+
async drainTaskQueries(turn, reads, options) {
|
|
13256
|
+
const carried = [];
|
|
13257
|
+
let current = turn;
|
|
13258
|
+
while (current.kind === "task_query") {
|
|
13259
|
+
carried.push(...current.researchLog);
|
|
13260
|
+
if (reads.answered >= MAX_TASK_QUERIES_HARD || !this.aiService.hasActiveConversation() || options?.signal?.aborted) {
|
|
13261
|
+
return {
|
|
13262
|
+
kind: "message",
|
|
13263
|
+
text: "The planner kept asking to read tasks instead of answering. Nothing was changed \u2014 ask again, or be more specific about the edit you want.",
|
|
13264
|
+
researchLog: carried
|
|
13265
|
+
};
|
|
13266
|
+
}
|
|
13267
|
+
const signature = taskQuerySignature(current.query);
|
|
13268
|
+
const insist = reads.seen.has(signature) || reads.answered >= MAX_TASK_QUERIES;
|
|
13269
|
+
reads.seen.add(signature);
|
|
13270
|
+
reads.answered++;
|
|
13271
|
+
const answer = this.taskQueryAnswer(current.query);
|
|
13272
|
+
current = await this.aiService.continueConversation(
|
|
13273
|
+
insist ? `${answer}
|
|
13274
|
+
|
|
13275
|
+
${TASK_QUERY_ANSWER_OR_OPS}` : answer,
|
|
13276
|
+
(p) => this.translateProgress(p),
|
|
13277
|
+
options?.signal
|
|
13278
|
+
);
|
|
13279
|
+
}
|
|
13280
|
+
return carried.length > 0 ? { ...current, researchLog: [...carried, ...current.researchLog] } : current;
|
|
13281
|
+
}
|
|
13282
|
+
/**
|
|
13283
|
+
* Render one read out of live state. Never persisted to the transcript: the
|
|
13284
|
+
* detail is context for the planner's next reply, and re-sending it on every
|
|
13285
|
+
* later turn is exactly the cost this channel exists to avoid.
|
|
13286
|
+
*/
|
|
13287
|
+
taskQueryAnswer(query) {
|
|
13288
|
+
const runners = this.plan?.runners ?? [];
|
|
13289
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
13290
|
+
return renderTaskQueryAnswer(query, this.store.planTasks, {
|
|
13291
|
+
runners,
|
|
13292
|
+
// Allowlist-filtered like the always-on block: a read must not offer a
|
|
13293
|
+
// model the planner is forbidden to assign.
|
|
13294
|
+
models: filterModelsForPrompt(this.modelsCache, allowlist),
|
|
13295
|
+
modes: this.runnerModesFor(runners),
|
|
13296
|
+
autonomousDefault: this.config.autonomousMode
|
|
13297
|
+
});
|
|
13298
|
+
}
|
|
11236
13299
|
/**
|
|
11237
13300
|
* Drive a planner turn to a persisted, broadcast outcome. Task edits apply
|
|
11238
13301
|
* atomically; validation failures are fed back to the model for up to 2
|
|
@@ -11240,7 +13303,7 @@ ${userMessage}` : userMessage;
|
|
|
11240
13303
|
* first turn (startPlanning) and every later turn route through here — one
|
|
11241
13304
|
* path, not two.
|
|
11242
13305
|
*/
|
|
11243
|
-
async settleTurn(turn, options) {
|
|
13306
|
+
async settleTurn(turn, options, reads = freshReadBudget()) {
|
|
11244
13307
|
const invalidOps = (errors, researchLog) => ({
|
|
11245
13308
|
turn: {
|
|
11246
13309
|
kind: "message",
|
|
@@ -11252,11 +13315,15 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11252
13315
|
}
|
|
11253
13316
|
});
|
|
11254
13317
|
const settled = await repairLoop({
|
|
11255
|
-
first: () => turn,
|
|
11256
|
-
resend: (corrective) => this.
|
|
11257
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
13318
|
+
first: () => this.drainTaskQueries(turn, reads, options),
|
|
13319
|
+
resend: async (corrective) => this.drainTaskQueries(
|
|
13320
|
+
await this.aiService.continueConversation(
|
|
13321
|
+
corrective,
|
|
13322
|
+
(p) => this.translateProgress(p),
|
|
13323
|
+
options?.signal
|
|
13324
|
+
),
|
|
13325
|
+
reads,
|
|
13326
|
+
options
|
|
11260
13327
|
),
|
|
11261
13328
|
interpret: (t) => {
|
|
11262
13329
|
if (t.kind !== "task_ops") return { done: { turn: t } };
|
|
@@ -11276,6 +13343,52 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11276
13343
|
}
|
|
11277
13344
|
return this.applyConversationTurn(settled.turn);
|
|
11278
13345
|
}
|
|
13346
|
+
/**
|
|
13347
|
+
* The catalog the planner may actually draw from — model ids and task-mode
|
|
13348
|
+
* ids, per selected runner — emitted on EVERY turn, before any plan exists
|
|
13349
|
+
* or after. The system prompt shows this once at conversation start; a long
|
|
13350
|
+
* grill-me interview or PRD negotiation outlives that single showing and the
|
|
13351
|
+
* planner starts misquoting it, so this re-states it per turn instead.
|
|
13352
|
+
*
|
|
13353
|
+
* Built from the allowlist-filtered view (`filterModelsForPrompt`), the same
|
|
13354
|
+
* one the system prompt used — a restricted allowlist stays a hard bound on
|
|
13355
|
+
* every turn, not just the first. `this.modelsCache` itself is never
|
|
13356
|
+
* filtered: `coerceAssignments` needs the full discovered catalog alongside
|
|
13357
|
+
* the allowlist to resolve labels and clamp effort to real variants.
|
|
13358
|
+
*
|
|
13359
|
+
* Reads `this.plan.runners` live, so a runner admitted mid-session by a
|
|
13360
|
+
* retarget (`admitRunner`) is filtered and shown like every other — there is
|
|
13361
|
+
* no separate "originally selected" list to fall stale.
|
|
13362
|
+
*/
|
|
13363
|
+
catalogBlock() {
|
|
13364
|
+
if (!this.plan) return null;
|
|
13365
|
+
const allowlist = this.settingsFn().modelAllowlist ?? this.currentAllowlist;
|
|
13366
|
+
const filteredModels = filterModelsForPrompt(this.modelsCache, allowlist);
|
|
13367
|
+
const runnerModes = this.runnerModesFor(this.plan.runners);
|
|
13368
|
+
const autonomousDefault = this.config.autonomousMode;
|
|
13369
|
+
const modelLines = this.plan.runners.map((runner) => {
|
|
13370
|
+
const models = filteredModels[runner] ?? [];
|
|
13371
|
+
const capped = models.slice(0, CATALOG_MODEL_CAP);
|
|
13372
|
+
const remainder = models.length - capped.length;
|
|
13373
|
+
const ids = capped.map((m) => m.modelId).join(", ") || "(no models discovered)";
|
|
13374
|
+
return `${runner}: ${ids}${remainder > 0 ? ` \u2026 +${remainder} more not shown` : ""}`;
|
|
13375
|
+
});
|
|
13376
|
+
const modeLines = this.plan.runners.map((runner) => {
|
|
13377
|
+
const modes = runnerModes[runner] ?? [];
|
|
13378
|
+
if (modes.length === 0) return `${runner}: (no modes declared)`;
|
|
13379
|
+
const defaultId = resolveDefaultMode(modes, autonomousDefault);
|
|
13380
|
+
const ids = modes.map((m) => `${m.id}${m.id === defaultId ? " (default)" : ""}`).join(", ");
|
|
13381
|
+
return `${runner}: ${ids}`;
|
|
13382
|
+
});
|
|
13383
|
+
return [
|
|
13384
|
+
"<available_models>",
|
|
13385
|
+
...modelLines,
|
|
13386
|
+
"</available_models>",
|
|
13387
|
+
"<available_task_modes>",
|
|
13388
|
+
...modeLines,
|
|
13389
|
+
"</available_task_modes>"
|
|
13390
|
+
].join("\n");
|
|
13391
|
+
}
|
|
11279
13392
|
/**
|
|
11280
13393
|
* The "you are here" block for post-plan chat: current tasks with stable
|
|
11281
13394
|
* references, plus the task-ops protocol. Injected per turn (never
|
|
@@ -11285,44 +13398,48 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
11285
13398
|
planContextBlock() {
|
|
11286
13399
|
if (!this.plan || this.store.planTasks.length === 0) return null;
|
|
11287
13400
|
const orderOf = new Map(this.store.planTasks.map((t) => [t.id, `#${t.order}`]));
|
|
11288
|
-
const lines = this.store.planTasks.map(
|
|
11289
|
-
|
|
11290
|
-
|
|
13401
|
+
const lines = this.store.planTasks.map((t) => {
|
|
13402
|
+
const isMan = t.type === "user";
|
|
13403
|
+
const runFields = isMan ? `steps:${t.userSteps?.length ?? 0}` : `${t.assignedModel ? `model:${t.assignedModel.modelId} ` : ""}mode:${t.taskMode ?? "build"} effort:${t.thinkingEffort ?? "-"}`;
|
|
13404
|
+
return `#${t.order} id=${t.id} "${t.title}" [${t.status}] type:${isMan ? "MAN" : "AI"} runner:${t.assignedRunner} ${runFields} autonomy:${t.autonomy ?? "-"} slice:${t.sliceType ?? "-"} deps:[${t.dependencies.map((d) => orderOf.get(d) ?? d).join(", ")}]`;
|
|
13405
|
+
});
|
|
11291
13406
|
const locked = this.store.planTasks.filter((t) => t.status === "in_progress");
|
|
11292
13407
|
const execNote = this.hasLiveWork ? `
|
|
11293
13408
|
Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.map((t) => `#${t.order}`).join(", ")}` : ""}. Any task edits you emit will be queued and applied between batches.` : "";
|
|
11294
|
-
const
|
|
11295
|
-
for (const runner of this.plan.runners) {
|
|
11296
|
-
const models = this.modelsCache[runner] ?? [];
|
|
11297
|
-
const ids = models.map((m) => m.modelId).join(", ");
|
|
11298
|
-
modelLines.push(`${runner}: ${ids || "(no models discovered)"}`);
|
|
11299
|
-
}
|
|
11300
|
-
const modelBlock = modelLines.length > 0 ? ["<available_models>", ...modelLines, "</available_models>"].join("\n") : "";
|
|
13409
|
+
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
11301
13410
|
return [
|
|
11302
13411
|
"<current_plan>",
|
|
11303
13412
|
...lines,
|
|
11304
13413
|
"</current_plan>",
|
|
11305
|
-
|
|
11306
|
-
"The block above is the CURRENT task plan. Choose how to respond:",
|
|
13414
|
+
"The block above is the CURRENT task plan \u2014 short fields only. Choose how to respond:",
|
|
11307
13415
|
"- To answer a question or discuss, reply in plain prose (no JSON).",
|
|
13416
|
+
// The reminder text is owned by TaskQuery beside the parser, so the
|
|
13417
|
+
// protocol the planner reads and the one Ordewell accepts stay one thing.
|
|
13418
|
+
TASK_QUERY_REMINDER,
|
|
11308
13419
|
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
11309
13420
|
' {"taskOps": [',
|
|
11310
|
-
|
|
11311
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?}},',
|
|
13421
|
+
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
13422
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
11312
13423
|
' {"op":"remove","taskId":"<id or #order>"},',
|
|
11313
|
-
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]},',
|
|
11314
|
-
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}},',
|
|
11315
|
-
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...]}',
|
|
13424
|
+
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
13425
|
+
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
13426
|
+
' {"op":"split","taskId":"<id or #order>","parts":[{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...}, ...],"handle"?:"<name>"},',
|
|
13427
|
+
` {"op":"rearm","taskId":"<id or #order>","changes"?:{${updateChanges}}}`,
|
|
11316
13428
|
" ]}",
|
|
11317
13429
|
'- For sweeping changes, you may instead emit a full {"tasks":[...]} plan JSON.',
|
|
13430
|
+
'Every "<id or #order>" ref in a batch resolves against the plan shown above, before any op in the batch runs \u2014 an earlier remove/merge/split never shifts what a later "#N" means.',
|
|
13431
|
+
'Give "add", "merge", or "split" a "handle" (any name you choose, unused elsewhere in this batch) to let a LATER op in the same batch reference the task it produces \u2014 for "split", the handle names its last part. A handle used before its defining op is rejected.',
|
|
11318
13432
|
'When creating or changing tasks, set "assignedRunner" and "assignedModel" ({"modelId","modelLabel","thinkingEffort"?}) using only the runners and models listed in <available_models>.',
|
|
11319
|
-
|
|
13433
|
+
'Keep dependencies consistent: no cycles, no references to removed tasks, and never touch running or completed tasks \u2014 "rearm" is the one exception, below.',
|
|
13434
|
+
'Just declare the dependencies you want \u2014 display order is repaired for you afterwards, so a rewire or a newly added prerequisite never needs a "reorder" op. Only a task that is running or completed cannot be shifted, so an edit that would need one to move is rejected.' + execNote,
|
|
13435
|
+
'Flipping "type" between "ai" and "user" is a content change, not just a label: an update to "user" needs "userSteps" in the SAME op, and an update to "ai" needs "prompt" in the SAME op \u2014 the model/mode/effort/autonomy fields (flipping to "user") or the userSteps (flipping to "ai") are cleared automatically.',
|
|
13436
|
+
'"rearm" puts a failed OR completed task back to pending \u2014 verdict and output summary are cleared, any dependents it had blocked are released, and it may carry field changes (e.g. a corrected "prompt") applied in the same op. A running task cannot be re-armed. Never rearm a task just to relabel it \u2014 only when it should actually run again.'
|
|
11320
13437
|
].filter(Boolean).join("\n");
|
|
11321
13438
|
}
|
|
11322
13439
|
/** Validate + commit a task_ops turn atomically. Returns the errors on rejection (plan untouched). */
|
|
11323
13440
|
applyTaskOpsTurn(turn) {
|
|
11324
13441
|
if (!this.plan) throw new Error("No active plan state");
|
|
11325
|
-
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners);
|
|
13442
|
+
const result = applyTaskOps(this.store.planTasks, turn.ops, this.plan.runners, this.editCatalog());
|
|
11326
13443
|
if (!result.ok) return { errors: result.errors };
|
|
11327
13444
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11328
13445
|
const content = `Tasks updated:
|
|
@@ -11589,15 +13706,22 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
11589
13706
|
return plan;
|
|
11590
13707
|
}
|
|
11591
13708
|
/**
|
|
11592
|
-
* Patch one task's fields. A hand-set dependency list
|
|
11593
|
-
*
|
|
11594
|
-
*
|
|
11595
|
-
*
|
|
13709
|
+
* Patch one task's fields. A hand-set dependency list, or a type flip
|
|
13710
|
+
* between AI and MAN, are the patches that can leave a task incoherent
|
|
13711
|
+
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
13712
|
+
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
13713
|
+
* task-ops applier uses (as the 'direct' actor, which skips the lock rule)
|
|
13714
|
+
* rather than a second copy of the rules — and throws, so the surface can
|
|
13715
|
+
* say why. Gated on the task existing so an edit to an unknown id still
|
|
13716
|
+
* falls through to the no-op `store.update` below instead of throwing.
|
|
11596
13717
|
*/
|
|
11597
13718
|
async updateTask(taskId, changes) {
|
|
11598
|
-
if (changes.dependencies) {
|
|
11599
|
-
const check =
|
|
11600
|
-
if (!check.ok) throw new PlanEditError(check.error ?? "Those
|
|
13719
|
+
if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode) && this.store.get(taskId)) {
|
|
13720
|
+
const check = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.editCatalog());
|
|
13721
|
+
if (!check.ok) throw new PlanEditError(check.error ?? "Those changes are not valid");
|
|
13722
|
+
if (check.clear?.length) {
|
|
13723
|
+
changes = { ...changes, ...Object.fromEntries(check.clear.map((f) => [f, void 0])) };
|
|
13724
|
+
}
|
|
11601
13725
|
}
|
|
11602
13726
|
return this.editPlan(
|
|
11603
13727
|
() => Boolean(this.store.update(taskId, changes)),
|
|
@@ -13695,6 +15819,11 @@ var PlannerModelMemory = class {
|
|
|
13695
15819
|
SettingsService,
|
|
13696
15820
|
StdioAgentAdapter,
|
|
13697
15821
|
TASK_OPS_ENVELOPE_KEY,
|
|
15822
|
+
TASK_QUERY_ANSWER_OR_OPS,
|
|
15823
|
+
TASK_QUERY_ENVELOPE_KEY,
|
|
15824
|
+
TASK_QUERY_FIELDS,
|
|
15825
|
+
TASK_QUERY_PROTOCOL,
|
|
15826
|
+
TASK_QUERY_REMINDER,
|
|
13698
15827
|
TRUNCATED_PLAN_REPAIR_INSTRUCTION,
|
|
13699
15828
|
TaskOrchestrator,
|
|
13700
15829
|
TmuxRunner,
|
|
@@ -13765,6 +15894,7 @@ var PlannerModelMemory = class {
|
|
|
13765
15894
|
emptyWarnings,
|
|
13766
15895
|
enabledRunners,
|
|
13767
15896
|
ensureDir,
|
|
15897
|
+
ensureStateDirIgnored,
|
|
13768
15898
|
escapeControlCharsInStrings,
|
|
13769
15899
|
executionLogBlock,
|
|
13770
15900
|
executionSummary,
|
|
@@ -13814,6 +15944,7 @@ var PlannerModelMemory = class {
|
|
|
13814
15944
|
parsePartialPlan,
|
|
13815
15945
|
parsePlanJson,
|
|
13816
15946
|
parseTaskOpsJson,
|
|
15947
|
+
parseTaskQueryJson,
|
|
13817
15948
|
pendingEditRulesBlock,
|
|
13818
15949
|
planDirectLaunch,
|
|
13819
15950
|
planShellLaunch,
|
|
@@ -13822,11 +15953,13 @@ var PlannerModelMemory = class {
|
|
|
13822
15953
|
providerForRunner,
|
|
13823
15954
|
reEmitPlanPrompt,
|
|
13824
15955
|
reEmitTaskOpsPrompt,
|
|
15956
|
+
reEmitTaskQueryPrompt,
|
|
13825
15957
|
readDaemonToken,
|
|
13826
15958
|
referencePattern,
|
|
13827
15959
|
removeTaskFromPlan,
|
|
13828
15960
|
renderPlanMap,
|
|
13829
15961
|
renderPriorOutputs,
|
|
15962
|
+
renderTaskQueryAnswer,
|
|
13830
15963
|
renumberTasks,
|
|
13831
15964
|
repairLoop,
|
|
13832
15965
|
researchShellWarning,
|
|
@@ -13860,7 +15993,9 @@ var PlannerModelMemory = class {
|
|
|
13860
15993
|
summarizeOutput,
|
|
13861
15994
|
summarizeToolCall,
|
|
13862
15995
|
taskOpsRejectedPrompt,
|
|
15996
|
+
taskQuerySignature,
|
|
13863
15997
|
textHasTaskOps,
|
|
15998
|
+
textHasTaskQuery,
|
|
13864
15999
|
tmuxSessionName,
|
|
13865
16000
|
tmuxSocketName,
|
|
13866
16001
|
tmuxWindowName,
|