copilot-operator 0.1.41 → 0.1.43
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/src/api/operator.controller.js +159 -6
- package/dist/src/api/operator.service.js +417 -10
- package/dist/src/api/server.js +16 -0
- package/dist/src/cli.js +12 -0
- package/dist/src/config/schema.js +17 -0
- package/dist/src/orchestrator/planMode.js +102 -0
- package/dist/src/orchestrator/review.js +8 -4
- package/dist/src/orchestrator/taskRunner.js +246 -40
- package/dist/src/plan/brief.js +69 -20
- package/dist/src/plan/changes.js +217 -0
- package/dist/src/plan/importPlan.js +2 -0
- package/dist/src/plan/schema.js +11 -1
- package/dist/src/plan/systemGuide.js +790 -14
- package/dist/src/protocol/parser.js +26 -2
- package/dist/src/protocol/replySchema.js +28 -4
- package/dist/src/session/compose.js +76 -7
- package/dist/src/session/exports.js +1 -0
- package/dist/src/session/model.js +38 -0
- package/dist/src/session/store.js +90 -3
- package/dist/src/vcs/startChoice.js +80 -8
- package/dist/src/vcs/taskVcs.js +126 -8
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/WmzBz4QN36pcz4PL_cfEW/_buildManifest.js +1 -0
- package/dist/web/_next/static/chunks/499-751e0be9d4f86e2d.js +1 -0
- package/dist/web/_next/static/chunks/549-2415dd26f4e53d07.js +1 -0
- package/dist/web/_next/static/chunks/699-9d031f8c145a0465.js +1 -0
- package/dist/web/_next/static/chunks/867-aea30f560bc18eaf.js +1 -0
- package/dist/web/_next/static/chunks/897-5a34109533c3da50.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-56134fbb8cc7554f.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-0bb537407fed5944.js +1 -0
- package/dist/web/_next/static/chunks/app/import/{page-999d6cd958989723.js → page-8ef75fc08638e75f.js} +1 -1
- package/dist/web/_next/static/chunks/app/layout-dc602b727480a653.js +1 -0
- package/dist/web/_next/static/chunks/app/page-d73a8ff300862cd8.js +1 -0
- package/dist/web/_next/static/chunks/app/{page-e557d58476ae6383.js → sessions/page-28376d2e4a9b4738.js} +1 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-afc235a9171535d8.js +1 -0
- package/dist/web/_next/static/css/{cea7913ba416dbb7.css → 72c85e4322c21223.css} +1 -1
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +25 -28
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +25 -28
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +25 -28
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +25 -28
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +25 -28
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +25 -28
- package/dist/web/presets.html +3 -3
- package/dist/web/presets.txt +25 -28
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +25 -28
- package/dist/web/sessions.html +16 -0
- package/dist/web/sessions.txt +26 -0
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +25 -28
- package/package.json +161 -158
- package/dist/web/_next/static/-htN5GTSrd3W3f5j_QC2y/_buildManifest.js +0 -1
- package/dist/web/_next/static/chunks/499-28e118e33ce07935.js +0 -1
- package/dist/web/_next/static/chunks/549-f295c8429e83b25b.js +0 -1
- package/dist/web/_next/static/chunks/584-578be94a0786334b.js +0 -1
- package/dist/web/_next/static/chunks/867-8dc5bbb092ca2c5c.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-879158e9e550eea7.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-9f69ccde726c9be2.js +0 -1
- package/dist/web/_next/static/chunks/app/layout-73f28bc065a46ed3.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-0485ee7f781156ec.js +0 -1
- /package/dist/web/_next/static/{-htN5GTSrd3W3f5j_QC2y → WmzBz4QN36pcz4PL_cfEW}/_ssgManifest.js +0 -0
package/dist/src/plan/brief.js
CHANGED
|
@@ -309,6 +309,12 @@ const FIELD_ROWS_EN = [
|
|
|
309
309
|
'no',
|
|
310
310
|
'Overrides the setting above for this one session: give the same name to the sessions that should share a chat, and leave it "" for a session that should have its own.',
|
|
311
311
|
],
|
|
312
|
+
[
|
|
313
|
+
'planMode',
|
|
314
|
+
'session',
|
|
315
|
+
'no',
|
|
316
|
+
'Plan first: "auto" — each task looks at the code read-only and makes a plan, approved by the runner, before it changes anything; "approve" — the same, approved by the operator in a run that asks before each command; "off". Leave it out to follow Settings. Ask when the work is large or the code base is new to the chat.',
|
|
317
|
+
],
|
|
312
318
|
[
|
|
313
319
|
'projectDir',
|
|
314
320
|
'session',
|
|
@@ -380,6 +386,12 @@ const FIELD_ROWS_BG = [
|
|
|
380
386
|
'не',
|
|
381
387
|
'Отменя настройката горе само за тази сесия: дай едно и също име на сесиите, които да споделят чат, и остави "" на онази, която да е сама.',
|
|
382
388
|
],
|
|
389
|
+
[
|
|
390
|
+
'planMode',
|
|
391
|
+
'сесия',
|
|
392
|
+
'не',
|
|
393
|
+
'Първо план: "auto" — всяка задача разглежда кода само с четене и прави план, одобрен от runner-а, преди да промени нещо; "approve" — същото, одобрено от оператора при пускане с питане преди всяка команда; "off". Пропусни го, за да следва Настройките. Питай, когато работата е голяма или кодът е непознат за чата.',
|
|
394
|
+
],
|
|
383
395
|
[
|
|
384
396
|
'projectDir',
|
|
385
397
|
'сесия',
|
|
@@ -784,7 +796,16 @@ Skipped entirely when every task ended **"done"**: say so in one line and go to
|
|
|
784
796
|
|
|
785
797
|
Otherwise: diagnose first, propose second, and say which kind of proposal it is.
|
|
786
798
|
|
|
787
|
-
**
|
|
799
|
+
**The quickest way to what happened is the operator's own report.** On \`/\`, the workbench, they choose
|
|
800
|
+
the task or its session, press **"Ask Kerrigan"**, write what they saw and want, and press **"Copy for
|
|
801
|
+
Kerrigan"**: you get every task with its id, how it ended and why, what it changed, and their words,
|
|
802
|
+
with the form of the answer. Answer it with **changes** to those sessions — a task carried on with your
|
|
803
|
+
prompt (\`continue\`), a done one given a new prompt (\`newPrompt\`), one started over (\`runAgain\`),
|
|
804
|
+
undone (\`undo\`), accepted as done (\`accept\`), a task added, edited or removed — not with a new plan,
|
|
805
|
+
which would make copies in new chats. They paste your whole answer under **"Her answer"**, press **"Check
|
|
806
|
+
her answer"**, read each change in words, and **"Make the {n} change(s)"**; nothing starts by itself.
|
|
807
|
+
|
|
808
|
+
**When you need more than that, ask for the exports**, with the record message from the script. Every row on \`/history\` carries
|
|
788
809
|
three links, each for that one task:
|
|
789
810
|
|
|
790
811
|
- **"plan"** — the sessions and tasks as they are now, in this format, with the edits made in the
|
|
@@ -800,7 +821,7 @@ three links, each for that one task:
|
|
|
800
821
|
**"save the log"** on the same row writes the runner's log of the latest attempt onto the
|
|
801
822
|
operator's Desktop and opens Explorer with the file selected, ready to drag into this chat;
|
|
802
823
|
**"save this attempt's log"** does the same for an earlier one. **"What happened"** unfolds the
|
|
803
|
-
whole story under the row without saving anything. For a whole run rather than one task, it is **"Download everything as JSON"** on
|
|
824
|
+
whole story under the row without saving anything. For a whole run rather than one task, it is **"Download everything as JSON"** on \`/sessions\`:
|
|
804
825
|
one file with every step, exit code, summary, review and commit of those sessions.
|
|
805
826
|
|
|
806
827
|
**Read \`whyItFailed\` first**, then the last rounds. The failure words: \`blocked\` — the chat
|
|
@@ -818,7 +839,11 @@ in those words, every time:
|
|
|
818
839
|
|
|
819
840
|
- **A new prompt.** The task text was ambiguous, a path or a port was wrong, the level 2 forbade
|
|
820
841
|
what the task needed, a check asked the git index about a file the runner had not committed yet,
|
|
821
|
-
or the chat did the wrong thing and the checks caught it.
|
|
842
|
+
or the chat did the wrong thing and the checks caught it. **Carry the task on with it first**: it keeps
|
|
843
|
+
what the failed attempt did, in its own chat and branch, and the chat is told how it ended and why.
|
|
844
|
+
Give the text, as a \`continue\` change, or for the operator to write on \`/\`: choose the task, put
|
|
845
|
+
the text in the chat box on the right, press **"Continue with this"**, then start it from the panel that
|
|
846
|
+
opens. Only when the earlier work should be thrown away, give the whole replacement text, then:
|
|
822
847
|
1. On \`/history\`, in the **"Flow"** view, press **"Fix the prompt and queue it again"** on that row.
|
|
823
848
|
2. Replace the text with the one above and press **"Save and queue again"**.
|
|
824
849
|
3. Under **"What is next"**, press **"Continue: run the {n} queued task(s) in {s} session(s)"**
|
|
@@ -838,7 +863,10 @@ in those words, every time:
|
|
|
838
863
|
When a failure shows up while the run is still going, **"Pause after this task"** holds the rest
|
|
839
864
|
while you work it out.
|
|
840
865
|
Putting the repository back to before the task is **"Restore"**; putting it back and re-running
|
|
841
|
-
that task and every task after it is **"Run again from here"**.
|
|
866
|
+
that task and every task after it is **"Run again from here"**. On \`/\`, **"Undo…"** on the task offers
|
|
867
|
+
**"Undo only this task"** — its work taken out of the session's branch, what came after it kept — and
|
|
868
|
+
**"Go back to before this task"** — it and the tasks after it queued again, nothing started. A task the
|
|
869
|
+
operator finished by hand is **"Accept as done"**.
|
|
842
870
|
- **A change to the bot.** No wording fixes it: the shell the task needs is not on the machine,
|
|
843
871
|
the claim cannot be written as any of the \`expect\` kinds in the checks table above, the runner
|
|
844
872
|
never picked up what the chat attached, the review did not run at all. Say **this is a change
|
|
@@ -924,17 +952,20 @@ answer means, and what you do on it:
|
|
|
924
952
|
for a criterion that is only **claimed**, because it is the cheapest: it changes nothing and turns
|
|
925
953
|
"claimed only" into "proven" or into a real failure you can then fix. Write it as a plan in this
|
|
926
954
|
format.
|
|
927
|
-
2. **
|
|
928
|
-
asked wrongly.
|
|
955
|
+
2. **Give a done task a new prompt** — when the work is right as far as it goes but more is needed,
|
|
956
|
+
or it was asked wrongly. A \`newPrompt\` change, or on \`/\`: the task, its chat box, **"Queue with
|
|
957
|
+
this new prompt"** — it builds on what the task did. To start it over instead: on \`/history\`,
|
|
929
958
|
**"Fix the prompt and queue it again"** on that row, replace the text, **"Save and queue
|
|
930
959
|
again"**, then continue the run.
|
|
931
960
|
3. **Put the repository back and solve it differently** — when the approach is wrong rather than
|
|
932
|
-
the wording, and building on it would be building on the wrong thing.
|
|
961
|
+
the wording, and building on it would be building on the wrong thing. An \`undo\` change, or on
|
|
962
|
+
\`/\` **"Undo…"**: **"Undo only this task"** takes its work out and keeps what came after;
|
|
963
|
+
**"Go back to before this task"** queues it and every one after it again. **"Restore"** on the
|
|
933
964
|
task card puts the code back to before that task; **"Run again from here"** puts it back and
|
|
934
965
|
re-runs that task and every one after it. Say what will be lost.
|
|
935
|
-
4. **
|
|
936
|
-
|
|
937
|
-
**"Create the sessions and tasks"**.
|
|
966
|
+
4. **More tasks** — when what is missing was never asked for by any task, so there is nothing to
|
|
967
|
+
fix and something to add. In the same session, as \`addTask\` changes; as a new session, a plan
|
|
968
|
+
in this format, for **"Create the sessions and tasks"**.
|
|
938
969
|
`.trim();
|
|
939
970
|
const PHASE3_BG = `
|
|
940
971
|
## Фаза 3 — задача не завърши готова
|
|
@@ -944,7 +975,16 @@ const PHASE3_BG = `
|
|
|
944
975
|
|
|
945
976
|
Иначе: първо диагноза, после предложение, и всеки път казвай от кой вид е предложението.
|
|
946
977
|
|
|
947
|
-
|
|
978
|
+
**Най-бързият път до случилото се е отчетът на самия оператор.** В \`/\`, работното място, той избира
|
|
979
|
+
задачата или сесията ѝ, натиска **„Попитай Кериган“**, пише какво е видял и какво иска и натиска
|
|
980
|
+
**„Копирай за Кериган“**: получаваш всяка задача с id-то ѝ, как е завършила и защо, какво е променила, и
|
|
981
|
+
думите му, с формата на отговора. Отговори с **промени** по тези сесии — задача, продължена с твоя prompt
|
|
982
|
+
(\`continue\`), готова с нов prompt (\`newPrompt\`), пусната отначало (\`runAgain\`), отменена (\`undo\`),
|
|
983
|
+
приета като готова (\`accept\`), добавена, редактирана или премахната задача — а не с нов план, който би
|
|
984
|
+
направил копия в нови чатове. Той поставя целия ти отговор под **„Нейният отговор“**, натиска **„Провери
|
|
985
|
+
отговора ѝ“**, чете всяка промяна с думи и **„Направи промените ({n})“**; нищо не тръгва само.
|
|
986
|
+
|
|
987
|
+
**Когато ти трябва повече, поискай файловете**, със съобщението за записа от сценария. Всеки ред в \`/history\` носи три
|
|
948
988
|
връзки, всяка за точно тази задача:
|
|
949
989
|
|
|
950
990
|
- **„план“** — сесиите и задачите, както са сега, в този формат, с редакциите от интерфейса.
|
|
@@ -961,7 +1001,7 @@ const PHASE3_BG = `
|
|
|
961
1001
|
оператора и отваря Explorer с избран файл, готов за влачене в този чат; **„запази log-а на този
|
|
962
1002
|
опит“** прави същото за по-ранен. **„Какво се случи“** разгъва цялата история под реда, без да се
|
|
963
1003
|
записва нищо. За цяло пускане, а не
|
|
964
|
-
за една задача, е **„Изтегли всичко като JSON“** в
|
|
1004
|
+
за една задача, е **„Изтегли всичко като JSON“** в \`/sessions\`: един файл с всяка стъпка, код на изход,
|
|
965
1005
|
обяснение, рецензия и комит на тези сесии.
|
|
966
1006
|
|
|
967
1007
|
**Чети първо \`whyItFailed\`**, после последните кръгове. Думите за провал: \`blocked\` — чатът
|
|
@@ -980,7 +1020,10 @@ const PHASE3_BG = `
|
|
|
980
1020
|
- **Нов prompt.** Текстът на задачата е бил двусмислен, път или порт е грешен, ниво 2 е
|
|
981
1021
|
забранявало това, което задачата иска, проверка е питала git индекса за файл, който runner-ът
|
|
982
1022
|
още не е комитнал, или чатът е направил грешното нещо и проверките са го хванали. Дай целия
|
|
983
|
-
|
|
1023
|
+
текст. **Първо продължи задачата с него**: пази каквото е свършил провалилият се опит, в своя чат и
|
|
1024
|
+
клон, а чатът научава как е завършил и защо. Дай текста като промяна \`continue\` или за оператора в
|
|
1025
|
+
\`/\`: задачата, текстът в полето на чата вдясно, **„Продължи с това“**, после пускане от панела,
|
|
1026
|
+
който се отваря. Само когато по-ранната работа трябва да се изхвърли, дай целия нов текст, после:
|
|
984
1027
|
1. В \`/history\`, в изгледа **„Поток“**, натисни **„Поправи prompt-а и върни в опашката“** на този ред.
|
|
985
1028
|
2. Замени текста с горния и натисни **„Запази и върни в опашката“**.
|
|
986
1029
|
3. Под **„Какво следва“** натисни **„Продължи: пусни {n} чакащи задачи в {s} сесии“** (пише
|
|
@@ -1000,7 +1043,10 @@ const PHASE3_BG = `
|
|
|
1000
1043
|
Когато провал се покаже, докато пускането още върви, **„Пауза след тази задача“** задържа
|
|
1001
1044
|
останалото, докато го разбереш.
|
|
1002
1045
|
Връщането на хранилището отпреди задачата е **„Върни“**; връщането му плюс ново изпълнение на
|
|
1003
|
-
тази задача и всички след нея е **„Пусни отново оттук“**.
|
|
1046
|
+
тази задача и всички след нея е **„Пусни отново оттук“**. В \`/\` **„Отмени…“** на задачата предлага
|
|
1047
|
+
**„Отмени само тази задача“** — работата ѝ се изважда от клона на сесията, каквото е след нея остава — и
|
|
1048
|
+
**„Върни отпреди тази задача“** — тя и задачите след нея отново на опашката, без пускане. Задача,
|
|
1049
|
+
довършена ръчно от оператора, е **„Приеми като готова“**.
|
|
1004
1050
|
- **Промяна по бота.** Никакви думи не го оправят: обвивката, която задачата иска, я няма на
|
|
1005
1051
|
машината, твърдението не се изразява с нито един от видовете \`expect\` от таблицата с
|
|
1006
1052
|
проверките по-горе, runner-ът изобщо не е взел това, което чатът е прикачил, рецензията не се
|
|
@@ -1087,17 +1133,20 @@ const PHASE4_BG = `
|
|
|
1087
1133
|
я първа за критерий, който е само **твърдение**, защото е най-евтината: не променя нищо и превръща
|
|
1088
1134
|
„само твърдение“ в „доказано“ или в истински провал, който после можеш да поправиш. Напиши я като
|
|
1089
1135
|
план в този формат.
|
|
1090
|
-
2.
|
|
1091
|
-
|
|
1136
|
+
2. **Дай на готова задача нов prompt** — когато работата е правилна дотук, но трябва още, или е била
|
|
1137
|
+
поискана грешно. Промяна \`newPrompt\` или в \`/\`: задачата, полето на чата, **„На опашката с този
|
|
1138
|
+
нов prompt“** — надгражда свършеното. За да тръгне отначало: в \`/history\`
|
|
1092
1139
|
**„Поправи prompt-а и върни в опашката“** на този ред, замени текста,
|
|
1093
1140
|
**„Запази и върни в опашката“**, после продължи изпълнението.
|
|
1094
1141
|
3. **Върни хранилището и реши иначе** — когато грешен е подходът, а не формулировката, и да се
|
|
1095
|
-
стъпва върху него значи да се стъпва върху грешното нещо.
|
|
1142
|
+
стъпва върху него значи да се стъпва върху грешното нещо. Промяна \`undo\` или в \`/\` **„Отмени…“**:
|
|
1143
|
+
**„Отмени само тази задача“** изважда работата ѝ и пази каквото е след нея; **„Върни отпреди тази
|
|
1144
|
+
задача“** слага нея и всяка след нея отново на опашката. **„Върни“** на картата на задачата
|
|
1096
1145
|
връща кода отпреди тази задача; **„Пусни отново оттук“** го връща и пуска пак нея и всяка след
|
|
1097
1146
|
нея. Кажи какво ще се загуби.
|
|
1098
|
-
4.
|
|
1099
|
-
|
|
1100
|
-
**„Създай сесиите и задачите“**.
|
|
1147
|
+
4. **Още задачи** — когато липсващото никога не е било поискано от никоя задача, така че няма какво
|
|
1148
|
+
да се поправя, а има какво да се добави. В същата сесия — като промени \`addTask\`; като нова сесия —
|
|
1149
|
+
план в този формат, за **„Създай сесиите и задачите“**.
|
|
1101
1150
|
`.trim();
|
|
1102
1151
|
/**
|
|
1103
1152
|
* The operator's own part, verbatim — or, while there is none, the interview that produces it.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kerrigan's answer about work that already exists (operator's request, 2026-10-14: "no dead end — the user
|
|
3
|
+
* tells Kerrigan what happened and she proposes how to go on; even when the tasks are done she may judge the
|
|
4
|
+
* implementation wrong and ask for more"). A plan only ever made new sessions, so everything she advised about
|
|
5
|
+
* a session that had run had to be done by hand, or became a copy of it in a new chat.
|
|
6
|
+
*
|
|
7
|
+
* Two halves, both plain text so they work with any chat: `kerriganReport` writes what happened — each session
|
|
8
|
+
* and task with its id, how it ended and why, what it changed — together with the operator's note and the exact
|
|
9
|
+
* form of the answer; `checkChanges` reads her answer, a list of changes to those sessions and tasks, against
|
|
10
|
+
* them as they are now, and says in words what each would do before anything is done. The service applies them
|
|
11
|
+
* one by one through the same paths the buttons use, so a change is held to the same rules as a click.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { TaskInput, extractJson } from './schema.js';
|
|
15
|
+
import { canFollowUp, clipText, isContinuable } from '../session/model.js';
|
|
16
|
+
const SessionRef = z.string({ error: 'Each change names its "session": the id from the report (s-…), or its exact name.' }).trim().min(1);
|
|
17
|
+
const TaskRef = z.union([z.string().trim().min(1), z.number().int().positive()], {
|
|
18
|
+
error: 'Each change names its "task": the id from the report (t-…), its number in the session, or its exact title.',
|
|
19
|
+
});
|
|
20
|
+
export const ChangeSchema = z.discriminatedUnion('op', [
|
|
21
|
+
z.object({ op: z.literal('addTask'), session: SessionRef, after: TaskRef.optional(), task: TaskInput }),
|
|
22
|
+
z.object({
|
|
23
|
+
op: z.literal('editTask'),
|
|
24
|
+
session: SessionRef,
|
|
25
|
+
task: TaskRef,
|
|
26
|
+
title: z.string().trim().min(3).max(120).optional(),
|
|
27
|
+
prompt: z.string().trim().min(30, 'A new prompt needs a real instruction, not a few words.').optional(),
|
|
28
|
+
checks: TaskInput.shape.checks.optional(),
|
|
29
|
+
scope: TaskInput.shape.scope.optional(),
|
|
30
|
+
}),
|
|
31
|
+
z.object({ op: z.literal('removeTask'), session: SessionRef, task: TaskRef }),
|
|
32
|
+
z.object({ op: z.literal('continue'), session: SessionRef, task: TaskRef, instruction: z.string().trim().min(10, 'Say what to do now: "instruction" is the new prompt the task carries on with.') }),
|
|
33
|
+
z.object({ op: z.literal('newPrompt'), session: SessionRef, task: TaskRef, prompt: z.string().trim().min(30, 'A new prompt needs a real instruction, not a few words.') }),
|
|
34
|
+
z.object({ op: z.literal('runAgain'), session: SessionRef, task: TaskRef, prompt: z.string().trim().min(30).optional() }),
|
|
35
|
+
z.object({ op: z.literal('undo'), session: SessionRef, task: TaskRef, how: z.enum(['this', 'back'], { error: '"how" is "this" (take only this task\'s work out) or "back" (go back to before it, and queue the tasks after it too).' }) }),
|
|
36
|
+
z.object({ op: z.literal('accept'), session: SessionRef, task: TaskRef, note: z.string().trim().optional() }),
|
|
37
|
+
z.object({ op: z.literal('note'), session: SessionRef, task: TaskRef, text: z.string().trim().min(1) }),
|
|
38
|
+
], {
|
|
39
|
+
error: (i) => (i.code === 'invalid_union' ? '"op" must be one of addTask, editTask, removeTask, continue, newPrompt, runAgain, undo, accept, note.' : undefined),
|
|
40
|
+
});
|
|
41
|
+
export const ChangesSchema = z.object({
|
|
42
|
+
version: z.literal(1, 'Set "version": 1.'),
|
|
43
|
+
notes: z.string().trim().default(''),
|
|
44
|
+
changes: z.array(ChangeSchema).min(1, 'An answer with "changes" needs at least one.'),
|
|
45
|
+
});
|
|
46
|
+
/** Whether the pasted text is an answer with changes rather than a plan of new sessions. */
|
|
47
|
+
export function looksLikeChanges(text) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = JSON.parse(extractJson(text ?? ''));
|
|
50
|
+
return !!raw && typeof raw === 'object' && Array.isArray(raw.changes);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const RUNNING = ['running', 'waiting-approval'];
|
|
57
|
+
function findSession(sessions, ref) {
|
|
58
|
+
return sessions.find((s) => s.id === ref) ?? sessions.find((s) => s.name === ref) ?? sessions.find((s) => s.name.toLowerCase() === ref.toLowerCase());
|
|
59
|
+
}
|
|
60
|
+
function findTask(session, ref) {
|
|
61
|
+
if (typeof ref === 'number' || /^\d+$/.test(ref))
|
|
62
|
+
return session.tasks[Number(ref) - 1];
|
|
63
|
+
return session.tasks.find((t) => t.id === ref) ?? session.tasks.find((t) => t.title === ref) ?? session.tasks.find((t) => t.title.toLowerCase() === ref.toLowerCase());
|
|
64
|
+
}
|
|
65
|
+
/** Reads Kerrigan's answer and finds each change against the sessions as they are now. Nothing is changed. */
|
|
66
|
+
export function checkChanges(text, sessions) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = JSON.parse(extractJson(text ?? ''));
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return { ok: false, warnings: [], issues: [{ path: '', message: `This is not valid JSON: ${e.message}.` }] };
|
|
73
|
+
}
|
|
74
|
+
const parsed = ChangesSchema.safeParse(raw);
|
|
75
|
+
if (!parsed.success) {
|
|
76
|
+
return { ok: false, warnings: [], issues: parsed.error.issues.map((i) => ({ path: i.path.map((p) => (typeof p === 'number' ? `[${p}]` : `.${String(p)}`)).join('').replace(/^\./, ''), message: i.message })) };
|
|
77
|
+
}
|
|
78
|
+
const issues = [];
|
|
79
|
+
const warnings = [];
|
|
80
|
+
const changes = [];
|
|
81
|
+
parsed.data.changes.forEach((change, index) => {
|
|
82
|
+
const path = `changes[${index}]`;
|
|
83
|
+
const session = findSession(sessions, change.session);
|
|
84
|
+
if (!session) {
|
|
85
|
+
issues.push({ path: `${path}.session`, message: `No session "${change.session}". Use the id from the report (s-…) or the session's exact name.` });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const base = { index, change, sessionId: session.id, sessionName: session.name };
|
|
89
|
+
if (change.op === 'addTask') {
|
|
90
|
+
const after = change.after !== undefined ? findTask(session, change.after) : undefined;
|
|
91
|
+
if (change.after !== undefined && !after) {
|
|
92
|
+
issues.push({ path: `${path}.after`, message: `Session "${session.name}" has no task "${change.after}" to put the new one after.` });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
changes.push({ ...base, says: `add task "${change.task.title}" to "${session.name}"${after ? `, after "${after.title}"` : ', at the end'}, queued` });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const task = findTask(session, change.task);
|
|
99
|
+
if (!task) {
|
|
100
|
+
issues.push({ path: `${path}.task`, message: `Session "${session.name}" has no task "${change.task}". Use the id from the report (t-…), its number, or its exact title.` });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const named = { ...base, taskId: task.id, taskTitle: task.title };
|
|
104
|
+
const refuse = (message) => {
|
|
105
|
+
issues.push({ path: `${path}.op`, message: `"${task.title}" in "${session.name}" is ${task.status}: ${message}` });
|
|
106
|
+
};
|
|
107
|
+
if (RUNNING.includes(task.status))
|
|
108
|
+
return refuse('it is running now; nothing can be changed on it until it stops.');
|
|
109
|
+
switch (change.op) {
|
|
110
|
+
case 'editTask':
|
|
111
|
+
if (task.status !== 'queued')
|
|
112
|
+
return refuse('only a queued task is edited; for one that ran use "runAgain" with a "prompt", "continue" or "newPrompt".');
|
|
113
|
+
changes.push({ ...named, says: `edit "${task.title}": ${['title', 'prompt', 'checks', 'scope'].filter((k) => change[k] !== undefined).join(', ') || 'nothing'}` });
|
|
114
|
+
return;
|
|
115
|
+
case 'removeTask':
|
|
116
|
+
if (task.status !== 'queued')
|
|
117
|
+
return refuse('only a queued task is removed; a task that ran keeps its record.');
|
|
118
|
+
changes.push({ ...named, says: `remove the queued task "${task.title}"` });
|
|
119
|
+
return;
|
|
120
|
+
case 'continue':
|
|
121
|
+
if (!isContinuable(task) && !canFollowUp(task))
|
|
122
|
+
return refuse('only a task that failed, blocked or stopped is carried on; for a done one use "newPrompt".');
|
|
123
|
+
changes.push({ ...named, says: `carry "${task.title}" on in its own chat and branch, from where it got to, with the new prompt` });
|
|
124
|
+
return;
|
|
125
|
+
case 'newPrompt':
|
|
126
|
+
if (task.status !== 'done')
|
|
127
|
+
return refuse('"newPrompt" builds on a done task\'s work; for a failed one use "continue".');
|
|
128
|
+
changes.push({ ...named, says: `give the done task "${task.title}" a new prompt that builds on its work, queued` });
|
|
129
|
+
return;
|
|
130
|
+
case 'runAgain':
|
|
131
|
+
if (task.status === 'queued')
|
|
132
|
+
return refuse('it is queued already; use "editTask" to change it.');
|
|
133
|
+
changes.push({ ...named, says: `run "${task.title}" again from where it first started${change.prompt ? ', with a new prompt' : ''}, queued` });
|
|
134
|
+
return;
|
|
135
|
+
case 'undo':
|
|
136
|
+
if (task.status === 'queued' || !task.startedAt)
|
|
137
|
+
return refuse('it has not run since it was queued; there is nothing of it to undo.');
|
|
138
|
+
changes.push({ ...named, says: change.how === 'this' ? `undo only "${task.title}": its work taken out, the tasks after it kept; it is queued again` : `go back to before "${task.title}": it and the tasks after it queued again` });
|
|
139
|
+
return;
|
|
140
|
+
case 'accept':
|
|
141
|
+
if (!['failed', 'blocked', 'limit-reached', 'aborted'].includes(task.status))
|
|
142
|
+
return refuse('only a task that ended without being done is accepted as done.');
|
|
143
|
+
changes.push({ ...named, says: `accept "${task.title}" as done${change.note ? ', with a note' : ''}` });
|
|
144
|
+
return;
|
|
145
|
+
case 'note':
|
|
146
|
+
if (task.status !== 'queued')
|
|
147
|
+
warnings.push(`The note for "${task.title}" (${task.status}) goes out only when it runs again.`);
|
|
148
|
+
changes.push({ ...named, says: `a note to the chat for "${task.title}"` });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (issues.length > 0)
|
|
153
|
+
return { ok: false, issues, warnings };
|
|
154
|
+
return { ok: true, notes: parsed.data.notes, changes, warnings };
|
|
155
|
+
}
|
|
156
|
+
// --- the report ------------------------------------------------------------------------------
|
|
157
|
+
const clip = (s, n) => (clipText(s, n) ?? '').replace(/\s+/g, ' ').trim();
|
|
158
|
+
function taskLines(task, index) {
|
|
159
|
+
const out = [`${index + 1}. "${task.title}" — id ${task.id} — ${task.status}${(task.attempt ?? 1) > 1 ? `, attempt ${task.attempt}` : ''}`];
|
|
160
|
+
if (task.accepted)
|
|
161
|
+
out.push(` accepted as done by the operator (it had ended ${task.accepted.was})${task.accepted.note ? `: ${clip(task.accepted.note, 300)}` : ''}`);
|
|
162
|
+
const undone = task.attempts?.at(-1)?.undone;
|
|
163
|
+
if (task.status === 'queued' && undone)
|
|
164
|
+
out.push(` its last attempt was undone by the operator (${undone.how === 'this' ? 'only its work taken out' : 'back to before it'})`);
|
|
165
|
+
if (task.continuing?.instruction)
|
|
166
|
+
out.push(` carried on with the operator's prompt: ${clip(task.continuing.instruction, 400)}`);
|
|
167
|
+
if (task.status === 'queued')
|
|
168
|
+
out.push(` prompt: ${clip(task.prompt, 500)}`);
|
|
169
|
+
if (task.summary && task.status !== 'queued')
|
|
170
|
+
out.push(` summary: ${clip(task.summary, 600)}`);
|
|
171
|
+
if (task.reason && task.reason !== task.summary && task.status !== 'done')
|
|
172
|
+
out.push(` why it stopped: ${clip(task.reason, 600)}`);
|
|
173
|
+
const failed = (task.checkResults ?? []).filter((c) => !c.passed);
|
|
174
|
+
if (failed.length > 0)
|
|
175
|
+
out.push(` checks that did not pass: ${failed.slice(0, 6).map((c) => `${c.name} (${clip(c.detail, 160)})`).join('; ')}`);
|
|
176
|
+
if (task.review?.verdict) {
|
|
177
|
+
const findings = (task.review.findings ?? []).slice(0, 5).map((f) => `${clip(f.what, 200)}${f.where ? ` [${clip(f.where, 80)}]` : ''}`);
|
|
178
|
+
out.push(` review: ${task.review.verdict}${findings.length > 0 ? ` — ${findings.join('; ')}` : ''}`);
|
|
179
|
+
}
|
|
180
|
+
const files = (task.vcs?.files ?? []).map((f) => f.path);
|
|
181
|
+
if (files.length > 0)
|
|
182
|
+
out.push(` changed: ${files.slice(0, 25).join(', ')}${files.length > 25 ? ` and ${files.length - 25} more` : ''}`);
|
|
183
|
+
if (task.vcs?.branch)
|
|
184
|
+
out.push(` branch ${task.vcs.branch}${task.vcs.commit ? ` at ${task.vcs.commit.slice(0, 8)}` : ''}`);
|
|
185
|
+
const notes = task.notes ?? [];
|
|
186
|
+
if (notes.length > 0)
|
|
187
|
+
out.push(` operator's notes: ${notes.slice(-3).map((n) => clip(n.text, 200)).join(' | ')}`);
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* What happened, for Kerrigan: the sessions, each task with its id, how it ended and why, and what it changed;
|
|
192
|
+
* the operator's note; and the exact form of the answer. Plain text, for any chat.
|
|
193
|
+
*/
|
|
194
|
+
export function kerriganReport(sessions, opts = {}) {
|
|
195
|
+
const only = new Set(opts.taskIds ?? []);
|
|
196
|
+
const lines = [
|
|
197
|
+
'# What happened — for Kerrigan',
|
|
198
|
+
'',
|
|
199
|
+
`The operator sends you the state of their work as copilot-operator recorded it (${opts.at ?? new Date().toISOString()}), and asks you to look at it`,
|
|
200
|
+
'and propose how to go on — whether the tasks failed, stopped, or are done but not what was meant.',
|
|
201
|
+
'',
|
|
202
|
+
];
|
|
203
|
+
if (opts.note?.trim())
|
|
204
|
+
lines.push('## The operator says', '', opts.note.trim(), '');
|
|
205
|
+
for (const s of sessions) {
|
|
206
|
+
const vcs = s.vcs?.enabled ? `${s.vcs.branchMode === 'per-task' ? 'a branch per task' : 'one branch for the session, a commit per task'} in ${s.vcs.repoDir || '(no folder)'}` : 'no version control';
|
|
207
|
+
lines.push(`## Session "${s.name}" — id ${s.id}`, '', `${vcs}; ${s.onFailure === 'continue' ? 'independent tasks' : 'one chain: a failed task stops the ones after it'}.`, '');
|
|
208
|
+
s.tasks.forEach((t, i) => {
|
|
209
|
+
if (only.size > 0 && !only.has(t.id))
|
|
210
|
+
return;
|
|
211
|
+
lines.push(...taskLines(t, i));
|
|
212
|
+
});
|
|
213
|
+
lines.push('');
|
|
214
|
+
}
|
|
215
|
+
lines.push('## How to answer', '', 'First tell the operator, in their language and in a few sentences, what you think happened and what you propose.', 'Then give the changes as one JSON object in a ```json block, which the operator pastes on the import page;', 'it shows each change in words before anything is done. Name sessions and tasks by the ids above.', '', '```json', '{"version": 1, "notes": "why, in one or two sentences", "changes": [', ' {"op": "continue", "session": "s-…", "task": "t-…", "instruction": "what to do now — the task goes on from where it got to, in its own chat and branch"},', ' {"op": "addTask", "session": "s-…", "after": "t-…", "task": {"title": "…", "prompt": "…", "expected": "…", "checks": [], "scope": []}}', ']}', '```', '', 'The changes, one object each:', '- "continue" {task, instruction} — a task that failed, blocked or stopped goes on from where it got to, with your prompt added to its own.', '- "newPrompt" {task, prompt} — a done task gets a new prompt that builds on its work (more to do, or done wrong).', '- "runAgain" {task, prompt?} — a task that ran starts over from where it first started, optionally with a corrected prompt; its earlier work is not used.', '- "undo" {task, how} — "this": only this task\'s work is taken out of the session\'s branch, the tasks after it kept; "back": the branch goes back to before it, and it and the tasks after it are queued again. Nothing is deleted.', '- "accept" {task, note?} — a task that did not end done is accepted as done (for example, the operator finished it by hand).', '- "addTask" {task, after?} — a new task in an existing session, after the task named (else at the end); "task" has the fields of a plan\'s task.', '- "editTask" {task, title?, prompt?, checks?, scope?} — a queued task, changed before it runs.', '- "removeTask" {task} — a queued task that is no longer wanted.', '- "note" {task, text} — information for the chat about a task, sent with its next message.', '', 'Nothing runs by itself after the import: the operator starts the run. For work that belongs in a new session, answer with a plan as before.');
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
@@ -231,6 +231,8 @@ defaultProjectDir = '') {
|
|
|
231
231
|
// the standing default, else the session's own.
|
|
232
232
|
s.review = { enabled: planned.review?.enabled !== false, model: (planned.review?.model || defaultReviewModel).trim() };
|
|
233
233
|
s.conversationGroup = group || undefined;
|
|
234
|
+
if (planned.planMode)
|
|
235
|
+
s.planMode = planned.planMode;
|
|
234
236
|
s.model = (planned.model || defaultModel).trim() || undefined;
|
|
235
237
|
// The plan's models are kept, and marked as the plan's: Settings outrank them (see `effectiveModels`).
|
|
236
238
|
if (planned.model?.trim())
|
package/dist/src/plan/schema.js
CHANGED
|
@@ -250,7 +250,7 @@ const TaskVcsInput = z.object({
|
|
|
250
250
|
* ignored with a warning. See `importPlan`.
|
|
251
251
|
*/
|
|
252
252
|
const LegacyMirrorInput = z.object({ enabled: z.boolean().optional(), rootDir: z.string().optional() }).passthrough();
|
|
253
|
-
const TaskInput = z.object({
|
|
253
|
+
export const TaskInput = z.object({
|
|
254
254
|
title: z
|
|
255
255
|
.string()
|
|
256
256
|
.trim()
|
|
@@ -348,6 +348,16 @@ const SessionInput = z.object({
|
|
|
348
348
|
* the same name. Empty means it gets a conversation of its own.
|
|
349
349
|
*/
|
|
350
350
|
conversationGroup: z.string().trim().default(''),
|
|
351
|
+
/**
|
|
352
|
+
* Plan mode for the session's tasks (see `PlanMode` in session/model.ts): "auto" plans first and goes
|
|
353
|
+
* on, "approve" plans first and waits for the operator in a run that asks before each command, "off"
|
|
354
|
+
* does not plan apart. Absent follows Settings.
|
|
355
|
+
*/
|
|
356
|
+
planMode: z
|
|
357
|
+
.enum(['off', 'auto', 'approve'], {
|
|
358
|
+
error: 'planMode must be "off", "auto" (plan first, approved by the runner) or "approve" (plan first, approved by the operator in a run that asks before each command).',
|
|
359
|
+
})
|
|
360
|
+
.optional(),
|
|
351
361
|
vcs: VcsInput,
|
|
352
362
|
review: ReviewInput.optional(),
|
|
353
363
|
/**
|