copilot-operator 0.1.12 → 0.1.13

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 (39) hide show
  1. package/dist/src/api/operator.service.js +34 -2
  2. package/dist/src/context/desktopMirror.js +39 -1
  3. package/dist/src/orchestrator/taskRunner.js +24 -5
  4. package/dist/src/plan/brief.js +19 -0
  5. package/dist/src/plan/importPlan.js +3 -0
  6. package/dist/src/plan/schema.js +26 -2
  7. package/dist/src/plan/script.js +79 -75
  8. package/dist/src/plan/systemGuide.js +37 -5
  9. package/dist/src/session/exports.js +3 -0
  10. package/dist/src/vcs/git.js +77 -2
  11. package/dist/src/vcs/taskVcs.js +71 -13
  12. package/dist/web/404.html +2 -2
  13. package/dist/web/_next/static/chunks/867-84c19f047972ffc3.js +1 -0
  14. package/dist/web/_next/static/chunks/app/history/page-d7314ac22d47a3ab.js +1 -0
  15. package/dist/web/_next/static/chunks/app/sessions/view/page-34d7988849f9aa76.js +1 -0
  16. package/dist/web/appearance.html +2 -2
  17. package/dist/web/appearance.txt +11 -11
  18. package/dist/web/defaults.html +2 -2
  19. package/dist/web/defaults.txt +11 -11
  20. package/dist/web/history.html +2 -2
  21. package/dist/web/history.txt +11 -11
  22. package/dist/web/import.html +2 -2
  23. package/dist/web/import.txt +11 -11
  24. package/dist/web/index.html +2 -2
  25. package/dist/web/index.txt +11 -11
  26. package/dist/web/level1.html +2 -2
  27. package/dist/web/level1.txt +11 -11
  28. package/dist/web/presets.html +2 -2
  29. package/dist/web/presets.txt +11 -11
  30. package/dist/web/sessions/view.html +2 -2
  31. package/dist/web/sessions/view.txt +11 -11
  32. package/dist/web/system.html +2 -2
  33. package/dist/web/system.txt +11 -11
  34. package/package.json +1 -1
  35. package/dist/web/_next/static/chunks/867-b64c436f5b25f39e.js +0 -1
  36. package/dist/web/_next/static/chunks/app/history/page-854e3dde672e38a2.js +0 -1
  37. package/dist/web/_next/static/chunks/app/sessions/view/page-cc99401da5173b36.js +0 -1
  38. /package/dist/web/_next/static/{bcBE4tY_aNh6kzLzN1luP → NosxirjkOgV-Boyq8dGPl}/_buildManifest.js +0 -0
  39. /package/dist/web/_next/static/{bcBE4tY_aNh6kzLzN1luP → NosxirjkOgV-Boyq8dGPl}/_ssgManifest.js +0 -0
@@ -38,7 +38,7 @@ function sameFolder(a, b) {
38
38
  import { importPlan, plannedSessionSignature, taskSignature } from '../plan/importPlan.js';
39
39
  import { readInterruption } from '../session/interruption.js';
40
40
  import { commitInterrupted, repoDirOf, vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
41
- import { changedFilesBetween, fileAt, gitAvailable, repoUnusableReason } from '../vcs/git.js';
41
+ import { branchExists, changedFilesBetween, fileAt, gitAvailable, localBranches, plannedBranchName, repoUnusableReason } from '../vcs/git.js';
42
42
  import { makeAuthorizer } from '../exec/authorizer.js';
43
43
  import { listSelectableDirs, collectFiles, findSelectionConflicts, describeConflicts, DEFAULT_IGNORE_DIRS } from '../context/projectMirror.js';
44
44
  import { pickFolder } from './folderPicker.js';
@@ -277,6 +277,8 @@ let OperatorService = class OperatorService {
277
277
  }
278
278
  if (merged.baseBranch !== undefined)
279
279
  merged.baseBranch = merged.baseBranch.trim();
280
+ if (merged.existingBranch !== undefined)
281
+ merged.existingBranch = merged.existingBranch.trim();
280
282
  /*
281
283
  * Where the session starts is fixed at its first run. A new choice clears that record, so
282
284
  * the next task to run is cut from what was chosen now; the tasks that already ran keep
@@ -284,6 +286,8 @@ let OperatorService = class OperatorService {
284
286
  */
285
287
  const startChanged = (merged.startFrom ?? 'head') !== (s.vcs?.startFrom ?? 'head') ||
286
288
  (merged.baseBranch || 'main') !== (s.vcs?.baseBranch || 'main') ||
289
+ (merged.existingBranch ?? '') !== (s.vcs?.existingBranch ?? '') ||
290
+ (merged.updateFromRemote !== false) !== (s.vcs?.updateFromRemote !== false) ||
287
291
  merged.repoDir.toLowerCase() !== (s.vcs?.repoDir ?? '').trim().toLowerCase();
288
292
  if (startChanged) {
289
293
  s.vcsBaseCommit = undefined;
@@ -1361,6 +1365,31 @@ let OperatorService = class OperatorService {
1361
1365
  });
1362
1366
  }
1363
1367
  });
1368
+ if (repoIssues.length > 0)
1369
+ return { ok: false, issues: repoIssues, warnings: check.warnings, duplicates: [] };
1370
+ /*
1371
+ * A branch to carry on must be there to carry on — unless a session before it in this same plan
1372
+ * is the one that makes it. Refused now, with the branches that are there, rather than at the
1373
+ * first run with a task refused.
1374
+ */
1375
+ for (const [i, session] of check.plan.sessions.entries()) {
1376
+ const wanted = session.vcs?.enabled === false ? '' : (session.vcs?.existingBranch ?? '').trim();
1377
+ if (!wanted)
1378
+ continue;
1379
+ const dir = session.vcs?.repoDir?.trim() || session.mirror?.rootDir?.trim() || '';
1380
+ const madeEarlier = check.plan.sessions
1381
+ .slice(0, i)
1382
+ .some((e) => !!e.vcs?.branchName?.trim() && plannedBranchName(e.vcs.branchName, e.vcs.branchPrefix?.trim() || 'cop/') === wanted);
1383
+ if (madeEarlier || (await branchExists(dir, wanted)))
1384
+ continue;
1385
+ const have = await localBranches(dir);
1386
+ repoIssues.push({
1387
+ path: `sessions[${i}].vcs.existingBranch`,
1388
+ message: `${dir} has no local branch "${wanted}" to carry on. ` +
1389
+ (have.length > 0 ? `Its branches: ${have.slice(0, 20).join(', ')}.` : 'It has no branches yet.') +
1390
+ ' Give the exact name, or use startFrom "branch" to start a new one.',
1391
+ });
1392
+ }
1364
1393
  if (repoIssues.length > 0)
1365
1394
  return { ok: false, issues: repoIssues, warnings: check.warnings, duplicates: [] };
1366
1395
  return { ...check, duplicates: await this.findDuplicates(check.plan) };
@@ -1553,7 +1582,10 @@ let OperatorService = class OperatorService {
1553
1582
  return { ok: false, repoDir: '', problem: "git is not installed, or not on this machine's PATH.", git: null };
1554
1583
  // `branch` is where HEAD is; `work` is where the session's work is. After a per-task run
1555
1584
  // those differ, and the second is the one the operator is asking about.
1556
- return { ...(await vcsPreflight(session)), git, work: sessionBranches(session) };
1585
+ const preflight = await vcsPreflight(session);
1586
+ // The local branches, so "carry on an existing branch" can offer them rather than be typed blind.
1587
+ const branches = preflight.repoDir ? await localBranches(preflight.repoDir) : [];
1588
+ return { ...preflight, git, work: sessionBranches(session), branches };
1557
1589
  }
1558
1590
  /**
1559
1591
  * What going back to before a task would do, and then doing it.
@@ -14,7 +14,7 @@
14
14
  * `project.mirrorToDesktop`; a session's own mirror (the files attached to its first message)
15
15
  * uses the same folder for its project whether or not the switch is on.
16
16
  */
17
- import { basename, join, resolve } from 'node:path';
17
+ import { basename, join, resolve, relative, isAbsolute } from 'node:path';
18
18
  import { readdir, rm, stat } from 'node:fs/promises';
19
19
  import { mirrorProject } from './projectMirror.js';
20
20
  import { defaultExportDir } from './contextFiles.js';
@@ -38,6 +38,44 @@ export function knownProjects(cfg) {
38
38
  out.push({ name: o.name, rootDir: o.rootDir, isDefault: false, mirror: o.mirror ?? EMPTY_SELECTION, desktop: o.desktop !== false });
39
39
  return out;
40
40
  }
41
+ /**
42
+ * Whether the Desktop copy of the project at `rootDir` is kept by "Keep the projects on the Desktop".
43
+ *
44
+ * Then its Desktop folder has one owner, the Desktop mirror, and a task's attachments are copied
45
+ * elsewhere. The two used to share it with different selections — the whole project there, the
46
+ * task's folders here — so every run deleted most of the folder and every round of steps put it
47
+ * back: thousands of deletions and uploads in a OneDrive-synced Desktop, while the chat was
48
+ * uploading the task's own attachments to the same OneDrive (2026-09-30, a first message timing out).
49
+ */
50
+ export function keptOnDesktop(cfg, rootDir) {
51
+ return !!cfg.project?.mirrorToDesktop && knownProjects(cfg).some((p) => p.desktop && sameFolder(p.rootDir, rootDir));
52
+ }
53
+ /**
54
+ * The project folder to copy from, for a folder a session names as its project files' root.
55
+ *
56
+ * A folder in the bot's own Desktop area (`contextRoot`) is not a project: it is the bot's copy of
57
+ * one, flattened, and the place the copies are written to. Named as a root — seen live on 2026-09-30,
58
+ * `...\Desktop\copilot-operator-context\Automation` — the mirror read the folder it was writing: with
59
+ * "Keep the projects on the Desktop" on it copied its own copies over the originals while the Desktop
60
+ * mirror put them back, and the first message timed out; with it off the folder was not there and
61
+ * the task was refused. Such a folder is taken to mean the project it is the copy of; one that is the
62
+ * copy of no known project is refused, with what to choose instead.
63
+ */
64
+ export function mirrorSourceRoot(cfg, rootDir) {
65
+ const area = resolve(contextRoot(cfg));
66
+ const dir = resolve(rootDir);
67
+ const rel = relative(area, dir);
68
+ if (rel === '') {
69
+ return { problem: `${dir} is the folder where the bot keeps its Desktop copies, not a project. Choose the project's own folder as the root of the files to attach.` };
70
+ }
71
+ if (rel.startsWith('..') || isAbsolute(rel))
72
+ return { rootDir };
73
+ const copyOf = knownProjects(cfg).find((p) => sameFolder(projectTargetDir(cfg, p.name), join(area, rel.split(/[\\/]/)[0])));
74
+ if (!copyOf) {
75
+ return { problem: `${dir} is inside the folder where the bot keeps its Desktop copies (${area}), not a project, and it is not the copy of any project on the Project page. Choose the project's own folder as the root of the files to attach.` };
76
+ }
77
+ return { rootDir: copyOf.rootDir, fromCopy: dir };
78
+ }
41
79
  /** What to call the folder for a session's project: its Settings name, else the folder's own. */
42
80
  export function projectNameFor(rootDir, cfg) {
43
81
  const known = knownProjects(cfg).find((p) => sameFolder(p.rootDir, rootDir));
@@ -45,7 +45,7 @@ import { ProgressWatch } from './progress.js';
45
45
  import { contractConflicts, readsTreeClean } from './contract.js';
46
46
  import { treeFingerprint } from '../vcs/git.js';
47
47
  import { mirrorProject, describeMirror, MirrorSelectionError } from '../context/projectMirror.js';
48
- import { projectNameFor, projectTargetDir, removeLegacyFlatMirror } from '../context/desktopMirror.js';
48
+ import { keptOnDesktop, mirrorSourceRoot, projectNameFor, projectTargetDir, removeLegacyFlatMirror } from '../context/desktopMirror.js';
49
49
  import { prepareForTask, commitTaskResult, repoDirOf } from '../vcs/taskVcs.js';
50
50
  import { exportMachine, writeAttemptRecord } from '../session/exports.js';
51
51
  /** Why a task that was reported as done is being closed as failed. */
@@ -670,6 +670,12 @@ export async function runTask(transport, session, task, deps) {
670
670
  const prepared = await prepareForTask(session, task, bus, async (mutate) => {
671
671
  await store.updateSession(session.id, mutate);
672
672
  }, () => store.listSessions());
673
+ if (prepared.refuse) {
674
+ await setTask((t) => {
675
+ t.vcs = prepared.vcs;
676
+ });
677
+ return await finish('failed', prepared.refuse);
678
+ }
673
679
  if (prepared.vcs.branch || prepared.vcs.problem) {
674
680
  await setTask((t) => {
675
681
  t.vcs = prepared.vcs;
@@ -716,15 +722,28 @@ export async function runTask(transport, session, task, deps) {
716
722
  if (session.mirror.enabled && session.mirror.rootDir) {
717
723
  // One folder per project, and the project's name in front of every file name, so three
718
724
  // repositories mirrored in turn do not delete each other's copies or collide in the chat.
719
- const projectName = projectNameFor(session.mirror.rootDir, cfg);
720
- const targetDir = projectTargetDir(cfg, projectName);
725
+ // The bot's own Desktop copy named as the root means the project it is a copy of. See `mirrorSourceRoot`.
726
+ const source = mirrorSourceRoot(cfg, session.mirror.rootDir);
727
+ if ('problem' in source) {
728
+ sink.event('mirror-refused', { error: source.problem }, source.problem, 'error');
729
+ return await finish('failed', `${source.problem} Nothing was sent to the chat.`);
730
+ }
731
+ if (source.fromCopy) {
732
+ sink.event('mirror-source', { asked: source.fromCopy, used: source.rootDir }, `the root of the files to attach is the bot's Desktop copy ${source.fromCopy}; they are copied from the project itself, ${source.rootDir}`, 'warn');
733
+ }
734
+ const sourceRoot = source.rootDir;
735
+ const projectName = projectNameFor(sourceRoot, cfg);
736
+ // A project kept on the Desktop has its folder there looked after by the Desktop mirror; the
737
+ // attachments are copied into this attempt's own folder instead. See `keptOnDesktop`.
738
+ const onDesktop = keptOnDesktop(cfg, sourceRoot);
739
+ const targetDir = onDesktop ? log.path('attachments') : projectTargetDir(cfg, projectName);
721
740
  await removeLegacyFlatMirror(cfg).catch(() => undefined);
722
741
  /*
723
742
  * A selection that cannot be copied stops the task before anything is sent: the chat would
724
743
  * otherwise work without the files it was promised. The copies already there are kept.
725
744
  */
726
745
  const result = await mirrorProject({
727
- rootDir: session.mirror.rootDir,
746
+ rootDir: sourceRoot,
728
747
  // `rules-engine` in a plan usually means the repository's folder, wherever the root is.
729
748
  alsoUnder: [repoDirOf(session)],
730
749
  includeDirs: session.mirror.includeDirs.length ? session.mirror.includeDirs : ['.'],
@@ -744,7 +763,7 @@ export async function runTask(transport, session, task, deps) {
744
763
  sink.event('mirror-refused', { error: result.message }, result.message, 'error');
745
764
  return await finish('failed', `${result.message} Nothing was sent to the chat. Fix the files to attach and queue the task again.`);
746
765
  }
747
- sink.event('mirror', { targetDir, ...result }, `project mirror: ${describeMirror(result)}`);
766
+ sink.event('mirror', { targetDir, onDesktop, ...result }, `project mirror${onDesktop ? ' (attachments, in the run folder: the Desktop copy is kept by the Desktop mirror)' : ''}: ${describeMirror(result)}`);
748
767
  if (session.mirror.includeEnvFiles) {
749
768
  const envCount = Object.keys(result.mapping).filter((p) => /(^|\/)\.env(\.|$)/i.test(p)).length;
750
769
  if (envCount > 0) {
@@ -432,6 +432,16 @@ is clean: the runner commits after the checks, and its own commit is what leaves
432
432
  - \`"previous-session"\` — the session carries on from the end of the branch of the session that
433
433
  ran before it **in the same repository**, so the sessions form a chain. The first session of
434
434
  such a chain starts from \`baseBranch\`. Sessions in other repositories do not count.
435
+ - \`"existing-branch"\` with \`existingBranch\` — no new branch at all: every task of the session
436
+ works on that existing local branch, exactly as named (\`"existingBranch": "recovery/apz-migration"\`),
437
+ one after another, building on what is already on it. For new work that continues a branch the
438
+ user already has. The branch must exist when the plan is imported; \`branchMode\`, \`branchName\`
439
+ and the prefix do not apply.
440
+
441
+ Before a session's first branch the runner fetches and fast-forwards the branch it starts from
442
+ (\`baseBranch\`, or \`existingBranch\`), so the work begins from the server's code; it never resets or
443
+ merges. \`"updateFromRemote": false\` turns that off — only when the user asks to work offline or from
444
+ local commits on purpose.
435
445
 
436
446
  Put the same \`startFrom\` on every session that works in one repository unless the user wants it
437
447
  mixed. Leaving it out keeps the old behaviour — whatever branch the repository is on — which is
@@ -479,6 +489,15 @@ const VCS_DETAIL_BG = `
479
489
  - \`"previous-session"\` — сесията продължава от края на клона на сесията, пусната преди нея **в
480
490
  същото хранилище**, така че сесиите образуват верига. Първата сесия от веригата тръгва от
481
491
  \`baseBranch\`. Сесии в други хранилища не се броят.
492
+ - \`"existing-branch"\` с \`existingBranch\` — без нов клон: всяка задача от сесията работи на този
493
+ съществуващ локален клон, точно с това име (\`"existingBranch": "recovery/apz-migration"\`), една след
494
+ друга, върху това, което вече е там. За нова работа, която продължава клон, който потребителят вече
495
+ има. Клонът трябва да съществува при импорта; \`branchMode\`, \`branchName\` и представката не се прилагат.
496
+
497
+ Преди първия клон на сесията runner-ът прави fetch и fast-forward на клона, от който тя тръгва
498
+ (\`baseBranch\` или \`existingBranch\`), така че работата започва от кода на сървъра; никога не прави
499
+ reset или merge. \`"updateFromRemote": false\` го изключва — само ако потребителят поиска да работи
500
+ офлайн или нарочно от локални commit-и.
482
501
 
483
502
  Сложи един и същ \`startFrom\` на всички сесии в едно хранилище, освен ако потребителят не иска
484
503
  различни. Ако го пропуснеш, остава старото поведение — от който клон е хранилището в момента —
@@ -169,6 +169,9 @@ persona = '') {
169
169
  const vcs = { ...DEFAULT_VCS, ...(planned.vcs ?? {}) };
170
170
  vcs.branchPrefix = vcs.branchPrefix.trim() || DEFAULT_VCS.branchPrefix;
171
171
  vcs.repoDir = vcs.repoDir.trim();
172
+ // A branch to carry on, given alone, is that choice: see `VersionControl.startFrom`.
173
+ if (vcs.existingBranch?.trim())
174
+ vcs.startFrom = 'existing-branch';
172
175
  const group = planned.conversationGroup.trim() || sharedGroup;
173
176
  await store.updateSession(created.id, (s) => {
174
177
  s.onFailure = planned.onFailure;
@@ -44,16 +44,40 @@ const VcsInput = z
44
44
  * Absent keeps the old behaviour, wherever the repository is. See `VersionControl.startFrom`.
45
45
  */
46
46
  startFrom: z
47
- .enum(['branch', 'previous-session', 'head'], {
48
- error: 'startFrom must be "branch" (every session starts from the local branch in baseBranch, "main" unless said otherwise) or "previous-session" (each session carries on from the branch of the session before it in the same repository).',
47
+ .enum(['branch', 'previous-session', 'head', 'existing-branch'], {
48
+ error: 'startFrom must be "branch" (every session starts from the local branch in baseBranch, "main" unless said otherwise), "previous-session" (each session carries on from the branch of the session before it in the same repository) or "existing-branch" (the session works on the existing branch named in existingBranch).',
49
49
  })
50
50
  .optional(),
51
51
  /** The local branch `startFrom: "branch"` starts from, and `previous-session`'s fallback. */
52
52
  baseBranch: z.string().trim().optional(),
53
+ /**
54
+ * The existing local branch the whole session works on, exactly as named: no new branch, no
55
+ * prefix. Given alone it means `startFrom: "existing-branch"`. See `VersionControl.startFrom`.
56
+ */
57
+ existingBranch: z.string().trim().optional(),
58
+ /**
59
+ * Fetch and fast-forward the branch the session starts from before its first branch is cut.
60
+ * Absent means true. See `VersionControl.updateFromRemote`.
61
+ */
62
+ updateFromRemote: z.boolean().optional(),
53
63
  }, {
54
64
  error: 'Every session needs a "vcs" object saying whether the runner does version control for it and, when it does, which git repository to work in. Ask the user both questions and write the answers here.',
55
65
  })
56
66
  .superRefine((vcs, ctx) => {
67
+ if (vcs.startFrom === 'existing-branch' && !vcs.existingBranch) {
68
+ ctx.addIssue({
69
+ code: 'custom',
70
+ path: ['existingBranch'],
71
+ message: 'startFrom is "existing-branch", so "existingBranch" must name the local branch to carry on, exactly as it is in git (for example "recovery/apz-migration").',
72
+ });
73
+ }
74
+ if (vcs.existingBranch && vcs.startFrom && vcs.startFrom !== 'existing-branch') {
75
+ ctx.addIssue({
76
+ code: 'custom',
77
+ path: ['existingBranch'],
78
+ message: `"existingBranch" carries on a branch that exists, and startFrom "${vcs.startFrom}" cuts a new one. Keep one: startFrom "existing-branch" with existingBranch, or leave existingBranch out.`,
79
+ });
80
+ }
57
81
  if (vcs.enabled && !vcs.repoDir.trim()) {
58
82
  ctx.addIssue({
59
83
  code: 'custom',
@@ -669,6 +669,10 @@ export const SCRIPT = [
669
669
  en: "From the previous session's branch — the sessions form a chain (previous-session)",
670
670
  bg: 'От клона на предната сесия — сесиите образуват верига (previous-session)',
671
671
  },
672
+ {
673
+ en: 'On an existing branch — no new branch, the work continues on it; name it (existing-branch + existingBranch)',
674
+ bg: 'На съществуващ клон — без нов клон, работата продължава на него; посочи го (existing-branch + existingBranch)',
675
+ },
672
676
  ],
673
677
  },
674
678
  {
@@ -944,82 +948,82 @@ export function scriptSection(stages, projects, lang) {
944
948
  return `### ${m.when[lang]}\n\n${quoted.join('\n')}`;
945
949
  });
946
950
  const head = lang === 'bg'
947
- ? `## Въпросите, дословно
948
-
949
- Всяко съобщение, в което питаш оператора нещо, е тук — дословно, в реда, в който идват. **Копирай го
950
- точно:** същите въпроси, в същия ред, със същите номера, същите опции и същите редове *Пример:*. Не
951
- добавяй въпрос, не махай, не сливай, не разделяй, не преформулирай и не пренареждай — операторът учи
952
- разговора, а това става само ако той е един и същ всеки път. Заглавието над цитата е за теб: казва
953
- кога се праща съобщението; операторът вижда само цитираното, без знака \`>\`.
954
-
955
- - **Първият ред на всеки цитат е редът за фазата.** Копираш го както е; не го сменяш и не добавяш
956
- друг.
957
- - **Номерата са само цифри.** Въпросите са 1, 2, 3; опциите под въпрос 2 са 2.1, 2.2, 2.3. Никога
958
- букви. Съобщение с един-единствен избор номерира опциите си 1, 2, 3.
959
- - **Попълваш само това в {скоби}** — път, име, брой, присъда — и нищо друго не пишеш наново.
960
- **Съобщение с непопълнени {скоби} не се изпраща никога**: ако не знаеш с какво да ги попълниш,
961
- значи съобщението не е за сега.
962
- - **Препоръка:** на въпрос с опции, освен на тези за това какво операторът има или знае, може да
963
- добавиш след една опция *(препоръчвам — причината в няколко думи)*. Само ако имаш причина.
964
- - **Вече казано:** ако отговорът на въпрос вече е даден — в документите на оператора по-горе или
965
- по-рано в този разговор — въпросът остава на мястото си, а под него, след празен ред и със същия
966
- отстъп като редовете *Пример:*, пишеш \`${ALREADY_SAID.bg} …\` с отговора — отделен ред, не в края на
967
- въпроса; на избор отбелязваш опцията с *(вече казано)*. Отговорът е целият, не етикет: за
968
- заданието — заглавието, целта и контекстът от „Тази работа“, не само заглавието; за критериите —
969
- всеки от тях. Операторът потвърждава това, което вижда, и не може да потвърди нещо, което не е
970
- показано. Последният ред
971
- на цитата тогава е точно един от тези два: когато всеки въпрос е вече казан — *${ON_RECORD_ALL.bg}*
972
- — а когато само някои — *${ON_RECORD_MIXED.bg}*
973
- - **Около цитата:** преди него — само поздравът, и то само в първото съобщение; след него — нищо: без
974
- „Очаквам отговорите ти“ и без „Когато си готов…“. Последният ред на цитата вече казва какво следва.
975
- Съобщенията, които не питат нищо, също започват с реда за фазата си и свършват на последната си
976
- стъпка. Редът за фаза 2 е \`${PHASE_LINES.p2.bg}\`.
977
- - **Единственото място за твои въпроси** е мъгляв или липсващ отговор във фаза 1: тогава, преди
978
- настройките, пращаш едно съобщение с уточнения — редът за фазата, само номерирани въпроси, 1, 2, 3,
979
- с опции 1.1, 1.2, когато отговорите могат да се изброят, и същия последен ред като тук.
980
-
981
- Съобщенията, които не питат нищо — JSON-ът, стъпките в приложението, диагнозата, таблицата с
951
+ ? `## Въпросите, дословно
952
+
953
+ Всяко съобщение, в което питаш оператора нещо, е тук — дословно, в реда, в който идват. **Копирай го
954
+ точно:** същите въпроси, в същия ред, със същите номера, същите опции и същите редове *Пример:*. Не
955
+ добавяй въпрос, не махай, не сливай, не разделяй, не преформулирай и не пренареждай — операторът учи
956
+ разговора, а това става само ако той е един и същ всеки път. Заглавието над цитата е за теб: казва
957
+ кога се праща съобщението; операторът вижда само цитираното, без знака \`>\`.
958
+
959
+ - **Първият ред на всеки цитат е редът за фазата.** Копираш го както е; не го сменяш и не добавяш
960
+ друг.
961
+ - **Номерата са само цифри.** Въпросите са 1, 2, 3; опциите под въпрос 2 са 2.1, 2.2, 2.3. Никога
962
+ букви. Съобщение с един-единствен избор номерира опциите си 1, 2, 3.
963
+ - **Попълваш само това в {скоби}** — път, име, брой, присъда — и нищо друго не пишеш наново.
964
+ **Съобщение с непопълнени {скоби} не се изпраща никога**: ако не знаеш с какво да ги попълниш,
965
+ значи съобщението не е за сега.
966
+ - **Препоръка:** на въпрос с опции, освен на тези за това какво операторът има или знае, може да
967
+ добавиш след една опция *(препоръчвам — причината в няколко думи)*. Само ако имаш причина.
968
+ - **Вече казано:** ако отговорът на въпрос вече е даден — в документите на оператора по-горе или
969
+ по-рано в този разговор — въпросът остава на мястото си, а под него, след празен ред и със същия
970
+ отстъп като редовете *Пример:*, пишеш \`${ALREADY_SAID.bg} …\` с отговора — отделен ред, не в края на
971
+ въпроса; на избор отбелязваш опцията с *(вече казано)*. Отговорът е целият, не етикет: за
972
+ заданието — заглавието, целта и контекстът от „Тази работа“, не само заглавието; за критериите —
973
+ всеки от тях. Операторът потвърждава това, което вижда, и не може да потвърди нещо, което не е
974
+ показано. Последният ред
975
+ на цитата тогава е точно един от тези два: когато всеки въпрос е вече казан — *${ON_RECORD_ALL.bg}*
976
+ — а когато само някои — *${ON_RECORD_MIXED.bg}*
977
+ - **Около цитата:** преди него — само поздравът, и то само в първото съобщение; след него — нищо: без
978
+ „Очаквам отговорите ти“ и без „Когато си готов…“. Последният ред на цитата вече казва какво следва.
979
+ Съобщенията, които не питат нищо, също започват с реда за фазата си и свършват на последната си
980
+ стъпка. Редът за фаза 2 е \`${PHASE_LINES.p2.bg}\`.
981
+ - **Единственото място за твои въпроси** е мъгляв или липсващ отговор във фаза 1: тогава, преди
982
+ настройките, пращаш едно съобщение с уточнения — редът за фазата, само номерирани въпроси, 1, 2, 3,
983
+ с опции 1.1, 1.2, когато отговорите могат да се изброят, и същия последен ред като тук.
984
+
985
+ Съобщенията, които не питат нищо — JSON-ът, стъпките в приложението, диагнозата, таблицата с
982
986
  доказателствата — следват описанието във фазата си.`
983
- : `## The questions, word for word
984
-
985
- Every message in which you ask the operator something is here — word for word, in the order they
986
- come. **Copy it exactly:** the same questions, in the same order, with the same numbers, the same
987
- options and the same *Example:* lines. Do not add a question, drop one, merge, split, reword or
988
- reorder — the operator learns the conversation, and that only works if it is the same every time. The
989
- heading above each quotation is for you: it says when the message is sent; the operator sees only
990
- what is quoted, without the \`>\`.
991
-
992
- - **The first line of every quotation is the phase line.** Copy it as it is; do not change it and do
993
- not add another.
994
- - **Numbers are digits only.** Questions are 1, 2, 3; the options under question 2 are 2.1, 2.2, 2.3.
995
- Never letters. A message with one single choice numbers its options 1, 2, 3.
996
- - **You fill in only what is in {braces}** — a path, a name, a count, a verdict — and write nothing
997
- else afresh. **A message with unfilled {braces} is never sent**: if you do not know what goes in
998
- them, the message is not for now.
999
- - **Recommending:** on a question with options, except those about what the operator has or knows,
1000
- you may add after one option *(recommended — the reason in a few words)*. Only when you have one.
1001
- - **Already said:** if a question has already been answered — in the operator's documents above or
1002
- earlier in this conversation — the question stays where it is and, under it, after a blank line
1003
- and indented like the *Example:* lines, you write \`${ALREADY_SAID.en} …\` with the answer — a line
1004
- of its own, not the end of the question; on a choice you mark the option *(already said)*. The
1005
- answer is the whole of it, not a label: for the assignment, the title, the goal and the context from
1006
- "This work", not the title alone; for the criteria, every one of them. The operator confirms what
1007
- they see, and cannot confirm what is not shown.
1008
- The last line of the quotation is then exactly one of these two: when every question is already
1009
- answered — *${ON_RECORD_ALL.en}* — and when only some are — *${ON_RECORD_MIXED.en}*
1010
- - **Around the quotation:** before it, only the greeting, and only in the first message; after it,
1011
- nothing: no "I look forward to your answers", no "When you are ready…". The quotation's last line
1012
- already says what happens next. Messages that ask nothing also open with their phase line and end
1013
- on their last step. The phase 2 line is \`${PHASE_LINES.p2.en}\`.
1014
- - **The one place for questions of your own** is a vague or missing answer in phase 1: then, before
1015
- the settings, you send one message of follow-ups — the phase line, numbered questions only, 1, 2,
1016
- 3, with options 1.1, 1.2 where the answers can be listed, and the same last line as here.
1017
-
1018
- Messages that ask nothing — the JSON, the steps in the app, the diagnosis, the table of evidence —
987
+ : `## The questions, word for word
988
+
989
+ Every message in which you ask the operator something is here — word for word, in the order they
990
+ come. **Copy it exactly:** the same questions, in the same order, with the same numbers, the same
991
+ options and the same *Example:* lines. Do not add a question, drop one, merge, split, reword or
992
+ reorder — the operator learns the conversation, and that only works if it is the same every time. The
993
+ heading above each quotation is for you: it says when the message is sent; the operator sees only
994
+ what is quoted, without the \`>\`.
995
+
996
+ - **The first line of every quotation is the phase line.** Copy it as it is; do not change it and do
997
+ not add another.
998
+ - **Numbers are digits only.** Questions are 1, 2, 3; the options under question 2 are 2.1, 2.2, 2.3.
999
+ Never letters. A message with one single choice numbers its options 1, 2, 3.
1000
+ - **You fill in only what is in {braces}** — a path, a name, a count, a verdict — and write nothing
1001
+ else afresh. **A message with unfilled {braces} is never sent**: if you do not know what goes in
1002
+ them, the message is not for now.
1003
+ - **Recommending:** on a question with options, except those about what the operator has or knows,
1004
+ you may add after one option *(recommended — the reason in a few words)*. Only when you have one.
1005
+ - **Already said:** if a question has already been answered — in the operator's documents above or
1006
+ earlier in this conversation — the question stays where it is and, under it, after a blank line
1007
+ and indented like the *Example:* lines, you write \`${ALREADY_SAID.en} …\` with the answer — a line
1008
+ of its own, not the end of the question; on a choice you mark the option *(already said)*. The
1009
+ answer is the whole of it, not a label: for the assignment, the title, the goal and the context from
1010
+ "This work", not the title alone; for the criteria, every one of them. The operator confirms what
1011
+ they see, and cannot confirm what is not shown.
1012
+ The last line of the quotation is then exactly one of these two: when every question is already
1013
+ answered — *${ON_RECORD_ALL.en}* — and when only some are — *${ON_RECORD_MIXED.en}*
1014
+ - **Around the quotation:** before it, only the greeting, and only in the first message; after it,
1015
+ nothing: no "I look forward to your answers", no "When you are ready…". The quotation's last line
1016
+ already says what happens next. Messages that ask nothing also open with their phase line and end
1017
+ on their last step. The phase 2 line is \`${PHASE_LINES.p2.en}\`.
1018
+ - **The one place for questions of your own** is a vague or missing answer in phase 1: then, before
1019
+ the settings, you send one message of follow-ups — the phase line, numbered questions only, 1, 2,
1020
+ 3, with options 1.1, 1.2 where the answers can be listed, and the same last line as here.
1021
+
1022
+ Messages that ask nothing — the JSON, the steps in the app, the diagnosis, the table of evidence —
1019
1023
  follow the description in their phase.`;
1020
- return `
1021
- ${head}
1022
-
1023
- ${blocks.join('\n\n')}
1024
+ return `
1025
+ ${head}
1026
+
1027
+ ${blocks.join('\n\n')}
1024
1028
  `;
1025
1029
  }
@@ -1128,6 +1128,30 @@ export const SYSTEM_GUIDE = [
1128
1128
  doesEn: 'The old behaviour, and what a session without a choice does: whatever branch is checked out at its first run, often the one the previous session left.',
1129
1129
  doesBg: 'Старото поведение и това, което прави сесия без избор: какъвто клон е избран при първото ѝ пускане, често оставеният от предната сесия.',
1130
1130
  },
1131
+ {
1132
+ key: 'vcs.startExisting',
1133
+ kind: 'radio',
1134
+ en: 'Carry on an existing branch',
1135
+ bg: 'Продължи съществуващ клон',
1136
+ doesEn: 'No new branch: every task works on the branch named under it, exactly as in git. A missing branch or uncommitted changes refuse the task; nothing runs.',
1137
+ doesBg: 'Без нов клон: всяка задача работи на клона, посочен под него, точно както е в git. Липсващ клон или незакомитнати промени отказват задачата; нищо не се изпълнява.',
1138
+ },
1139
+ {
1140
+ key: 'vcs.existingBranch',
1141
+ kind: 'field',
1142
+ en: 'The branch to carry on',
1143
+ bg: 'Клонът, който да продължи',
1144
+ doesEn: 'The exact name of the branch "Carry on an existing branch" works on; the repository\'s local branches are offered as you type.',
1145
+ doesBg: 'Точното име на клона, на който работи „Продължи съществуващ клон“; докато пишеш, се предлагат локалните клонове.',
1146
+ },
1147
+ {
1148
+ key: 'vcs.updateFromRemote',
1149
+ kind: 'checkbox',
1150
+ en: 'Bring the starting branch up to date from the remote first',
1151
+ bg: 'Първо обнови началния клон от отдалеченото хранилище',
1152
+ doesEn: 'On by default. Before the session\'s first branch: git fetch, then a fast-forward of the starting branch only; a branch with its own commits is left and said.',
1153
+ doesBg: 'Включено по подразбиране. Преди първия клон на сесията: git fetch и само fast-forward на началния клон; клон със свои commit-и се оставя и се казва.',
1154
+ },
1131
1155
  {
1132
1156
  key: 'vcs.baseBranch',
1133
1157
  kind: 'field',
@@ -2190,6 +2214,14 @@ export const SYSTEM_GUIDE = [
2190
2214
  doesEn: 'The same button once the story is open: folds it away again.',
2191
2215
  doesBg: 'Същият бутон, когато историята е отворена: сгъва я обратно.',
2192
2216
  },
2217
+ {
2218
+ key: 'reg.rerunHere',
2219
+ kind: 'button',
2220
+ en: 'Run only this one again',
2221
+ bg: 'Пусни само тази отново',
2222
+ doesEn: 'On a row that failed or blocked on its work: says what happens to the chat, branch and record, then starts a new attempt of that task alone; the rest of the queue stays.',
2223
+ doesBg: 'На ред, провален или блокиран по работата: казва какво става с чата, клона и записа, после пуска нов опит само за тази задача; останалите в опашката остават.',
2224
+ },
2193
2225
  {
2194
2226
  key: 'reg.continueHere',
2195
2227
  kind: 'button',
@@ -2439,17 +2471,17 @@ export const SYSTEM_GUIDE = [
2439
2471
  {
2440
2472
  key: 'reg.pickAll',
2441
2473
  kind: 'button',
2442
- en: 'All of them',
2443
- bg: 'Всички',
2474
+ en: 'Tick all',
2475
+ bg: 'Маркирай всички',
2444
2476
  doesEn: 'In that panel: ticks every task.',
2445
2477
  doesBg: 'В панела: отметва всички задачи.',
2446
2478
  },
2447
2479
  {
2448
2480
  key: 'reg.pickNone',
2449
2481
  kind: 'button',
2450
- en: 'None',
2451
- bg: 'Нито една',
2452
- doesEn: 'In that panel: unticks every task (the start buttons then do nothing until one is ticked).',
2482
+ en: 'Untick all',
2483
+ bg: 'Размаркирай всички',
2484
+ doesEn: 'In that panel: unticks every task, beside the count of ticked ones (the start buttons then do nothing until one is ticked).',
2453
2485
  doesBg: 'В панела: маха всички отметки (бутоните за пускане тогава не действат, докато не се отметне поне една).',
2454
2486
  },
2455
2487
  {
@@ -144,6 +144,9 @@ export function buildPlanExport(scope) {
144
144
  // Only when chosen: absent is the old behaviour, and writing it out would suggest a choice.
145
145
  ...(s.vcs?.startFrom ? { startFrom: s.vcs.startFrom } : {}),
146
146
  ...(s.vcs?.baseBranch ? { baseBranch: s.vcs.baseBranch } : {}),
147
+ ...(s.vcs?.existingBranch ? { existingBranch: s.vcs.existingBranch } : {}),
148
+ // Only when switched off: absent is on.
149
+ ...(s.vcs?.updateFromRemote === false ? { updateFromRemote: false } : {}),
147
150
  },
148
151
  review: { enabled: s.review?.enabled !== false, model: s.review?.model ?? '' },
149
152
  mirror: {