copilot-operator 0.1.39 → 0.1.41
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 +101 -2
- package/dist/src/api/operator.service.js +292 -25
- package/dist/src/api/server.js +7 -0
- package/dist/src/log/terminal.js +145 -0
- package/dist/src/plan/brief.js +1375 -1369
- package/dist/src/plan/schema.js +5 -0
- package/dist/src/plan/systemGuide.js +158 -16
- package/dist/src/session/exports.js +1 -0
- package/dist/src/session/startFailure.js +91 -0
- package/dist/src/transport/copilotTransport.js +28 -10
- package/dist/src/transport/locators.js +7 -1
- package/dist/src/vcs/prepareFromRemote.js +7 -20
- package/dist/src/vcs/remoteAuth.js +26 -1
- package/dist/src/vcs/runPreflight.js +31 -14
- package/dist/src/vcs/snapshot.js +3 -1
- package/dist/src/vcs/startChoice.js +295 -0
- package/dist/src/vcs/taskVcs.js +40 -5
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/499-28e118e33ce07935.js +1 -0
- package/dist/web/_next/static/chunks/549-f295c8429e83b25b.js +1 -0
- package/dist/web/_next/static/chunks/584-578be94a0786334b.js +1 -0
- package/dist/web/_next/static/chunks/867-8dc5bbb092ca2c5c.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-9f69ccde726c9be2.js +1 -0
- package/dist/web/_next/static/chunks/app/layout-73f28bc065a46ed3.js +1 -0
- package/dist/web/_next/static/chunks/app/page-e557d58476ae6383.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-0485ee7f781156ec.js +1 -0
- package/dist/web/_next/static/chunks/app/system/{page-5ae1f8054208e6eb.js → page-227a259e383ca2a0.js} +1 -1
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +27 -25
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +27 -25
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +27 -25
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +27 -25
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +27 -25
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +27 -25
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +27 -25
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +27 -25
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +27 -25
- package/package.json +158 -157
- package/dist/web/_next/static/chunks/549-bfde82e4a70ed1a0.js +0 -1
- package/dist/web/_next/static/chunks/584-eba7ffe26cf1be83.js +0 -1
- package/dist/web/_next/static/chunks/867-7fccf68b0840c0de.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-d3461384566aa332.js +0 -1
- package/dist/web/_next/static/chunks/app/layout-630b663626cebe4a.js +0 -1
- package/dist/web/_next/static/chunks/app/page-4defcfd1eb482644.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-945be57e397ae162.js +0 -1
- /package/dist/web/_next/static/{KYi1jz7Awse_66Fg8c7Ur → -htN5GTSrd3W3f5j_QC2y}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{KYi1jz7Awse_66Fg8c7Ur → -htN5GTSrd3W3f5j_QC2y}/_ssgManifest.js +0 -0
package/dist/src/plan/schema.js
CHANGED
|
@@ -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',
|
|
@@ -3505,6 +3639,14 @@ export const SYSTEM_GUIDE = [
|
|
|
3505
3639
|
doesEn: 'The block showing the settings as saved and the paths they resolve to.',
|
|
3506
3640
|
doesBg: 'Блокът с настройките, както са запазени, и пътищата, до които водят.',
|
|
3507
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
|
+
},
|
|
3508
3650
|
{
|
|
3509
3651
|
key: 'sys.profileHeld',
|
|
3510
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
|
+
}
|
|
@@ -39,6 +39,29 @@ export function isReplyTimeout(e) {
|
|
|
39
39
|
return e instanceof ReplyTimeoutError || (e instanceof Error && e.name === 'ReplyTimeoutError' && typeof e.seconds === 'number');
|
|
40
40
|
}
|
|
41
41
|
const ORIGIN = 'https://m365.cloud.microsoft';
|
|
42
|
+
/**
|
|
43
|
+
* Which Copilot an address is, by its host — not by whether the text appears anywhere in it: the
|
|
44
|
+
* consumer Copilot and a sign-in page carry the work chat's address in their query (`ru=`,
|
|
45
|
+
* `redirect_uri=`), and a check of the whole URL took them for each other. `work` here only says the
|
|
46
|
+
* host is the work Copilot's; `surface()` also looks for its composer.
|
|
47
|
+
*/
|
|
48
|
+
export function surfaceOfUrl(url) {
|
|
49
|
+
let host = '';
|
|
50
|
+
try {
|
|
51
|
+
host = new URL(url).hostname.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return 'unknown';
|
|
55
|
+
}
|
|
56
|
+
const on = (h) => host === h || host.endsWith(`.${h}`);
|
|
57
|
+
if (on(Signal.loginHost))
|
|
58
|
+
return 'sign-in';
|
|
59
|
+
if (Surface.consumerHosts.some(on))
|
|
60
|
+
return 'consumer';
|
|
61
|
+
if (Surface.workHosts.some(on))
|
|
62
|
+
return 'work';
|
|
63
|
+
return 'unknown';
|
|
64
|
+
}
|
|
42
65
|
/**
|
|
43
66
|
* Which of the operator's locators a chosen model is. Groups first, by the word the locator is written
|
|
44
67
|
* with ("Sonnet", "Opus", "GPT-5.6 Sol Think deeper") contained in the name, longest first; then the
|
|
@@ -256,16 +279,11 @@ export class CopilotTransport {
|
|
|
256
279
|
* when it is really a sign-in problem.
|
|
257
280
|
*/
|
|
258
281
|
async surface() {
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
return
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (url.includes(Surface.workHost)) {
|
|
265
|
-
const hasWorkComposer = (await this.p.locator(Surface.workMarkers[0]).count()) > 0;
|
|
266
|
-
return hasWorkComposer ? 'work' : 'unknown';
|
|
267
|
-
}
|
|
268
|
-
return 'unknown';
|
|
282
|
+
const where = surfaceOfUrl(this.p.url());
|
|
283
|
+
if (where !== 'work')
|
|
284
|
+
return where;
|
|
285
|
+
const hasWorkComposer = (await this.p.locator(Surface.workMarkers[0]).count()) > 0;
|
|
286
|
+
return hasWorkComposer ? 'work' : 'unknown';
|
|
269
287
|
}
|
|
270
288
|
/**
|
|
271
289
|
* Email addresses visible in the page, as a way to tell which account is signed in.
|
|
@@ -283,7 +283,13 @@ export const Blocker = {
|
|
|
283
283
|
* bot when it is really a sign-in problem.
|
|
284
284
|
*/
|
|
285
285
|
export const Surface = {
|
|
286
|
-
|
|
286
|
+
/**
|
|
287
|
+
* The work Copilot's hosts. `copilot.cloud.microsoft` since Microsoft began moving the chat there: on
|
|
288
|
+
* 2026-10-13 the work machine's `m365.cloud.microsoft/chat` landed on
|
|
289
|
+
* `copilot.cloud.microsoft/chat?es=SSR&redirfrom=mcmToCcm`, the same chat with the same composer, and
|
|
290
|
+
* the sign-in was refused as "not the Microsoft 365 Copilot chat" because only the old host was known.
|
|
291
|
+
*/
|
|
292
|
+
workHosts: ['m365.cloud.microsoft', 'copilot.cloud.microsoft'],
|
|
287
293
|
/** Only the work surface has this composer id and these test ids. */
|
|
288
294
|
workMarkers: ['#m365-chat-editor-target-element'],
|
|
289
295
|
consumerHosts: ['copilot.microsoft.com', 'www.bing.com', 'bing.com'],
|
|
@@ -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,
|
|
33
|
-
import {
|
|
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
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
|
100
|
-
|
|
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
|
+
}
|
|
@@ -27,7 +27,7 @@ import { join, resolve } from 'node:path';
|
|
|
27
27
|
import { branchNameFrom, describeUpdate, freeBranchName, git, updateFromRemote } from './git.js';
|
|
28
28
|
import { commitOnto, dirtyBesidesInputs, inputSettings, inputSumsAt, missingInputPatterns } from './inputs.js';
|
|
29
29
|
import { approvalMismatch, dirtyPolicy, takeSnapshot } from './snapshot.js';
|
|
30
|
-
import { repoDirOf, sessionBranchName, vcsPreflight } from './taskVcs.js';
|
|
30
|
+
import { baseRefOf, repoDirOf, sessionBranchName, vcsPreflight } from './taskVcs.js';
|
|
31
31
|
import { artifactPatterns } from './artifacts.js';
|
|
32
32
|
import { inScope } from './scope.js';
|
|
33
33
|
import { findSuspicious } from './commitHygiene.js';
|
|
@@ -58,8 +58,15 @@ async function groupFor(list, allSessions) {
|
|
|
58
58
|
const branch = branchR.ok && branchR.stdout !== 'HEAD' ? branchR.stdout : null;
|
|
59
59
|
const head = headR.ok && headR.stdout ? headR.stdout : null;
|
|
60
60
|
const startFrom = first.vcs?.startFrom ?? 'head';
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
/*
|
|
62
|
+
* The branch the sessions start from, by its name on screen (`origin/x` for a remote branch chosen in
|
|
63
|
+
* the start window) and the ref git reads for it: a remote-tracking branch is never the local one of
|
|
64
|
+
* the same name, and is not brought up to anything here — the start window fetched it.
|
|
65
|
+
*/
|
|
66
|
+
const base = startFrom === 'branch' || startFrom === 'previous-session' ? baseRefOf(first.vcs) : undefined;
|
|
67
|
+
const baseBranch = base?.name;
|
|
68
|
+
const baseHead = base ? (await git(repoDir, ['rev-parse', '--verify', '--quiet', `${base.ref}^{commit}`])).stdout || null : undefined;
|
|
69
|
+
const updates = first.vcs?.updateFromRemote !== false && !base?.remote;
|
|
63
70
|
const settings = inputSettings(first.vcs);
|
|
64
71
|
const workName = first.vcs?.startFrom === 'existing-branch'
|
|
65
72
|
? first.vcs.existingBranch?.trim()
|
|
@@ -69,7 +76,7 @@ async function groupFor(list, allSessions) {
|
|
|
69
76
|
const workBranch = workName ? { name: workName, head: (await git(repoDir, ['rev-parse', '--verify', '--quiet', `refs/heads/${workName}^{commit}`])).stdout || null } : undefined;
|
|
70
77
|
const sessions = list.map((s, i) => {
|
|
71
78
|
const sf = s.vcs?.startFrom ?? 'head';
|
|
72
|
-
const role = i === 0 ? 'first' : sf === 'previous-session' ? 'inherits' : sf === 'branch' && (s.vcs
|
|
79
|
+
const role = i === 0 ? 'first' : sf === 'previous-session' ? 'inherits' : sf === 'branch' && baseRefOf(s.vcs).name === baseBranch && !s.vcsBaseCommit ? 'shares' : 'own';
|
|
73
80
|
// Started means a task of it ran; a start recorded by the panel (a snapshot) is not that (live run 2026-10-03).
|
|
74
81
|
const started = s.tasks.some((t) => !!t.startedAt || (t.attempts ?? []).some((a) => !!a.startedAt));
|
|
75
82
|
return { id: s.id, name: s.name, startFrom: sf, started, hasStart: !!s.vcsBaseCommit, role };
|
|
@@ -111,16 +118,21 @@ async function groupFor(list, allSessions) {
|
|
|
111
118
|
* The branches the sessions name, before the browser opens rather than at their first task: a
|
|
112
119
|
* branch to carry on that is not there, a base branch that is not there.
|
|
113
120
|
*/
|
|
121
|
+
const otherThanStart = (!!problem && !pre.startProblem) || blocking;
|
|
122
|
+
let branchMissing = false;
|
|
114
123
|
for (const s of list) {
|
|
115
124
|
if (s.vcsBaseCommit)
|
|
116
125
|
continue;
|
|
117
|
-
const named = s.vcs?.startFrom === 'existing-branch' ? s.vcs.existingBranch?.trim() : s.vcs?.startFrom === 'branch' ? s.vcs
|
|
118
|
-
if (!named)
|
|
126
|
+
const named = s.vcs?.startFrom === 'existing-branch' ? { ref: `refs/heads/${s.vcs.existingBranch?.trim() ?? ''}`, name: s.vcs.existingBranch?.trim() ?? '', remote: undefined } : s.vcs?.startFrom === 'branch' ? baseRefOf(s.vcs) : null;
|
|
127
|
+
if (!named?.name)
|
|
119
128
|
continue;
|
|
120
|
-
const there = (await git(repoDir, ['rev-parse', '--verify', '--quiet',
|
|
129
|
+
const there = (await git(repoDir, ['rev-parse', '--verify', '--quiet', `${named.ref}^{commit}`])).stdout;
|
|
121
130
|
if (!there) {
|
|
122
131
|
blocking = true;
|
|
123
|
-
|
|
132
|
+
branchMissing = true;
|
|
133
|
+
problem =
|
|
134
|
+
`${problem ? `${problem} ` : ''}"${s.name}" is set to ${s.vcs?.startFrom === 'existing-branch' ? 'carry on' : 'start from'} the ${named.remote ? 'remote' : 'local'} branch "${named.name}", and there is no such branch` +
|
|
135
|
+
(named.remote ? ' here: start the run again and choose it in the start window, which fetches it.' : ': create it, or choose another branch when you start the run.');
|
|
124
136
|
}
|
|
125
137
|
}
|
|
126
138
|
const noCommits = first.vcs?.commitOnFinish === false;
|
|
@@ -136,7 +148,7 @@ async function groupFor(list, allSessions) {
|
|
|
136
148
|
}
|
|
137
149
|
if (baseBranch && !first.vcsBaseCommit && inputs.length > 0) {
|
|
138
150
|
const why = !baseHead
|
|
139
|
-
? `there is no local branch "${baseBranch}"`
|
|
151
|
+
? `there is no ${base?.remote ? 'fetched copy of the remote' : 'local'} branch "${baseBranch}"`
|
|
140
152
|
: unrelated.length > 0
|
|
141
153
|
? `besides the input files there are other uncommitted changes (${unrelated.slice(0, 5).map((u) => u.path).join(', ')}${unrelated.length > 5 ? '…' : ''}): commit or stash them first${branch !== baseBranch ? ', or use the current branch instead' : ', or take them as a starting snapshot'}`
|
|
142
154
|
: blockedInputs.length > 0
|
|
@@ -151,11 +163,11 @@ async function groupFor(list, allSessions) {
|
|
|
151
163
|
recommended: !why,
|
|
152
164
|
...(why ? { why } : {}),
|
|
153
165
|
// The update from the remote said first: it moves the base branch, and the checkout with it when that is the branch checked out (live run 2026-10-04).
|
|
154
|
-
result: (
|
|
166
|
+
result: (updates
|
|
155
167
|
? `First brings "${baseBranch}" up to its remote, fast-forward only${branch === baseBranch ? ', which moves your checkout with it' : ''}; then commits`
|
|
156
168
|
: 'Commits') +
|
|
157
|
-
` the ${inputs.length} input file(s) on top of "${baseBranch}" (${short(baseHead)}${
|
|
158
|
-
`without switching branches: your checkout${branch ? ` of "${branch}"` : ''} and the files stay as they are${
|
|
169
|
+
` the ${inputs.length} input file(s) on top of "${baseBranch}" (${short(baseHead)}${updates ? ' now, or where the update takes it' : ''}) as "Capture user-provided inputs", on the new branch ${baselineName}, ` +
|
|
170
|
+
`without switching branches: your checkout${branch ? ` of "${branch}"` : ''} and the files stay as they are${updates && branch === baseBranch ? ', apart from that update' : ''}. ` +
|
|
159
171
|
`${names(sharing)} ${verb(sharing, 'starts', 'start')} from it${inheriting.length > 0 ? `; ${names(inheriting)} ${verb(inheriting, 'carries', 'carry')} it on through the chain` : ''}.`,
|
|
160
172
|
});
|
|
161
173
|
}
|
|
@@ -218,6 +230,7 @@ async function groupFor(list, allSessions) {
|
|
|
218
230
|
head,
|
|
219
231
|
...(baseBranch ? { baseBranch, baseHead } : {}),
|
|
220
232
|
...(workBranch && workBranch.name !== branch ? { workBranch } : {}),
|
|
233
|
+
...(!ready && !otherThanStart && (branchMissing || pre.startProblem) ? { startChoiceCanFix: true } : {}),
|
|
221
234
|
...(settings && inputs.length === 0 && !first.vcsBaseCommit && !blocking
|
|
222
235
|
? { carry: `the input files (${settings.patterns.join(', ')}) are not in the working tree: when the first task starts they are committed on top of its start as ${prefix}input/${first.id}, from the latest earlier capture of them` }
|
|
223
236
|
: {}),
|
|
@@ -257,6 +270,8 @@ export async function runVcsPrepare(sessions, repoDir, action, choices, bus, sav
|
|
|
257
270
|
if (x.vcs) {
|
|
258
271
|
x.vcs.startFrom = 'branch';
|
|
259
272
|
x.vcs.baseBranch = group.branch ?? undefined;
|
|
273
|
+
// The branch the repository is on is a local one.
|
|
274
|
+
delete x.vcs.baseRemote;
|
|
260
275
|
}
|
|
261
276
|
x.vcsBaseCommit = undefined;
|
|
262
277
|
x.vcsStart = undefined;
|
|
@@ -289,8 +304,10 @@ export async function runVcsPrepare(sessions, repoDir, action, choices, bus, sav
|
|
|
289
304
|
* branch does: the snapshot is the commit every session here starts from, and it skipped the update
|
|
290
305
|
* without a word, so a run cut its work from a main the team had already moved past (live run 2026-10-03).
|
|
291
306
|
*/
|
|
292
|
-
|
|
293
|
-
const
|
|
307
|
+
// Not a remote branch chosen in the start window: that was fetched there, and has no local branch to move.
|
|
308
|
+
const baseRef = baseRefOf(first.vcs);
|
|
309
|
+
const update = first.vcs?.updateFromRemote !== false && !baseRef.remote ? await updateFromRemote(top, group.baseBranch) : undefined;
|
|
310
|
+
const baseHead = (await git(top, ['rev-parse', '--verify', '--quiet', `${baseRef.ref}^{commit}`])).stdout || group.baseHead;
|
|
294
311
|
const files = [];
|
|
295
312
|
for (const path of paths) {
|
|
296
313
|
const blob = await git(top, ['hash-object', '-w', `--path=${path}`, '--', join(top, path)]);
|
package/dist/src/vcs/snapshot.js
CHANGED
|
@@ -119,7 +119,9 @@ async function startProblem(session, dir, branch, head, allSessions) {
|
|
|
119
119
|
const commit = resolved.start?.commit;
|
|
120
120
|
if (!commit || commit === head)
|
|
121
121
|
return undefined;
|
|
122
|
-
const wanted = resolved.start?.kind === 'previous-session'
|
|
122
|
+
const wanted = resolved.start?.kind === 'previous-session'
|
|
123
|
+
? `the end of "${resolved.start.fromSession?.name ?? ''}"'s branch ${resolved.start.branch}`
|
|
124
|
+
: `the ${resolved.start?.remote ? 'remote' : 'local'} branch ${resolved.start?.branch ?? ''}`;
|
|
123
125
|
return (`the changes are on ${where}, and this session is set to start from ${wanted} (${commit.slice(0, 8)}). ` +
|
|
124
126
|
'A snapshot is taken where the changes are, so the session would not start where it was told to. ' +
|
|
125
127
|
`Put the repository on ${resolved.start?.branch ?? 'that branch'} first, or choose "From wherever the repository is (as until now)".`);
|