copilot-operator 0.1.5 → 0.1.7

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 +37 -2
  2. package/dist/src/api/operator.service.js +110 -14
  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 +1471 -190
  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/git.js +71 -0
  12. package/dist/src/vcs/taskVcs.js +2 -1
  13. package/dist/web/404.html +2 -2
  14. package/dist/web/_next/static/chunks/549-33f0d3d53338b2f2.js +1 -0
  15. package/dist/web/_next/static/chunks/723-edc7b5709e5407ec.js +1 -0
  16. package/dist/web/_next/static/chunks/867-4aa80f5a32311bce.js +1 -0
  17. package/dist/web/_next/static/chunks/app/history/page-2251664c0abe3f11.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-9517dd74c6d00f85.js +1 -0
  20. package/dist/web/_next/static/css/{5a10c60adf9c5336.css → b67acb571615432b.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-0cd2c3cfd0ff0a45.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-83543937cecde5de.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-4631bdf5113a73b2.js +0 -1
  46. /package/dist/web/_next/static/{7TE2rEuHqGdGx14If8TEb → DCpEMjhrg9APkRwBBw5Do}/_buildManifest.js +0 -0
  47. /package/dist/web/_next/static/{7TE2rEuHqGdGx14If8TEb → DCpEMjhrg9APkRwBBw5Do}/_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) {
@@ -177,6 +178,19 @@ let OperatorController = class OperatorController {
177
178
  throw new NotFoundException('This task has not run yet, or that attempt is not its own.');
178
179
  return story;
179
180
  }
181
+ /**
182
+ * What a task changed: the files between the commit it started from and the one it produced.
183
+ * `run` picks an earlier attempt. See `taskChanges`.
184
+ */
185
+ taskChanges(id, taskId, run) {
186
+ return this.ops.taskChanges(id, taskId, run?.trim() || undefined).catch(fail);
187
+ }
188
+ /** One of those files, before and after. Only a file the task changed can be asked for. */
189
+ taskChangeFile(id, taskId, path, run) {
190
+ if (!path)
191
+ throw new BadRequestException('path is required');
192
+ return this.ops.taskChangeFile(id, taskId, path, run?.trim() || undefined).catch(fail);
193
+ }
180
194
  taskFiles(id, taskId, run) {
181
195
  return this.ops.taskFiles(id, taskId, run);
182
196
  }
@@ -320,13 +334,15 @@ let OperatorController = class OperatorController {
320
334
  startBatch(body) {
321
335
  if (!Array.isArray(body?.sessionIds))
322
336
  throw new BadRequestException('sessionIds must be a list of session ids');
337
+ if (body?.taskIds !== undefined && !Array.isArray(body.taskIds))
338
+ throw new BadRequestException('taskIds must be a list of task ids');
323
339
  return this.ops
324
340
  .startBatch(body.sessionIds, body?.mode === 'unattended' ? 'unattended' : 'confirm', body?.onFailure === 'continue' ? 'continue' : 'stop',
325
341
  // Absent or empty means every session keeps the model it already has, which is not the
326
342
  // same as "no model": an empty string on a session means "leave the chat alone".
327
343
  body?.model,
328
344
  // The same for the review: absent means each session keeps whatever it is set to.
329
- body?.reviewModel, body?.name)
345
+ body?.reviewModel, body?.name, body?.taskIds)
330
346
  .catch(fail);
331
347
  }
332
348
  stopBatch() {
@@ -726,6 +742,25 @@ __decorate([
726
742
  __metadata("design:paramtypes", [String, String, String]),
727
743
  __metadata("design:returntype", Promise)
728
744
  ], OperatorController.prototype, "taskStory", null);
745
+ __decorate([
746
+ Get('sessions/:id/tasks/:taskId/changes'),
747
+ __param(0, Param('id')),
748
+ __param(1, Param('taskId')),
749
+ __param(2, Query('run')),
750
+ __metadata("design:type", Function),
751
+ __metadata("design:paramtypes", [String, String, String]),
752
+ __metadata("design:returntype", Promise)
753
+ ], OperatorController.prototype, "taskChanges", null);
754
+ __decorate([
755
+ Get('sessions/:id/tasks/:taskId/changes/file'),
756
+ __param(0, Param('id')),
757
+ __param(1, Param('taskId')),
758
+ __param(2, Query('path')),
759
+ __param(3, Query('run')),
760
+ __metadata("design:type", Function),
761
+ __metadata("design:paramtypes", [String, String, String, String]),
762
+ __metadata("design:returntype", Promise)
763
+ ], OperatorController.prototype, "taskChangeFile", null);
729
764
  __decorate([
730
765
  Get('sessions/:id/tasks/:taskId/files'),
731
766
  __param(0, Param('id')),
@@ -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';
@@ -36,8 +36,8 @@ function sameFolder(a, b) {
36
36
  return a.trim() !== '' && norm(a) === norm(b);
37
37
  }
38
38
  import { importPlan, plannedSessionSignature, taskSignature } from '../plan/importPlan.js';
39
- import { commitInterrupted, vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
40
- import { gitAvailable, repoUnusableReason } from '../vcs/git.js';
39
+ import { commitInterrupted, repoDirOf, vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
40
+ import { changedFilesBetween, fileAt, gitAvailable, repoUnusableReason } from '../vcs/git.js';
41
41
  import { makeAuthorizer } from '../exec/authorizer.js';
42
42
  import { listSelectableDirs, collectFiles, findSelectionConflicts, describeConflicts, DEFAULT_IGNORE_DIRS } from '../context/projectMirror.js';
43
43
  import { pickFolder } from './folderPicker.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
  }
@@ -378,6 +388,78 @@ let OperatorService = class OperatorService {
378
388
  return await buildStory(cfg.resolved.runsDir, chosen, view, live);
379
389
  }
380
390
  /** Files a task produced, so the UI can list reports and downloaded scripts. */
391
+ /**
392
+ * What a task changed in the repository: the files, between the commit it started from and the
393
+ * commit it produced. `runId` picks an earlier attempt. Read from git each time rather than kept
394
+ * on the record, so what is shown is the repository's own account; the record only says which
395
+ * two commits to compare.
396
+ *
397
+ * There is something to show only when version control was on and the task committed: a task
398
+ * that changed nothing, or ran with version control off, left no commit to compare, and the
399
+ * answer says which.
400
+ */
401
+ async taskChanges(sessionId, taskId, runId) {
402
+ const found = await this.changesOf(sessionId, taskId, runId);
403
+ if ('problem' in found)
404
+ return { ok: false, problem: found.problem, files: [] };
405
+ const listed = await changedFilesBetween(found.dir, found.base, found.commit);
406
+ if (listed.problem)
407
+ return { ok: false, problem: `the repository no longer has these commits: ${listed.problem}`, files: [] };
408
+ return { ok: true, repoDir: found.dir, branch: found.branch, base: found.base, commit: found.commit, files: listed.files };
409
+ }
410
+ /**
411
+ * One changed file, before and after, as text. Only a file the task changed can be asked for:
412
+ * the path is checked against that list, so this cannot be used to read anything else in the
413
+ * repository's history. A binary file, or one over 2 MB on either side, is said to be so rather
414
+ * than returned.
415
+ */
416
+ async taskChangeFile(sessionId, taskId, path, runId) {
417
+ const found = await this.changesOf(sessionId, taskId, runId);
418
+ if ('problem' in found)
419
+ throw new Error(found.problem);
420
+ const listed = await changedFilesBetween(found.dir, found.base, found.commit);
421
+ const file = listed.files.find((f) => f.path === path);
422
+ if (!file)
423
+ throw new Error('That file is not one this task changed.');
424
+ const MAX = 2 * 1024 * 1024;
425
+ const before = file.status === 'A' ? { bytes: null, tooLarge: false } : await fileAt(found.dir, found.base, file.oldPath ?? file.path, MAX);
426
+ const after = file.status === 'D' ? { bytes: null, tooLarge: false } : await fileAt(found.dir, found.commit, file.path, MAX);
427
+ // Git's own test for binary: a NUL byte in the first 8000.
428
+ const binary = [before.bytes, after.bytes].some((b) => !!b && b.subarray(0, 8000).includes(0));
429
+ const text = (b) => (b === null || binary ? null : b.toString('utf8').replace(/^\uFEFF/, ''));
430
+ return {
431
+ path: file.path,
432
+ ...(file.oldPath ? { oldPath: file.oldPath } : {}),
433
+ status: file.status,
434
+ before: text(before.bytes),
435
+ after: text(after.bytes),
436
+ binary,
437
+ tooLarge: before.tooLarge || after.tooLarge,
438
+ };
439
+ }
440
+ /** The repository and the two commits a task's changes lie between, or why there are none. */
441
+ async changesOf(sessionId, taskId, runId) {
442
+ await this.init();
443
+ const session = await this.store.getSession(sessionId);
444
+ const task = session?.tasks.find((t) => t.id === taskId);
445
+ if (!session || !task)
446
+ return { problem: 'No such task.' };
447
+ const chosen = runId ? resolveRunId(task, runId) : task.runId;
448
+ if (runId && !chosen)
449
+ return { problem: 'That attempt is not one of this task\'s.' };
450
+ const vcs = !chosen || chosen === task.runId ? task.vcs : (task.attempts ?? []).find((a) => a.runId === chosen)?.vcs;
451
+ if (!vcs?.baseCommit || !vcs.commit) {
452
+ return {
453
+ problem: vcs?.branch
454
+ ? 'This attempt committed nothing, so there are no changes to show.'
455
+ : 'Version control was not on for this attempt, so its changes were not recorded.',
456
+ };
457
+ }
458
+ const dir = repoDirOf(session);
459
+ if (!dir)
460
+ return { problem: 'The session has no repository folder set.' };
461
+ return { dir, base: vcs.baseCommit, commit: vcs.commit, branch: vcs.branch };
462
+ }
381
463
  async taskFiles(sessionId, taskId, runId) {
382
464
  const cfg = await this.settings.load();
383
465
  const s = await this.store.getSession(sessionId);
@@ -643,7 +725,9 @@ let OperatorService = class OperatorService {
643
725
  /** A browser that is already open. A batch passes its own; a single run opens one. */
644
726
  transport,
645
727
  /** The press of a start button this belongs to, stamped onto every task it reaches. */
646
- runGroup) {
728
+ runGroup,
729
+ /** Only these queued tasks, when the operator chose some; the rest stay queued. */
730
+ onlyTasks) {
647
731
  await this.init();
648
732
  // Every project's Desktop copy brought up to date before the chat opens, when the operator
649
733
  // asked for that. Before the run rather than before each task: a task's own mirror already
@@ -655,7 +739,7 @@ let OperatorService = class OperatorService {
655
739
  const session = await this.store.getSession(sessionId);
656
740
  if (!session)
657
741
  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);
742
+ const queuedIds = queuedToRun(session, onlyTasks).map((t) => t.id);
659
743
  if (queuedIds.length === 0)
660
744
  return { started: false, reason: 'no queued tasks', done: Promise.resolve(idle) };
661
745
  const cfg = await this.settings.load();
@@ -714,6 +798,7 @@ let OperatorService = class OperatorService {
714
798
  continueOnFailure: session.onFailure === 'continue',
715
799
  transport,
716
800
  runGroup,
801
+ onlyTasks,
717
802
  })
718
803
  .then(async (outcome) => ({ ...(await this.tally(sessionId, queuedIds)), paused: outcome.paused }))
719
804
  .catch(async (e) => {
@@ -816,8 +901,15 @@ let OperatorService = class OperatorService {
816
901
  */
817
902
  reviewModel,
818
903
  /** What to call the run. Offered from the plan's name; empty means it goes by its id. */
819
- name) {
904
+ name,
905
+ /**
906
+ * The queued tasks to run, when not every queued task of those sessions: what the operator
907
+ * ticked in the register. A session none of whose chosen tasks is queued is skipped; the
908
+ * tasks nobody chose stay queued exactly as they were. Absent means every queued task.
909
+ */
910
+ taskIds) {
820
911
  await this.init();
912
+ const onlyTasks = taskIds && taskIds.length > 0 ? new Set(taskIds.map((t) => t.trim()).filter(Boolean)) : undefined;
821
913
  if (this.batch?.running)
822
914
  return { started: false, reason: 'a batch is already running' };
823
915
  const wanted = [...new Set(sessionIds.map((s) => s.trim()).filter(Boolean))];
@@ -837,9 +929,9 @@ let OperatorService = class OperatorService {
837
929
  sessions.push({ sessionId: id, name: id, state: 'skipped', ran: 0, failed: 0, reason: 'no such session' });
838
930
  continue;
839
931
  }
840
- const queued = s.tasks.filter((t) => t.status === 'queued').length;
932
+ const queued = queuedToRun(s, onlyTasks).length;
841
933
  sessions.push(queued === 0
842
- ? { sessionId: id, name: s.name, state: 'skipped', ran: 0, failed: 0, reason: 'nothing queued' }
934
+ ? { sessionId: id, name: s.name, state: 'skipped', ran: 0, failed: 0, reason: onlyTasks ? 'none of its chosen tasks is queued' : 'nothing queued' }
843
935
  : { sessionId: id, name: s.name, state: 'waiting', ran: 0, failed: 0 });
844
936
  }
845
937
  if (!sessions.some((s) => s.state === 'waiting')) {
@@ -899,6 +991,7 @@ let OperatorService = class OperatorService {
899
991
  id: newId('b-'),
900
992
  startedAt: new Date().toISOString(),
901
993
  name: chosenName,
994
+ ...(onlyTasks ? { onlyTasks: [...onlyTasks] } : {}),
902
995
  mode,
903
996
  onFailure,
904
997
  stopping: false,
@@ -1045,7 +1138,8 @@ let OperatorService = class OperatorService {
1045
1138
  s.runGroup = {
1046
1139
  ...runGroup,
1047
1140
  order: at,
1048
- taskIds: s.tasks.filter((t) => t.status === 'queued').map((t) => t.id),
1141
+ // What this run was asked to do: the chosen tasks, when some were chosen.
1142
+ taskIds: queuedToRun(s, batch.onlyTasks).map((t) => t.id),
1049
1143
  mode: batch.mode,
1050
1144
  onFailure: batch.onFailure,
1051
1145
  };
@@ -1075,7 +1169,7 @@ let OperatorService = class OperatorService {
1075
1169
  message: `starting as part of a batch of ${batch.sessions.length} session(s), in ${batch.mode} mode`,
1076
1170
  data: { batchId: batch.id },
1077
1171
  });
1078
- const begun = await this.beginRun(entry.sessionId, batch.mode, browser, runGroup);
1172
+ const begun = await this.beginRun(entry.sessionId, batch.mode, browser, runGroup, batch.onlyTasks ? new Set(batch.onlyTasks) : undefined);
1079
1173
  if (!begun.started) {
1080
1174
  entry.state = 'skipped';
1081
1175
  entry.reason = begun.reason;
@@ -1925,6 +2019,7 @@ let OperatorService = class OperatorService {
1925
2019
  readOnly: t.readOnly || undefined,
1926
2020
  autoRetries: t.autoRetries || undefined,
1927
2021
  attempt: t.attempt,
2022
+ changedFiles: t.vcs?.commit && t.vcs.baseCommit ? (t.vcs.files?.length ?? 0) : undefined,
1928
2023
  attempts: (t.attempts ?? []).map((a, n) => {
1929
2024
  const from = a.startedAt ? Date.parse(a.startedAt) : undefined;
1930
2025
  const to = a.finishedAt ? Date.parse(a.finishedAt) : undefined;
@@ -1939,6 +2034,7 @@ let OperatorService = class OperatorService {
1939
2034
  reason: a.reason,
1940
2035
  runId: a.runId,
1941
2036
  branch: a.vcs?.branch,
2037
+ changedFiles: a.vcs?.commit && a.vcs.baseCommit ? (a.vcs.files?.length ?? 0) : undefined,
1942
2038
  };
1943
2039
  }),
1944
2040
  });
@@ -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: [