copilot-operator 0.1.5 → 0.1.6

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 (45) hide show
  1. package/dist/src/api/operator.controller.js +5 -2
  2. package/dist/src/api/operator.service.js +34 -12
  3. package/dist/src/orchestrator/taskRunner.js +11 -1
  4. package/dist/src/plan/brief.js +52 -10
  5. package/dist/src/plan/script.js +13 -0
  6. package/dist/src/plan/systemGuide.js +1377 -185
  7. package/dist/src/session/compose.js +5 -1
  8. package/dist/src/session/exports.js +1 -0
  9. package/dist/src/session/store.js +4 -1
  10. package/dist/src/session/story.js +10 -1
  11. package/dist/src/vcs/taskVcs.js +2 -1
  12. package/dist/web/404.html +2 -2
  13. package/dist/web/_next/static/chunks/549-afafecd6bfbb138e.js +1 -0
  14. package/dist/web/_next/static/chunks/56-91cd1709158ebf99.js +1 -0
  15. package/dist/web/_next/static/chunks/867-fc983d08916b4e28.js +1 -0
  16. package/dist/web/_next/static/chunks/app/history/page-b3b4af994c868b5d.js +1 -0
  17. package/dist/web/_next/static/chunks/app/layout-e0174d863960f109.js +1 -0
  18. package/dist/web/_next/static/chunks/app/sessions/view/{page-4631bdf5113a73b2.js → page-d4213354730a945c.js} +1 -1
  19. package/dist/web/_next/static/css/{5a10c60adf9c5336.css → 63fea96806e6b2cb.css} +1 -1
  20. package/dist/web/appearance.html +2 -2
  21. package/dist/web/appearance.txt +25 -24
  22. package/dist/web/defaults.html +2 -2
  23. package/dist/web/defaults.txt +25 -24
  24. package/dist/web/history.html +2 -2
  25. package/dist/web/history.txt +25 -24
  26. package/dist/web/import.html +2 -2
  27. package/dist/web/import.txt +25 -24
  28. package/dist/web/index.html +2 -2
  29. package/dist/web/index.txt +25 -24
  30. package/dist/web/level1.html +2 -2
  31. package/dist/web/level1.txt +25 -24
  32. package/dist/web/presets.html +2 -2
  33. package/dist/web/presets.txt +25 -24
  34. package/dist/web/sessions/view.html +2 -2
  35. package/dist/web/sessions/view.txt +25 -24
  36. package/dist/web/system.html +2 -2
  37. package/dist/web/system.txt +25 -24
  38. package/package.json +1 -1
  39. package/dist/web/_next/static/chunks/549-2086c05135bc9753.js +0 -1
  40. package/dist/web/_next/static/chunks/867-0cd2c3cfd0ff0a45.js +0 -1
  41. package/dist/web/_next/static/chunks/89-382e9b29486d2747.js +0 -1
  42. package/dist/web/_next/static/chunks/app/history/page-83543937cecde5de.js +0 -1
  43. package/dist/web/_next/static/chunks/app/layout-92fa876afe619cf3.js +0 -1
  44. /package/dist/web/_next/static/{7TE2rEuHqGdGx14If8TEb → WJ_8hRZsp4VWiDZx8NMUw}/_buildManifest.js +0 -0
  45. /package/dist/web/_next/static/{7TE2rEuHqGdGx14If8TEb → WJ_8hRZsp4VWiDZx8NMUw}/_ssgManifest.js +0 -0
@@ -133,7 +133,8 @@ let OperatorController = class OperatorController {
133
133
  return this.ops.continueTask(id, taskId).catch(fail);
134
134
  }
135
135
  rerunTask(id, taskId, body) {
136
- return this.ops.rerunTask(id, taskId, body ?? {}).catch(fail);
136
+ const { buildOnFinished, ...patch } = body ?? {};
137
+ return this.ops.rerunTask(id, taskId, patch, { buildOnFinished: buildOnFinished === true }).catch(fail);
137
138
  }
138
139
  /** What going back to before this task would do. Changes nothing. */
139
140
  restorePreview(id, taskId) {
@@ -320,13 +321,15 @@ let OperatorController = class OperatorController {
320
321
  startBatch(body) {
321
322
  if (!Array.isArray(body?.sessionIds))
322
323
  throw new BadRequestException('sessionIds must be a list of session ids');
324
+ if (body?.taskIds !== undefined && !Array.isArray(body.taskIds))
325
+ throw new BadRequestException('taskIds must be a list of task ids');
323
326
  return this.ops
324
327
  .startBatch(body.sessionIds, body?.mode === 'unattended' ? 'unattended' : 'confirm', body?.onFailure === 'continue' ? 'continue' : 'stop',
325
328
  // Absent or empty means every session keeps the model it already has, which is not the
326
329
  // same as "no model": an empty string on a session means "leave the chat alone".
327
330
  body?.model,
328
331
  // The same for the review: absent means each session keeps whatever it is set to.
329
- body?.reviewModel, body?.name)
332
+ body?.reviewModel, body?.name, body?.taskIds)
330
333
  .catch(fail);
331
334
  }
332
335
  stopBatch() {
@@ -20,7 +20,7 @@ import { installLayout } from '../config/layout.js';
20
20
  import { SessionStore, DEFAULT_VCS, DEFAULT_REVIEW } from '../session/store.js';
21
21
  import { EventBus } from '../session/events.js';
22
22
  import { newId, tidyVcsPlan } from '../session/model.js';
23
- import { runSession, openBrowser } from '../orchestrator/taskRunner.js';
23
+ import { runSession, openBrowser, queuedToRun } from '../orchestrator/taskRunner.js';
24
24
  import { buildExport } from '../session/exportRecord.js';
25
25
  import { buildDebugExport } from '../session/debugExport.js';
26
26
  import { buildPlanExport, buildDomainExport, buildBotExport, buildBundleExport, exportFileName, exportMachine, taskAtAttempt, withTask, writeAttemptRecord } from '../session/exports.js';
@@ -328,16 +328,26 @@ let OperatorService = class OperatorService {
328
328
  });
329
329
  return task;
330
330
  }
331
- async rerunTask(sessionId, taskId, patch = {}) {
331
+ async rerunTask(sessionId, taskId, patch = {},
332
+ /**
333
+ * A new prompt for a task that ended done builds on what it did: the next attempt works on
334
+ * that attempt's branch. Only for a done task — a failed attempt's work is what is being
335
+ * replaced, so a re-run of one starts again from where the task first started.
336
+ */
337
+ opts = {}) {
332
338
  await this.init();
333
- const task = await this.store.rerunTask(sessionId, taskId, patch);
339
+ const before = (await this.store.getSession(sessionId))?.tasks.find((t) => t.id === taskId);
340
+ const buildsOn = opts.buildOnFinished && before?.status === 'done' ? { fromAttempt: before.attempt ?? 1 } : undefined;
341
+ const task = await this.store.rerunTask(sessionId, taskId, patch, undefined, buildsOn);
334
342
  this.bus.publish({
335
343
  sessionId,
336
344
  taskId,
337
345
  type: 'task-requeued',
338
346
  level: 'info',
339
- message: `"${task.title}" is queued again (attempt ${task.attempt ?? 1})`,
340
- data: { attempt: task.attempt },
347
+ message: buildsOn
348
+ ? `"${task.title}" is queued again with a new prompt that builds on attempt ${buildsOn.fromAttempt} (attempt ${task.attempt ?? 1})`
349
+ : `"${task.title}" is queued again (attempt ${task.attempt ?? 1})`,
350
+ data: { attempt: task.attempt, ...(buildsOn ? { buildsOn } : {}) },
341
351
  });
342
352
  return task;
343
353
  }
@@ -643,7 +653,9 @@ let OperatorService = class OperatorService {
643
653
  /** A browser that is already open. A batch passes its own; a single run opens one. */
644
654
  transport,
645
655
  /** The press of a start button this belongs to, stamped onto every task it reaches. */
646
- runGroup) {
656
+ runGroup,
657
+ /** Only these queued tasks, when the operator chose some; the rest stay queued. */
658
+ onlyTasks) {
647
659
  await this.init();
648
660
  // Every project's Desktop copy brought up to date before the chat opens, when the operator
649
661
  // asked for that. Before the run rather than before each task: a task's own mirror already
@@ -655,7 +667,7 @@ let OperatorService = class OperatorService {
655
667
  const session = await this.store.getSession(sessionId);
656
668
  if (!session)
657
669
  return { started: false, reason: 'no such session', done: Promise.resolve(idle) };
658
- const queuedIds = session.tasks.filter((t) => t.status === 'queued').map((t) => t.id);
670
+ const queuedIds = queuedToRun(session, onlyTasks).map((t) => t.id);
659
671
  if (queuedIds.length === 0)
660
672
  return { started: false, reason: 'no queued tasks', done: Promise.resolve(idle) };
661
673
  const cfg = await this.settings.load();
@@ -714,6 +726,7 @@ let OperatorService = class OperatorService {
714
726
  continueOnFailure: session.onFailure === 'continue',
715
727
  transport,
716
728
  runGroup,
729
+ onlyTasks,
717
730
  })
718
731
  .then(async (outcome) => ({ ...(await this.tally(sessionId, queuedIds)), paused: outcome.paused }))
719
732
  .catch(async (e) => {
@@ -816,8 +829,15 @@ let OperatorService = class OperatorService {
816
829
  */
817
830
  reviewModel,
818
831
  /** What to call the run. Offered from the plan's name; empty means it goes by its id. */
819
- name) {
832
+ name,
833
+ /**
834
+ * The queued tasks to run, when not every queued task of those sessions: what the operator
835
+ * ticked in the register. A session none of whose chosen tasks is queued is skipped; the
836
+ * tasks nobody chose stay queued exactly as they were. Absent means every queued task.
837
+ */
838
+ taskIds) {
820
839
  await this.init();
840
+ const onlyTasks = taskIds && taskIds.length > 0 ? new Set(taskIds.map((t) => t.trim()).filter(Boolean)) : undefined;
821
841
  if (this.batch?.running)
822
842
  return { started: false, reason: 'a batch is already running' };
823
843
  const wanted = [...new Set(sessionIds.map((s) => s.trim()).filter(Boolean))];
@@ -837,9 +857,9 @@ let OperatorService = class OperatorService {
837
857
  sessions.push({ sessionId: id, name: id, state: 'skipped', ran: 0, failed: 0, reason: 'no such session' });
838
858
  continue;
839
859
  }
840
- const queued = s.tasks.filter((t) => t.status === 'queued').length;
860
+ const queued = queuedToRun(s, onlyTasks).length;
841
861
  sessions.push(queued === 0
842
- ? { sessionId: id, name: s.name, state: 'skipped', ran: 0, failed: 0, reason: 'nothing queued' }
862
+ ? { sessionId: id, name: s.name, state: 'skipped', ran: 0, failed: 0, reason: onlyTasks ? 'none of its chosen tasks is queued' : 'nothing queued' }
843
863
  : { sessionId: id, name: s.name, state: 'waiting', ran: 0, failed: 0 });
844
864
  }
845
865
  if (!sessions.some((s) => s.state === 'waiting')) {
@@ -899,6 +919,7 @@ let OperatorService = class OperatorService {
899
919
  id: newId('b-'),
900
920
  startedAt: new Date().toISOString(),
901
921
  name: chosenName,
922
+ ...(onlyTasks ? { onlyTasks: [...onlyTasks] } : {}),
902
923
  mode,
903
924
  onFailure,
904
925
  stopping: false,
@@ -1045,7 +1066,8 @@ let OperatorService = class OperatorService {
1045
1066
  s.runGroup = {
1046
1067
  ...runGroup,
1047
1068
  order: at,
1048
- taskIds: s.tasks.filter((t) => t.status === 'queued').map((t) => t.id),
1069
+ // What this run was asked to do: the chosen tasks, when some were chosen.
1070
+ taskIds: queuedToRun(s, batch.onlyTasks).map((t) => t.id),
1049
1071
  mode: batch.mode,
1050
1072
  onFailure: batch.onFailure,
1051
1073
  };
@@ -1075,7 +1097,7 @@ let OperatorService = class OperatorService {
1075
1097
  message: `starting as part of a batch of ${batch.sessions.length} session(s), in ${batch.mode} mode`,
1076
1098
  data: { batchId: batch.id },
1077
1099
  });
1078
- const begun = await this.beginRun(entry.sessionId, batch.mode, browser, runGroup);
1100
+ const begun = await this.beginRun(entry.sessionId, batch.mode, browser, runGroup, batch.onlyTasks ? new Set(batch.onlyTasks) : undefined);
1079
1101
  if (!begun.started) {
1080
1102
  entry.state = 'skipped';
1081
1103
  entry.reason = begun.reason;
@@ -626,6 +626,7 @@ export async function runTask(transport, session, task, deps) {
626
626
  taskNumber,
627
627
  contractAlreadySent: session.contractSent,
628
628
  continuing: task.continuing,
629
+ buildsOn: task.buildsOn,
629
630
  workDirNote: workingDirNote(work),
630
631
  vcsNote: prepared.note,
631
632
  readOnlyNote: task.readOnly ? READ_ONLY_NOTE : undefined,
@@ -1462,6 +1463,15 @@ export async function runTask(transport, session, task, deps) {
1462
1463
  return await finish('failed', crash ? `${describeCrash(crash)}. Then: ${message}` : message);
1463
1464
  }
1464
1465
  }
1466
+ /**
1467
+ * The queued tasks of a session a run takes: all of them, or only the ones the operator chose.
1468
+ * One rule for the run itself and for every place that counts or records what a run will do, so
1469
+ * the batch entrance, the run's record and the run cannot disagree about it.
1470
+ */
1471
+ export function queuedToRun(session, onlyTasks) {
1472
+ const only = onlyTasks ? new Set(onlyTasks) : undefined;
1473
+ return session.tasks.filter((t) => t.status === 'queued' && (!only || only.has(t.id)));
1474
+ }
1465
1475
  /**
1466
1476
  * Runs every queued task of a session, in order, in one conversation. Stops at the first
1467
1477
  * task that does not end with `done` unless `continueOnFailure` is set, because a failed
@@ -1472,7 +1482,7 @@ export async function runSession(sessionId, deps) {
1472
1482
  let session = await store.getSession(sessionId);
1473
1483
  if (!session)
1474
1484
  throw new Error(`Session ${sessionId} does not exist.`);
1475
- const queued = session.tasks.filter((t) => t.status === 'queued');
1485
+ const queued = queuedToRun(session, deps.onlyTasks);
1476
1486
  if (queued.length === 0) {
1477
1487
  bus.publish({ sessionId, type: 'session-idle', level: 'info', message: 'no queued tasks' });
1478
1488
  return { ran: 0, paused: false };
@@ -633,6 +633,18 @@ your paraphrase means. Quote the label, and name the page by the route in the he
633
633
  button in the guide, say so and ask what they can see, rather than inventing one. A label written
634
634
  with {braces} is filled in with a number or a name at the time, so quote it with the braces and
635
635
  say what will be in them.
636
+
637
+ **When asked what to do next, or where something is:** first be sure which page they are on and
638
+ what they see — ask, in one line, if it is not clear. Then answer as numbered steps, each naming the
639
+ page by its route, the section by its bold heading from the guide, and the control by its exact
640
+ label. Say the condition a control needs when the guide gives one ("only in the **"Flow"** view",
641
+ "only after **"Work on branches and commit what was changed"** is ticked"), because a control that is
642
+ not showing is otherwise a control that "does not exist".
643
+
644
+ **A run that looks stuck is usually waiting for them.** A step waiting for a decision appears at the
645
+ top of every page under **"A step is waiting for your decision"**, one card per step, with **"Run"**,
646
+ **"Run this and the rest without asking"**, **"Skip"** and **"Abort task"**; the browser tab then
647
+ starts with "(n) waiting for you". Send them there first.
636
648
  `.trim();
637
649
  const GUIDE_RULE_BG = `
638
650
  ## Говори с думите от екрана
@@ -645,6 +657,18 @@ const GUIDE_RULE_BG = `
645
657
  **Никога не пращай оператора към контрол, който не е изброен там.** Ако това, което искаш да се
646
658
  направи, няма бутон в справочника, кажи го и попитай какво вижда, вместо да си измислиш. Надпис с
647
659
  {скоби} се попълва с число или име в момента — цитирай го със скобите и казвай какво ще има в тях.
660
+
661
+ **Когато те питат какво следва или къде е нещо:** първо се увери на коя страница е операторът и
662
+ какво вижда — попитай с един ред, ако не е ясно. После отговаряй с номерирани стъпки, като всяка
663
+ назовава страницата с маршрута, раздела с удебеленото му заглавие от справочника и контрола с точния
664
+ му надпис. Казвай условието, което контролът изисква, когато справочникът го дава („само в изгледа
665
+ **„Поток“**“, „само след отметка на **„Работи по клонове и комитвай промененото“**“), защото
666
+ контрол, който не се вижда, иначе е контрол, който „го няма“.
667
+
668
+ **Пускане, което изглежда заседнало, обикновено чака оператора.** Стъпка, чакаща решение, се
669
+ появява горе на всяка страница под **„Стъпка чака вашето решение“**, по една карта за стъпка, с
670
+ **„Изпълни“**, **„Изпълни без да питаш повече“**, **„Пропусни“** и **„Прекрати задачата“**; разделът
671
+ на браузъра тогава започва с „(n) чака вас“. Прати го там първо.
648
672
  `.trim();
649
673
  /**
650
674
  * The persona's two later roles, software level: what the operator brings back when a task
@@ -699,14 +723,23 @@ in those words, every time:
699
723
  - **A new prompt.** The task text was ambiguous, a path or a port was wrong, the level 2 forbade
700
724
  what the task needed, a check asked the git index about a file the runner had not committed yet,
701
725
  or the chat did the wrong thing and the checks caught it. Give the whole replacement text, then:
702
- 1. On \`/history\`, press **"Fix the prompt and queue it again"** on that row.
726
+ 1. On \`/history\`, in the **"Flow"** view, press **"Fix the prompt and queue it again"** on that row.
703
727
  2. Replace the text with the one above and press **"Save and queue again"**.
704
- 3. Press **"Continue: run the {n} queued task(s) in {s} session(s)"**.
705
- 4. In **"Before it continues"**, press **"Continue without asking"** — the left-hand button —
706
- to let it run, or **"Continue, asking before each command"** beside it to approve every one.
728
+ 3. Under **"What is next"**, press **"Continue: run the {n} queued task(s) in {s} session(s)"**
729
+ (it reads **"Continue: run {f} failed and {n} queued task(s) in {s} session(s)"** when a chain
730
+ has a failed task).
731
+ 4. In **"Before it continues"** every task is ticked. To run only the fixed one, press
732
+ **"only this one"** beside it.
733
+ 5. Press **"Continue without asking"** — the left-hand button — to let it run, or
734
+ **"Continue, asking before each command"** beside it to approve every one.
707
735
 
708
736
  A check that is wrong rather than a prompt that is wrong is edited instead: **"Edit"** on the
709
- task card, then **"What it checks"**, **"Must be"** and **"Command"** under **"Checks"**.
737
+ task card, then **"What it checks"**, **"Must be"** and **"Command"** under **"Checks"**, then
738
+ **"Save and queue it again"** — which, like every "queue" button, starts nothing by itself.
739
+ A task that ended **limit-reached** is carried on in its own chat instead: **"Continue in the
740
+ same chat"** on its card on the session page, then **"Run {n} task(s)"** at the top of that page.
741
+ When a failure shows up while the run is still going, **"Pause after this task"** holds the rest
742
+ while you work it out.
710
743
  Putting the repository back to before the task is **"Restore"**; putting it back and re-running
711
744
  that task and every task after it is **"Run again from here"**.
712
745
  - **A change to the bot.** No wording fixes it: the shell the task needs is not on the machine,
@@ -851,14 +884,23 @@ const PHASE3_BG = `
851
884
  забранявало това, което задачата иска, проверка е питала git индекса за файл, който runner-ът
852
885
  още не е комитнал, или чатът е направил грешното нещо и проверките са го хванали. Дай целия
853
886
  нов текст, после:
854
- 1. В \`/history\` натисни **„Поправи prompt-а и върни в опашката“** на този ред.
887
+ 1. В \`/history\`, в изгледа **„Поток“**, натисни **„Поправи prompt-а и върни в опашката“** на този ред.
855
888
  2. Замени текста с горния и натисни **„Запази и върни в опашката“**.
856
- 3. Натисни **„Продължи: пусни {n} чакащи задачи в {s} сесии“**.
857
- 4. В **„Преди да продължи“** натисни **„Продължи без да пита“** — левият бутон — за да върви
858
- само, или **„Продължи, с питане преди всяка команда“** до него, за да одобряваш всяка.
889
+ 3. Под **„Какво следва“** натисни **„Продължи: пусни {n} чакащи задачи в {s} сесии“** (пише
890
+ **„Продължи: пусни {f} провалили се и {n} чакащи задачи в {s} сесии“**, когато във верига има
891
+ провалена задача).
892
+ 4. В **„Преди да продължи“** всички задачи са отметнати. За да пуснеш само поправената, натисни
893
+ **„само тази“** до нея.
894
+ 5. Натисни **„Продължи без да пита“** — левият бутон — за да върви само, или **„Продължи, с
895
+ питане преди всяка команда“** до него, за да одобряваш всяка.
859
896
 
860
897
  Когато е сгрешена проверката, а не prompt-ът, се редактира друго: **„Редактирай“** на картата
861
- на задачата, после **„Какво проверява“**, **„Трябва“** и **„Команда“** под **„Проверки“**.
898
+ на задачата, после **„Какво проверява“**, **„Трябва“** и **„Команда“** под **„Проверки“**, после
899
+ **„Запази и върни в опашката“** — който, като всеки бутон за връщане в опашката, сам не пуска нищо.
900
+ Задача, завършила с **limit-reached**, продължава в собствения си чат: **„Продължи в същия чат“**
901
+ на картата ѝ на страницата на сесията, после **„Пусни {n} задача(и)“** горе на същата страница.
902
+ Когато провал се покаже, докато пускането още върви, **„Пауза след тази задача“** задържа
903
+ останалото, докато го разбереш.
862
904
  Връщането на хранилището отпреди задачата е **„Върни“**; връщането му плюс ново изпълнение на
863
905
  тази задача и всички след нея е **„Пусни отново оттук“**.
864
906
  - **Промяна по бота.** Никакви думи не го оправят: обвивката, която задачата иска, я няма на
@@ -658,6 +658,19 @@ export const SCRIPT = [
658
658
  },
659
659
  ],
660
660
  },
661
+ {
662
+ choose: { en: 'Where each session starts, if 1.1 (`startFrom`):', bg: 'Откъде тръгва всяка сесия, ако е 1.1 (`startFrom`):' },
663
+ options: [
664
+ {
665
+ en: 'From the local branch main — every session starts from the same clean code (branch; name another in baseBranch)',
666
+ bg: 'От локалния клон main — всяка сесия тръгва от един и същ чист код (branch; друг клон в baseBranch)',
667
+ },
668
+ {
669
+ en: "From the previous session's branch — the sessions form a chain (previous-session)",
670
+ bg: 'От клона на предната сесия — сесиите образуват верига (previous-session)',
671
+ },
672
+ ],
673
+ },
661
674
  {
662
675
  choose: { en: 'If a task fails (`onFailure` of the session, required):', bg: 'Ако задача се провали (`onFailure` на сесията, задължително):' },
663
676
  options: [