copilot-operator 0.1.4 → 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 (47) hide show
  1. package/dist/src/api/operator.controller.js +5 -2
  2. package/dist/src/api/operator.service.js +48 -12
  3. package/dist/src/orchestrator/taskRunner.js +12 -2
  4. package/dist/src/plan/brief.js +1290 -1221
  5. package/dist/src/plan/schema.js +12 -0
  6. package/dist/src/plan/script.js +13 -0
  7. package/dist/src/plan/systemGuide.js +1396 -172
  8. package/dist/src/session/compose.js +5 -1
  9. package/dist/src/session/exports.js +4 -0
  10. package/dist/src/session/store.js +4 -1
  11. package/dist/src/session/story.js +10 -1
  12. package/dist/src/vcs/taskVcs.js +122 -12
  13. package/dist/web/404.html +2 -2
  14. package/dist/web/_next/static/chunks/549-afafecd6bfbb138e.js +1 -0
  15. package/dist/web/_next/static/chunks/56-91cd1709158ebf99.js +1 -0
  16. package/dist/web/_next/static/chunks/867-fc983d08916b4e28.js +1 -0
  17. package/dist/web/_next/static/chunks/app/history/page-b3b4af994c868b5d.js +1 -0
  18. package/dist/web/_next/static/chunks/app/layout-e0174d863960f109.js +1 -0
  19. package/dist/web/_next/static/chunks/app/sessions/view/page-d4213354730a945c.js +1 -0
  20. package/dist/web/_next/static/css/{5a10c60adf9c5336.css → 63fea96806e6b2cb.css} +1 -1
  21. package/dist/web/appearance.html +2 -2
  22. package/dist/web/appearance.txt +25 -24
  23. package/dist/web/defaults.html +2 -2
  24. package/dist/web/defaults.txt +25 -24
  25. package/dist/web/history.html +2 -2
  26. package/dist/web/history.txt +25 -24
  27. package/dist/web/import.html +2 -2
  28. package/dist/web/import.txt +25 -24
  29. package/dist/web/index.html +2 -2
  30. package/dist/web/index.txt +25 -24
  31. package/dist/web/level1.html +2 -2
  32. package/dist/web/level1.txt +25 -24
  33. package/dist/web/presets.html +2 -2
  34. package/dist/web/presets.txt +25 -24
  35. package/dist/web/sessions/view.html +2 -2
  36. package/dist/web/sessions/view.txt +25 -24
  37. package/dist/web/system.html +2 -2
  38. package/dist/web/system.txt +25 -24
  39. package/package.json +4 -3
  40. package/dist/web/_next/static/chunks/549-2086c05135bc9753.js +0 -1
  41. package/dist/web/_next/static/chunks/867-f0e3e152f6f67e45.js +0 -1
  42. package/dist/web/_next/static/chunks/89-382e9b29486d2747.js +0 -1
  43. package/dist/web/_next/static/chunks/app/history/page-8d4f817e36986e3f.js +0 -1
  44. package/dist/web/_next/static/chunks/app/layout-92fa876afe619cf3.js +0 -1
  45. package/dist/web/_next/static/chunks/app/sessions/view/page-185f5fcc383e6619.js +0 -1
  46. /package/dist/web/_next/static/{p9lexLS0u8W5N6VzLavHO → WJ_8hRZsp4VWiDZx8NMUw}/_buildManifest.js +0 -0
  47. /package/dist/web/_next/static/{p9lexLS0u8W5N6VzLavHO → 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';
@@ -249,6 +249,20 @@ let OperatorService = class OperatorService {
249
249
  if (reason)
250
250
  throw new Error(reason);
251
251
  }
252
+ if (merged.baseBranch !== undefined)
253
+ merged.baseBranch = merged.baseBranch.trim();
254
+ /*
255
+ * Where the session starts is fixed at its first run. A new choice clears that record, so
256
+ * the next task to run is cut from what was chosen now; the tasks that already ran keep
257
+ * their own base commits, which is what a re-run of them goes back to.
258
+ */
259
+ const startChanged = (merged.startFrom ?? 'head') !== (s.vcs?.startFrom ?? 'head') ||
260
+ (merged.baseBranch || 'main') !== (s.vcs?.baseBranch || 'main') ||
261
+ merged.repoDir.toLowerCase() !== (s.vcs?.repoDir ?? '').trim().toLowerCase();
262
+ if (startChanged) {
263
+ s.vcsBaseCommit = undefined;
264
+ s.vcsStart = undefined;
265
+ }
252
266
  s.vcs = merged;
253
267
  }
254
268
  });
@@ -314,16 +328,26 @@ let OperatorService = class OperatorService {
314
328
  });
315
329
  return task;
316
330
  }
317
- 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 = {}) {
318
338
  await this.init();
319
- 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);
320
342
  this.bus.publish({
321
343
  sessionId,
322
344
  taskId,
323
345
  type: 'task-requeued',
324
346
  level: 'info',
325
- message: `"${task.title}" is queued again (attempt ${task.attempt ?? 1})`,
326
- 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 } : {}) },
327
351
  });
328
352
  return task;
329
353
  }
@@ -629,7 +653,9 @@ let OperatorService = class OperatorService {
629
653
  /** A browser that is already open. A batch passes its own; a single run opens one. */
630
654
  transport,
631
655
  /** The press of a start button this belongs to, stamped onto every task it reaches. */
632
- runGroup) {
656
+ runGroup,
657
+ /** Only these queued tasks, when the operator chose some; the rest stay queued. */
658
+ onlyTasks) {
633
659
  await this.init();
634
660
  // Every project's Desktop copy brought up to date before the chat opens, when the operator
635
661
  // asked for that. Before the run rather than before each task: a task's own mirror already
@@ -641,7 +667,7 @@ let OperatorService = class OperatorService {
641
667
  const session = await this.store.getSession(sessionId);
642
668
  if (!session)
643
669
  return { started: false, reason: 'no such session', done: Promise.resolve(idle) };
644
- const queuedIds = session.tasks.filter((t) => t.status === 'queued').map((t) => t.id);
670
+ const queuedIds = queuedToRun(session, onlyTasks).map((t) => t.id);
645
671
  if (queuedIds.length === 0)
646
672
  return { started: false, reason: 'no queued tasks', done: Promise.resolve(idle) };
647
673
  const cfg = await this.settings.load();
@@ -700,6 +726,7 @@ let OperatorService = class OperatorService {
700
726
  continueOnFailure: session.onFailure === 'continue',
701
727
  transport,
702
728
  runGroup,
729
+ onlyTasks,
703
730
  })
704
731
  .then(async (outcome) => ({ ...(await this.tally(sessionId, queuedIds)), paused: outcome.paused }))
705
732
  .catch(async (e) => {
@@ -802,8 +829,15 @@ let OperatorService = class OperatorService {
802
829
  */
803
830
  reviewModel,
804
831
  /** What to call the run. Offered from the plan's name; empty means it goes by its id. */
805
- 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) {
806
839
  await this.init();
840
+ const onlyTasks = taskIds && taskIds.length > 0 ? new Set(taskIds.map((t) => t.trim()).filter(Boolean)) : undefined;
807
841
  if (this.batch?.running)
808
842
  return { started: false, reason: 'a batch is already running' };
809
843
  const wanted = [...new Set(sessionIds.map((s) => s.trim()).filter(Boolean))];
@@ -823,9 +857,9 @@ let OperatorService = class OperatorService {
823
857
  sessions.push({ sessionId: id, name: id, state: 'skipped', ran: 0, failed: 0, reason: 'no such session' });
824
858
  continue;
825
859
  }
826
- const queued = s.tasks.filter((t) => t.status === 'queued').length;
860
+ const queued = queuedToRun(s, onlyTasks).length;
827
861
  sessions.push(queued === 0
828
- ? { 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' }
829
863
  : { sessionId: id, name: s.name, state: 'waiting', ran: 0, failed: 0 });
830
864
  }
831
865
  if (!sessions.some((s) => s.state === 'waiting')) {
@@ -885,6 +919,7 @@ let OperatorService = class OperatorService {
885
919
  id: newId('b-'),
886
920
  startedAt: new Date().toISOString(),
887
921
  name: chosenName,
922
+ ...(onlyTasks ? { onlyTasks: [...onlyTasks] } : {}),
888
923
  mode,
889
924
  onFailure,
890
925
  stopping: false,
@@ -1031,7 +1066,8 @@ let OperatorService = class OperatorService {
1031
1066
  s.runGroup = {
1032
1067
  ...runGroup,
1033
1068
  order: at,
1034
- 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),
1035
1071
  mode: batch.mode,
1036
1072
  onFailure: batch.onFailure,
1037
1073
  };
@@ -1061,7 +1097,7 @@ let OperatorService = class OperatorService {
1061
1097
  message: `starting as part of a batch of ${batch.sessions.length} session(s), in ${batch.mode} mode`,
1062
1098
  data: { batchId: batch.id },
1063
1099
  });
1064
- 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);
1065
1101
  if (!begun.started) {
1066
1102
  entry.state = 'skipped';
1067
1103
  entry.reason = begun.reason;
@@ -567,7 +567,7 @@ export async function runTask(transport, session, task, deps) {
567
567
  // --- version control: a branch of this task's own, before anything is touched -------
568
568
  const prepared = await prepareForTask(session, task, bus, async (mutate) => {
569
569
  await store.updateSession(session.id, mutate);
570
- });
570
+ }, () => store.listSessions());
571
571
  if (prepared.vcs.branch || prepared.vcs.problem) {
572
572
  await setTask((t) => {
573
573
  t.vcs = prepared.vcs;
@@ -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 };