copilot-operator 0.1.38 → 0.1.40

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.
Files changed (55) hide show
  1. package/dist/src/api/operator.controller.js +128 -2
  2. package/dist/src/api/operator.service.js +473 -39
  3. package/dist/src/api/server.js +7 -0
  4. package/dist/src/log/terminal.js +145 -0
  5. package/dist/src/orchestrator/contract.js +77 -6
  6. package/dist/src/plan/brief.js +1375 -1369
  7. package/dist/src/plan/schema.js +5 -0
  8. package/dist/src/plan/systemGuide.js +218 -20
  9. package/dist/src/session/exports.js +1 -0
  10. package/dist/src/session/startFailure.js +91 -0
  11. package/dist/src/vcs/prepareFromRemote.js +7 -20
  12. package/dist/src/vcs/remoteAuth.js +26 -1
  13. package/dist/src/vcs/runPreflight.js +31 -14
  14. package/dist/src/vcs/snapshot.js +3 -1
  15. package/dist/src/vcs/startChoice.js +295 -0
  16. package/dist/src/vcs/taskVcs.js +61 -5
  17. package/dist/web/404.html +2 -2
  18. package/dist/web/_next/static/chunks/499-28e118e33ce07935.js +1 -0
  19. package/dist/web/_next/static/chunks/{549-7e35e4fdefadb05d.js → 549-f295c8429e83b25b.js} +1 -1
  20. package/dist/web/_next/static/chunks/584-578be94a0786334b.js +1 -0
  21. package/dist/web/_next/static/chunks/867-8dc5bbb092ca2c5c.js +1 -0
  22. package/dist/web/_next/static/chunks/app/history/page-9f69ccde726c9be2.js +1 -0
  23. package/dist/web/_next/static/chunks/app/layout-73f28bc065a46ed3.js +1 -0
  24. package/dist/web/_next/static/chunks/app/page-e557d58476ae6383.js +1 -0
  25. package/dist/web/_next/static/chunks/app/sessions/view/page-0485ee7f781156ec.js +1 -0
  26. package/dist/web/_next/static/chunks/app/system/{page-5ae1f8054208e6eb.js → page-227a259e383ca2a0.js} +1 -1
  27. package/dist/web/_next/static/css/cea7913ba416dbb7.css +1 -0
  28. package/dist/web/appearance.html +2 -2
  29. package/dist/web/appearance.txt +28 -26
  30. package/dist/web/defaults.html +2 -2
  31. package/dist/web/defaults.txt +28 -26
  32. package/dist/web/history.html +2 -2
  33. package/dist/web/history.txt +28 -26
  34. package/dist/web/import.html +2 -2
  35. package/dist/web/import.txt +28 -26
  36. package/dist/web/index.html +2 -2
  37. package/dist/web/index.txt +28 -26
  38. package/dist/web/level1.html +2 -2
  39. package/dist/web/level1.txt +28 -26
  40. package/dist/web/presets.html +2 -2
  41. package/dist/web/presets.txt +28 -26
  42. package/dist/web/sessions/view.html +2 -2
  43. package/dist/web/sessions/view.txt +28 -26
  44. package/dist/web/system.html +2 -2
  45. package/dist/web/system.txt +28 -26
  46. package/package.json +158 -156
  47. package/dist/web/_next/static/chunks/36-94660f1cde2da529.js +0 -1
  48. package/dist/web/_next/static/chunks/867-d4f5bd8c5805bffb.js +0 -1
  49. package/dist/web/_next/static/chunks/app/history/page-a0bd59986f7c8beb.js +0 -1
  50. package/dist/web/_next/static/chunks/app/layout-630b663626cebe4a.js +0 -1
  51. package/dist/web/_next/static/chunks/app/page-4defcfd1eb482644.js +0 -1
  52. package/dist/web/_next/static/chunks/app/sessions/view/page-ce99494b1997ee25.js +0 -1
  53. package/dist/web/_next/static/css/c37978b44194c37b.css +0 -1
  54. /package/dist/web/_next/static/{8C2D7RnTerPO3g_WyLEC3 → Y9tuV5LV-O0XyCREkeGc5}/_buildManifest.js +0 -0
  55. /package/dist/web/_next/static/{8C2D7RnTerPO3g_WyLEC3 → Y9tuV5LV-O0XyCREkeGc5}/_ssgManifest.js +0 -0
@@ -52,6 +52,11 @@ const VcsInput = z
52
52
  .optional(),
53
53
  /** The local branch `startFrom: "branch"` starts from, and `previous-session`'s fallback. */
54
54
  baseBranch: z.string().trim().optional(),
55
+ /**
56
+ * The remote `baseBranch` was chosen from in the start window (`origin`): the session starts from
57
+ * that remote branch as fetched there. Written by the export; the start window asks every time.
58
+ */
59
+ baseRemote: z.string().trim().optional(),
55
60
  /**
56
61
  * The existing local branch the whole session works on, exactly as named: no new branch, no
57
62
  * prefix. Given alone it means `startFrom: "existing-branch"`. See `VersionControl.startFrom`.
@@ -40,8 +40,8 @@ export const SYSTEM_GUIDE = [
40
40
  route: 'every page',
41
41
  nameEn: 'The header and the navigation',
42
42
  nameBg: 'Заглавната лента и навигацията',
43
- purposeEn: 'The links down the side of every page, the language switch, the round to-the-top and to-the-bottom buttons of a long page, the box that asks before anything irreversible, and the banner of steps waiting for a decision, which appears on every page.',
44
- purposeBg: 'Връзките отстрани на всяка страница, смяната на езика, кръглите бутони към началото и края на дълга страница, прозорецът, който пита преди нещо необратимо, и лентата със стъпки, чакащи решение, която се появява на всяка страница.',
43
+ purposeEn: 'The links down the side of every page, the language switch, the round to-the-top and to-the-bottom buttons of a long page, the box that asks before anything irreversible, the start window every start button opens first (where the work starts), the notice of a start that did not happen, and the banner of steps waiting for a decision, which appears on every page.',
44
+ purposeBg: 'Връзките отстрани на всяка страница, смяната на езика, кръглите бутони към началото и края на дълга страница, прозорецът, който пита преди нещо необратимо, прозорецът за старт, който всеки бутон за пускане отваря първо (откъде тръгва работата), съобщението за пускане, което не е започнало, и лентата със стъпки, чакащи решение, която се появява на всяка страница.',
45
45
  sections: [
46
46
  {
47
47
  headingKey: 'nav.label',
@@ -177,6 +177,148 @@ export const SYSTEM_GUIDE = [
177
177
  },
178
178
  ],
179
179
  },
180
+ {
181
+ headingKey: 'startdlg.title',
182
+ en: 'Where the work starts',
183
+ bg: 'Откъде да тръгне работата',
184
+ controls: [
185
+ {
186
+ key: 'startdlg.keep',
187
+ kind: 'radio',
188
+ en: 'Keep the start prepared before this run',
189
+ bg: 'Запази началото, подготвено преди това пускане',
190
+ doesEn: 'Only for a session whose start was prepared before the run — a starting snapshot approved on the run panel — and chosen first then: the run starts from it as approved. Any other choice below drops it.',
191
+ doesBg: 'Само за сесия, чието начало е подготвено преди пускането — стартова снимка, одобрена в панела за пускане — и тогава е избрано първо: пускането тръгва от нея, както е одобрена. Всеки друг избор по-долу я отменя.',
192
+ },
193
+ {
194
+ key: 'startdlg.local',
195
+ kind: 'radio',
196
+ en: 'Local branch',
197
+ bg: 'Локален клон',
198
+ doesEn: 'The start window opens on every start button — the run buttons on /, a session\'s Start, "Run this task", the register\'s Continue and "Run only this one again" — and nothing starts until its own button is pressed. This choice: the work starts from the local branch picked in the list beside it, as it is in the folder; nothing is fetched.',
199
+ doesBg: 'Прозорецът за старт се отваря от всеки бутон за пускане — бутоните за пускане на /, „Старт“ на сесия, „Пусни тази задача“, „Продължи“ и „Пусни само тази отново“ в регистъра — и нищо не тръгва, преди да се натисне неговият бутон. Този избор: работата тръгва от локалния клон, избран в списъка до него, какъвто е в папката; нищо не се изтегля.',
200
+ },
201
+ {
202
+ key: 'startdlg.remote',
203
+ kind: 'radio',
204
+ en: 'Remote branch (fetched now)',
205
+ bg: 'Remote клон (изтегля се сега)',
206
+ doesEn: 'The work starts from the branch as it is on the server: type its name (feature/…) or pick it from the list. It is fetched when the window\'s start button is pressed; if the SSH key has a passphrase, Git\'s own window asks for it (it may be behind the browser) — the operator types it there, the bot never sees it. The local branch of that name is not touched.',
207
+ doesBg: 'Работата тръгва от клона, какъвто е на сървъра: напиши името му (feature/…) или го избери от списъка. Изтегля се при натискане на бутона за старт на прозореца; ако SSH ключът има passphrase, собственият прозорец на Git я иска (може да е зад браузъра) — операторът я въвежда там, ботът не я вижда. Локалният клон със същото име не се пипа.',
208
+ },
209
+ {
210
+ key: 'startdlg.refreshRemote',
211
+ kind: 'button',
212
+ en: 'Get the list of branches from the remote',
213
+ bg: 'Вземи списъка с клонове от remote',
214
+ doesEn: 'Fetches the whole remote (the password window may open) so the list of remote branches to pick from is current.',
215
+ doesBg: 'Изтегля целия remote (може да се отвори прозорецът за парола), за да е актуален списъкът с remote клонове за избор.',
216
+ },
217
+ {
218
+ key: 'startdlg.previous',
219
+ kind: 'radio',
220
+ en: 'Carry on from the session before it',
221
+ bg: 'Продължи от предната сесия',
222
+ doesEn: 'Offered for a session that follows another in the same repository: carries on from the end of that session\'s branch.',
223
+ doesBg: 'Предлага се за сесия след друга в същото хранилище: продължава от края на клона на онази сесия.',
224
+ },
225
+ {
226
+ key: 'startdlg.onBranch',
227
+ kind: 'checkbox',
228
+ en: 'Work directly on this branch, without a new branch',
229
+ bg: 'Работи направо на този клон, без нов клон',
230
+ doesEn: 'Off, as usual: the bot cuts a branch of its own from the chosen one. On: the tasks commit on the chosen branch itself; a remote branch is first made locally, or the local one brought up to it (fast-forward only). A local branch that has gone its own way from the remote one is refused, with nothing changed.',
231
+ doesBg: 'Изключено, както обикновено: ботът прави свой клон от избрания. Включено: задачите комитват направо на избрания клон; при remote клон първо се прави локален или локалният се довежда до него (само fast-forward). Локален клон, тръгнал по свой път спрямо remote, се отказва, без да се променя нищо.',
232
+ },
233
+ {
234
+ key: 'startdlg.goStep',
235
+ kind: 'button',
236
+ en: 'Start, asking before each command',
237
+ bg: 'Започни, с питане преди всяка команда',
238
+ doesEn: 'The window\'s own start button, for a run that asks before each command: fetches what was chosen from the remote, writes the choice on the sessions and starts.',
239
+ doesBg: 'Собственият бутон за старт на прозореца, за пускане с питане преди всяка команда: изтегля избраното от remote, записва избора в сесиите и стартира.',
240
+ },
241
+ {
242
+ key: 'startdlg.goUnattended',
243
+ kind: 'button',
244
+ en: 'Start without asking',
245
+ bg: 'Започни без питане',
246
+ doesEn: 'The same, for a run without asking.',
247
+ doesBg: 'Същото, за пускане без питане.',
248
+ },
249
+ {
250
+ key: 'startdlg.retryFetch',
251
+ kind: 'button',
252
+ en: 'Fetch again',
253
+ bg: 'Изтегли отново',
254
+ doesEn: 'After a fetch that did not get through — a passphrase not typed, a branch name the remote does not have: fetches again.',
255
+ doesBg: 'След изтегляне, което не е минало — непопълнен passphrase, име на клон, което remote няма: изтегля отново.',
256
+ },
257
+ {
258
+ key: 'startdlg.useFetched',
259
+ kind: 'button',
260
+ en: 'Go on with what was last fetched',
261
+ bg: 'Продължи с последно изтегленото',
262
+ doesEn: 'After such a fetch, when that branch was fetched here before: starts from that older copy instead.',
263
+ doesBg: 'След такова изтегляне, ако клонът е бил изтеглян тук преди: тръгва от онова по-старо копие.',
264
+ },
265
+ {
266
+ key: 'startdlg.back',
267
+ kind: 'button',
268
+ en: 'Back to the choice',
269
+ bg: 'Обратно към избора',
270
+ doesEn: 'Back to the choice of branches, after a fetch or a start that did not go through.',
271
+ doesBg: 'Обратно към избора на клонове след изтегляне или старт, които не са минали.',
272
+ },
273
+ {
274
+ key: 'startdlg.tryAgain',
275
+ kind: 'button',
276
+ en: 'Try to start again',
277
+ bg: 'Опитай пак да започнеш',
278
+ doesEn: 'After a start that did not happen — for instance once the version control fixes shown in the window are done: starts again with the same choice, without fetching again.',
279
+ doesBg: 'След старт, който не се е случил — например след като поправките на контрола на версиите, показани в прозореца, са направени: стартира пак със същия избор, без ново изтегляне.',
280
+ },
281
+ ],
282
+ },
283
+ {
284
+ headingKey: null,
285
+ en: 'A start that did not happen',
286
+ bg: 'Пускане, което не е започнало',
287
+ controls: [
288
+ {
289
+ key: 'startfail.title',
290
+ kind: 'note',
291
+ en: 'The work did not start',
292
+ bg: 'Работата не започна',
293
+ doesEn: 'In the start window when its button did not start the work, on / under the run buttons once the window is closed, and on every page when a run was accepted and then could not begin (the browser, the profile, the sign-in, a session refused at its turn): the error itself and where it stopped.',
294
+ doesBg: 'В прозореца за старт, когато бутонът му не е стартирал работата, на / под бутоните за пускане след затварянето му, и на всяка страница, когато пускане е прието, а после не е могло да започне (браузърът, профилът, влизането, сесия, отказана на своя ред): самата грешка и къде е спряло.',
295
+ },
296
+ {
297
+ key: 'startfail.download',
298
+ kind: 'link',
299
+ en: 'Download what happened (error, run record and terminal)',
300
+ bg: 'Изтегли какво се случи (грешка, запис на пускането и терминал)',
301
+ doesEn: 'A text file with the error, where it stopped, the run\'s own record, the sessions\' events and every line the bot\'s console printed from the press until a few seconds after. Ask the operator for it when a run does not start.',
302
+ doesBg: 'Текстов файл с грешката, къде е спряло, собствения запис на пускането, събитията на сесиите и всеки ред, който конзолата на бота е отпечатала от натискането до няколко секунди след това. Поискай го от оператора, когато пускане не тръгва.',
303
+ },
304
+ {
305
+ key: 'startfail.terminal',
306
+ kind: 'link',
307
+ en: 'Download what the terminal showed',
308
+ bg: 'Изтегли какво показа терминалът',
309
+ doesEn: 'The latest lines the bot\'s console (npm start or cop start) printed, with the time of each, as a text file. Also on /system.',
310
+ doesBg: 'Последните редове, отпечатани от конзолата на бота (npm start или cop start), с часа на всеки, като текстов файл. Има го и на /system.',
311
+ },
312
+ {
313
+ key: 'startfail.dismiss',
314
+ kind: 'button',
315
+ en: 'Close this notice',
316
+ bg: 'Затвори съобщението',
317
+ doesEn: 'Closes the notice of a run that could not begin, on every page; the files stay on record.',
318
+ doesBg: 'Затваря съобщението за пускане, което не е могло да започне, на всяка страница; файловете остават.',
319
+ },
320
+ ],
321
+ },
180
322
  {
181
323
  headingKey: 'approval.waitingHere',
182
324
  en: 'A step is waiting for your decision',
@@ -722,8 +864,8 @@ export const SYSTEM_GUIDE = [
722
864
  kind: 'note',
723
865
  en: 'Prepare version control for this run',
724
866
  bg: 'Подготви контрола на версиите за това пускане',
725
- doesEn: 'Shown under the run settings when a repository of the ticked sessions is not ready: one box per repository, with the fixes. The run buttons stay off until every box is ready, and a start is refused before the browser opens.',
726
- doesBg: 'Показва се под настройките на пускането, когато хранилище на отметнатите сесии не е готово: по една кутия на хранилище, с решенията. Бутоните за пускане остават изключени, докато всяка кутия не е готова, а старт се отказва преди да се отвори браузърът.',
867
+ doesEn: 'Shown under the run settings when a repository of the ticked sessions is not ready: one box per repository, with the fixes. The run buttons stay off until every box is ready — unless all that is wrong is where a session starts (a branch that is not here), which the start window settles — and a start is refused before the browser opens.',
868
+ doesBg: 'Показва се под настройките на пускането, когато хранилище на отметнатите сесии не е готово: по една кутия на хранилище, с решенията. Бутоните за пускане остават изключени, докато всяка кутия не е готова — освен ако единственият проблем е откъде тръгва сесия (клон, който го няма тук), което се решава в прозореца за старт — а старт се отказва преди да се отвори браузърът.',
727
869
  },
728
870
  {
729
871
  key: 'runvcs.repo',
@@ -834,24 +976,16 @@ export const SYSTEM_GUIDE = [
834
976
  kind: 'button',
835
977
  en: 'Run {n} session(s)',
836
978
  bg: 'Пусни {n} сесия(и)',
837
- doesEn: 'Starts the ticked sessions one after another and runs every command without asking. It asks "are you sure" first, unless Settings → Execution → "Starting a run without supervision" is "Start it without asking".',
838
- doesBg: 'Пуска отметнатите сесии една след друга и изпълнява всяка команда без питане. Първо пита „сигурен ли си“, освен ако Настройки → Изпълнение → „Пускане без надзор“ е „Пускай, без да питаш“.',
979
+ doesEn: 'Starts the ticked sessions one after another and runs every command without asking. It asks "are you sure" first, unless Settings → Execution → "Starting a run without supervision" is "Start it without asking". The start window opens first ("Where the work starts"), where the branch of each session is chosen.',
980
+ doesBg: 'Пуска отметнатите сесии една след друга и изпълнява всяка команда без питане. Първо пита „сигурен ли си“, освен ако Настройки → Изпълнение → „Пускане без надзор“ е „Пускай, без да питаш“. Първо се отваря прозорецът за старт („Откъде да тръгне работата“), където се избира клонът на всяка сесия.',
839
981
  },
840
982
  {
841
983
  key: 'batch.runStep',
842
984
  kind: 'button',
843
985
  en: 'Step by step',
844
986
  bg: 'Стъпка по стъпка',
845
- doesEn: 'Starts the same sessions but stops before every command and waits for a decision.',
846
- doesBg: 'Пуска същите сесии, но спира преди всяка команда и чака решение.',
847
- },
848
- {
849
- key: 'batch.notStartedTitle',
850
- kind: 'note',
851
- en: 'The run did not start',
852
- bg: 'Пускането не тръгна',
853
- doesEn: 'The box over the two run buttons when a press was refused, with the reason under it. Nothing was opened and nothing ran; read the reason before pressing again.',
854
- doesBg: 'Карето над двата бутона за пускане, когато натискането е отказано, с причината под него. Нищо не е отваряно и нищо не е пускано; прочети причината, преди да натиснеш пак.',
987
+ doesEn: 'Starts the same sessions but stops before every command and waits for a decision. The start window opens first, as for the other button.',
988
+ doesBg: 'Пуска същите сесии, но спира преди всяка команда и чака решение. Първо се отваря прозорецът за старт, както при другия бутон.',
855
989
  },
856
990
  {
857
991
  key: 'batch.notStartedIsolation',
@@ -1587,8 +1721,8 @@ export const SYSTEM_GUIDE = [
1587
1721
  kind: 'button',
1588
1722
  en: 'Run again',
1589
1723
  bg: 'Пусни отново',
1590
- doesEn: 'Puts a finished task back in the queue unchanged, to start afresh; the earlier attempt is kept with its own log. It only queues.',
1591
- doesBg: 'Връща приключила задача в опашката без промяна, да тръгне отначало; предишният опит се пази със собствен дневник. Само я нарежда.',
1724
+ doesEn: 'Puts a finished task back in the queue unchanged, to start afresh; the earlier attempt is kept with its own log. It only queues, then offers "Run this task" on the card.',
1725
+ doesBg: 'Връща приключила задача в опашката без промяна, да тръгне отначало; предишният опит се пази със собствен дневник. Само я нарежда, после предлага „Пусни тази задача“ на картата.',
1592
1726
  },
1593
1727
  {
1594
1728
  key: 'restore.button',
@@ -2300,8 +2434,64 @@ export const SYSTEM_GUIDE = [
2300
2434
  kind: 'button',
2301
2435
  en: 'Fix the prompt and queue it again',
2302
2436
  bg: 'Поправи prompt-а и върни в опашката',
2303
- doesEn: 'On a row that did not end done, its session idle: rewrite the task\'s text and queue it again. It does not start it.',
2304
- doesBg: 'На ред, който не е завършил готов, при спряна сесия: пренапишете текста на задачата и я върнете в опашката. Не я пуска.',
2437
+ doesEn: 'On a row that did not end done, its session idle: rewrite the task\'s text and queue it again. Saving does not start it; the same dialog then asks "Run it now?" with the "Run this task" choices and buttons.',
2438
+ doesBg: 'На ред, който не е завършил готов, при спряна сесия: пренапишете текста на задачата и я върнете в опашката. Запазването не я пуска; после същият прозорец пита „Да я пусна ли сега?“ с изборите и бутоните на „Пусни тази задача“.',
2439
+ },
2440
+ {
2441
+ key: 'runq.later',
2442
+ kind: 'button',
2443
+ en: 'Later',
2444
+ bg: 'По-късно',
2445
+ doesEn: 'On a "Run this task" panel that was offered rather than asked for — after a prompt was saved, a contradiction fixed, or a task queued again on its card: leaves the task waiting in the queue.',
2446
+ doesBg: 'В панела „Пусни тази задача“, когато е предложен, а не поискан — след запазен prompt, поправено противоречие или задача, върната в опашката от картата ѝ: оставя задачата да чака в опашката.',
2447
+ },
2448
+ {
2449
+ key: 'runq.button',
2450
+ kind: 'button',
2451
+ en: 'Run this task',
2452
+ bg: 'Пусни тази задача',
2453
+ doesEn: 'On a waiting (queued) row, its session idle and active: a panel to start that task now — "Only this task", or "This one and the N queued after it" in its session. It lists what runs, says the tasks that already ran are not touched, and names an earlier task of a chain that did not succeed (a warning, not a refusal). Also on a queued task card on the session page.',
2454
+ doesBg: 'На чакащ ред, при спряна и активна сесия: панел за пускане на задачата сега — „Само тази задача“ или „Тази и N чакащи след нея“ в сесията ѝ. Изброява какво ще тръгне, казва, че вече изпълнените задачи не се пипат, и назовава по-ранна задача от верига, която не е минала (предупреждение, не отказ). Има го и на картата на чакаща задача в страницата на сесията.',
2455
+ },
2456
+ {
2457
+ key: 'runq.go',
2458
+ kind: 'button',
2459
+ en: 'Run, asking before each command',
2460
+ bg: 'Пусни, с питане преди всяка команда',
2461
+ doesEn: 'In "Run this task": starts what the panel lists, step by step. A refusal is said in the panel; if the task contradicts itself, its fixes open right there.',
2462
+ doesBg: 'В „Пусни тази задача“: пуска изброеното в панела, стъпка по стъпка. Отказът се казва в панела; ако задачата си противоречи, поправките ѝ се отварят там.',
2463
+ },
2464
+ {
2465
+ key: 'runq.unattended',
2466
+ kind: 'button',
2467
+ en: 'Run without asking',
2468
+ bg: 'Пусни без да пита',
2469
+ doesEn: 'In "Run this task": the same, on its own, without stopping to ask; asks "are you sure" first unless Settings say not to.',
2470
+ doesBg: 'В „Пусни тази задача“: същото, самостоятелно, без да спира да пита; първо пита „сигурни ли сте“, освен ако настройките не казват друго.',
2471
+ },
2472
+ {
2473
+ key: 'cfix.button',
2474
+ kind: 'button',
2475
+ en: 'Fix the contradiction',
2476
+ bg: 'Поправи противоречието',
2477
+ doesEn: 'On a row (a dialog) or task card blocked because it contradicts itself (the chip "task contradicts itself"): lists each contradiction with the ways out, the least change chosen — add the file to the scope, let a read-only task change only that file, drop the scope, or drop one of its own checks. Never changes the prompt. A task a run refused to start for this stays queued: its fixes open inside "Run this task".',
2478
+ doesBg: 'На ред (или карта), блокиран с „задачата си противоречи“: изброява всяко противоречие с изходите от него, като е избрана най-малката промяна — файлът в обхвата, задача само за четене да променя само този файл, без обхват, или без една от собствените ѝ проверки. Prompt-ът никога не се променя.',
2479
+ },
2480
+ {
2481
+ key: 'cfix.apply',
2482
+ kind: 'button',
2483
+ en: 'Apply and queue it again',
2484
+ bg: 'Приложи и върни в опашката',
2485
+ doesEn: 'Applies the chosen fixes, queues the task as a new attempt and offers "Run this task". Nothing starts by itself.',
2486
+ doesBg: 'Прилага избраните поправки, връща задачата в опашката като нов опит и предлага „Пусни тази задача“. Нищо не тръгва само.',
2487
+ },
2488
+ {
2489
+ key: 'cfix.applyQueued',
2490
+ kind: 'button',
2491
+ en: 'Apply',
2492
+ bg: 'Приложи',
2493
+ doesEn: 'The same for a task still waiting (a run refused to start it): edits it where it waits; then start it again from the same panel.',
2494
+ doesBg: 'Същото за задача, която още чака (пускането е отказано): променя я на място; после я пуснете отново от същия панел.',
2305
2495
  },
2306
2496
  {
2307
2497
  key: 'reg.newPrompt',
@@ -3449,6 +3639,14 @@ export const SYSTEM_GUIDE = [
3449
3639
  doesEn: 'The block showing the settings as saved and the paths they resolve to.',
3450
3640
  doesBg: 'Блокът с настройките, както са запазени, и пътищата, до които водят.',
3451
3641
  },
3642
+ {
3643
+ key: 'startfail.terminal',
3644
+ kind: 'link',
3645
+ en: 'Download what the terminal showed',
3646
+ bg: 'Изтегли какво показа терминалът',
3647
+ doesEn: 'Under "This machine": the latest lines the bot\'s console printed, with their times, as a text file — what to ask for when something went wrong and nothing on the pages says why.',
3648
+ doesBg: 'Под „Тази машина“: последните редове, отпечатани от конзолата на бота, с часовете им, като текстов файл — това се иска, когато нещо се е объркало и нищо по страниците не казва защо.',
3649
+ },
3452
3650
  {
3453
3651
  key: 'sys.profileHeld',
3454
3652
  kind: 'note',
@@ -147,6 +147,7 @@ export function buildPlanExport(scope) {
147
147
  // Only when chosen: absent is the old behaviour, and writing it out would suggest a choice.
148
148
  ...(s.vcs?.startFrom ? { startFrom: s.vcs.startFrom } : {}),
149
149
  ...(s.vcs?.baseBranch ? { baseBranch: s.vcs.baseBranch } : {}),
150
+ ...(s.vcs?.baseRemote ? { baseRemote: s.vcs.baseRemote } : {}),
150
151
  ...(s.vcs?.existingBranch ? { existingBranch: s.vcs.existingBranch } : {}),
151
152
  // Only when switched off: absent is on.
152
153
  ...(s.vcs?.updateFromRemote === false ? { updateFromRemote: false } : {}),
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A start that did not get as far as the first task, kept so the operator can see why and download
3
+ * what happened.
4
+ *
5
+ * Asked for on 2026-10-13: "if something fails and the bot does not start at all, I want to see what
6
+ * the error is — the specific error, and a place to download what happened in the terminal". Some of
7
+ * these were answered under the button that was pressed, in a sentence; others were not answered at
8
+ * all: a run accepted, then the browser would not open, and the reason was in the console and in a
9
+ * table row further down the page. Each one is now recorded here with where it stopped, the message,
10
+ * the run and its sessions, and the page offers a file that puts the run's own record, the sessions'
11
+ * events and the console lines of those moments in one place.
12
+ *
13
+ * Kept in memory only, for this process: the files it points at (the run log, the terminal log) are
14
+ * what stays.
15
+ */
16
+ import { randomBytes } from 'node:crypto';
17
+ import { formatTerminal, localTime } from '../log/terminal.js';
18
+ const STAGE_SAID = {
19
+ refused: 'the start was not let in, before anything opened',
20
+ branch: 'choosing the branch the work starts from',
21
+ preflight: 'the version control check, before the browser opened',
22
+ browser: 'opening the browser (Edge), its profile or the chat',
23
+ run: 'the run, before its first task began',
24
+ 'at-turn': "the session's turn in the run, before its first task",
25
+ error: 'the bot itself, while starting',
26
+ };
27
+ export function describeStage(stage) {
28
+ return STAGE_SAID[stage];
29
+ }
30
+ const KEEP = 50;
31
+ export class StartFailures {
32
+ list = [];
33
+ record(f) {
34
+ const full = { ...f, id: `sf-${Date.now().toString(36)}-${randomBytes(3).toString('hex')}`, at: f.at ?? new Date().toISOString() };
35
+ this.list.push(full);
36
+ if (this.list.length > KEEP)
37
+ this.list.splice(0, this.list.length - KEEP);
38
+ return full;
39
+ }
40
+ get(id) {
41
+ return this.list.find((f) => f.id === id);
42
+ }
43
+ /** The newest first; only those nobody was answered with, when asked for. */
44
+ recent(opts = {}) {
45
+ return [...this.list].reverse().filter((f) => !opts.unansweredOnly || !f.answered);
46
+ }
47
+ }
48
+ export function refOf(f) {
49
+ return { id: f.id, at: f.at, stage: f.stage, message: f.message, ...(f.runId ? { runId: f.runId } : {}), answered: f.answered, sessions: f.sessions.map((s) => s.name) };
50
+ }
51
+ /**
52
+ * The file the page offers: what failed and where, then the run's own record, the sessions' events,
53
+ * and what the console printed from the press to a few seconds after the failure. Plain text, so it
54
+ * opens anywhere and can be pasted into a message; the caller redacts it.
55
+ */
56
+ export function failureReport(f, parts) {
57
+ const out = [];
58
+ out.push('copilot-operator — the work did not start');
59
+ out.push('');
60
+ out.push(`When: ${localTime(f.at)} (pressed ${localTime(f.pressedAt)})`);
61
+ out.push(`Where it stopped: ${describeStage(f.stage)} [${f.stage}]`);
62
+ if (f.runId)
63
+ out.push(`Run: ${f.runId}`);
64
+ if (f.sessions.length > 0)
65
+ out.push(`Sessions: ${f.sessions.map((s) => `"${s.name}" (${s.id})`).join(', ')}`);
66
+ if (parts.version)
67
+ out.push(`Version: ${parts.version}`);
68
+ out.push('');
69
+ out.push('What went wrong:');
70
+ out.push(...f.message.split(/\r?\n/).map((l) => ` ${l}`));
71
+ out.push('');
72
+ out.push(`The run's record (${parts.runLog.length} entr${parts.runLog.length === 1 ? 'y' : 'ies'}):`);
73
+ if (parts.runLog.length === 0)
74
+ out.push(' (none: the start stopped before the run was recorded)');
75
+ for (const e of parts.runLog)
76
+ out.push(` ${localTime(e.at)} ${e.type} ${e.message}`);
77
+ out.push('');
78
+ out.push(`The sessions' events since the press (${parts.events.length}):`);
79
+ if (parts.events.length === 0)
80
+ out.push(' (none)');
81
+ for (const e of parts.events)
82
+ out.push(` ${localTime(e.at)} ${e.level === 'info' ? ' ' : e.level.toUpperCase().padEnd(4)} ${e.type} ${e.message ?? ''}`);
83
+ out.push('');
84
+ out.push(`What the terminal showed (the bot's console, ${parts.terminal.length} line(s) from the press to a few seconds after):`);
85
+ out.push(parts.terminal.length > 0 ? formatTerminal(parts.terminal) : ' (nothing was printed)');
86
+ out.push('');
87
+ if (parts.terminalFile)
88
+ out.push(`The whole console is kept in ${parts.terminalFile}; past 5 MB the older part goes to terminal.1.log beside it.`);
89
+ out.push('');
90
+ return out.join('\n');
91
+ }
@@ -29,8 +29,8 @@ import { rm } from 'node:fs/promises';
29
29
  import { tmpdir } from 'node:os';
30
30
  import { join, resolve } from 'node:path';
31
31
  import { createHash } from 'node:crypto';
32
- import { branchNameFrom, freeBranchName, git, gitFetch, lastFetchedAt, porcelainPaths, RUNNER_EMAIL } from './git.js';
33
- import { closePasswordWindows, describeFetchFailure, fetchCommandFor, operatorFetchEnv, QUIET_FETCH_ENV } from './remoteAuth.js';
32
+ import { branchNameFrom, freeBranchName, git, lastFetchedAt, porcelainPaths, RUNNER_EMAIL } from './git.js';
33
+ import { fetchAsOperator, fetchCommandFor } from './remoteAuth.js';
34
34
  const short = (c) => (c ?? '').slice(0, 8);
35
35
  const lines = (s) => s.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
36
36
  function stamp(now = new Date()) {
@@ -92,25 +92,12 @@ export async function planPrepare(folder, opts) {
92
92
  let fetched;
93
93
  if (opts.fetch) {
94
94
  /*
95
- * With the password window: five minutes for the operator to type, and the window not hidden. One
96
- * connection only — `remote set-head --auto` would open a second and ask for the password again —
97
- * so the remote's main is read from what was fetched, below.
95
+ * With the password window when there is one, the whole remote in one connection, so the remote's
96
+ * main is read from what was fetched, below. Once more when the first try fails: see `gitFetch`
97
+ * (2026-10-05, every first press failed at work).
98
98
  */
99
- const asked = !!opts.askpass;
100
- const env = asked ? operatorFetchEnv(opts.askpass) : QUIET_FETCH_ENV;
101
- const args = [...(asked ? [] : ['-c', 'credential.interactive=false']), 'fetch', '--quiet', '--no-tags', '--prune', remote];
102
- const started = new Date(Date.now() - 1000);
103
- // Once more when the first try fails: see `gitFetch` (2026-10-05, every first press failed at work).
104
- const f = await gitFetch(repoDir, args, asked ? 300_000 : 120_000, env, { windowsHide: !asked, killTree: true });
105
- // Given up on with the window still open: closed, so it is not left on screen asking for nothing.
106
- if (!f.ok && asked && /gave up after/.test(f.stderr))
107
- await closePasswordWindows(opts.askpass, started);
108
- if (f.ok)
109
- fetched = { ok: true, asked, ...(f.retried ? { retried: true, ...(f.firstError ? { firstError: f.firstError } : {}) } : {}) };
110
- else {
111
- const why = describeFetchFailure(f.stderr || f.stdout);
112
- fetched = { ok: false, detail: why.detail, auth: why.auth, asked };
113
- }
99
+ const f = await fetchAsOperator(repoDir, remote, { askpass: opts.askpass ?? null });
100
+ fetched = f.ok ? { ok: true, asked: f.asked, ...(f.retried ? { retried: true, ...(f.firstError ? { firstError: f.firstError } : {}) } : {}) } : { ok: false, detail: f.detail, auth: f.auth, asked: f.asked };
114
101
  }
115
102
  let target = '';
116
103
  const remoteHead = await git(repoDir, ['symbolic-ref', '--quiet', '--short', `refs/remotes/${remote}/HEAD`]);
@@ -20,7 +20,7 @@
20
20
  import { execFile } from 'node:child_process';
21
21
  import { existsSync } from 'node:fs';
22
22
  import { resolve } from 'node:path';
23
- import { git } from './git.js';
23
+ import { git, gitFetch } from './git.js';
24
24
  import { psQuote } from './syncCommand.js';
25
25
  /** No prompt anywhere: what every fetch of a run uses. */
26
26
  export const QUIET_FETCH_ENV = { ...process.env, GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never', GIT_ASKPASS: '', SSH_ASKPASS: '' };
@@ -90,3 +90,28 @@ export function closePasswordWindows(program, since) {
90
90
  export function fetchCommandFor(dir, remote) {
91
91
  return `git -C ${psQuote(dir)} fetch --prune ${psQuote(remote)}`;
92
92
  }
93
+ /**
94
+ * A fetch the operator just pressed a button for, and is looking at: with Git's password window when
95
+ * there is one (five minutes to type, the window not hidden), quiet otherwise. One connection only —
96
+ * `remote set-head --auto` would open a second and ask for the password again. Tried once more when
97
+ * the first try fails (see `gitFetch`), and a window left on screen by a fetch that was given up on is
98
+ * closed. `refspecs` fetches only those branches; without them the whole remote is fetched and pruned.
99
+ *
100
+ * The one fetch behind "Prepare the folder from the remote main branch" and the start window's
101
+ * remote branches (`startChoice.ts`).
102
+ */
103
+ export async function fetchAsOperator(repoDir, remote, opts) {
104
+ const asked = !!opts.askpass;
105
+ const env = asked ? operatorFetchEnv(opts.askpass) : QUIET_FETCH_ENV;
106
+ const whole = !opts.refspecs || opts.refspecs.length === 0;
107
+ const args = [...(asked ? [] : ['-c', 'credential.interactive=false']), 'fetch', '--quiet', '--no-tags', ...(whole ? ['--prune'] : []), remote, ...(opts.refspecs ?? [])];
108
+ const started = new Date(Date.now() - 1000);
109
+ const f = await gitFetch(repoDir, args, asked ? 300_000 : 120_000, env, { windowsHide: !asked, killTree: true });
110
+ // Given up on with the window still open: closed, so it is not left on screen asking for nothing.
111
+ if (!f.ok && asked && /gave up after/.test(f.stderr))
112
+ await closePasswordWindows(opts.askpass, started);
113
+ if (f.ok)
114
+ return { ok: true, asked, ...(f.retried ? { retried: true, ...(f.firstError ? { firstError: f.firstError } : {}) } : {}) };
115
+ const why = describeFetchFailure(f.stderr || f.stdout);
116
+ return { ok: false, detail: why.detail, auth: why.auth, asked };
117
+ }