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.
- package/dist/src/api/operator.service.js +34 -2
- package/dist/src/context/desktopMirror.js +39 -1
- package/dist/src/orchestrator/taskRunner.js +24 -5
- package/dist/src/plan/brief.js +19 -0
- package/dist/src/plan/importPlan.js +3 -0
- package/dist/src/plan/schema.js +26 -2
- package/dist/src/plan/script.js +79 -75
- package/dist/src/plan/systemGuide.js +37 -5
- package/dist/src/session/exports.js +3 -0
- package/dist/src/vcs/git.js +77 -2
- package/dist/src/vcs/taskVcs.js +71 -13
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/867-84c19f047972ffc3.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-d7314ac22d47a3ab.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-34d7988849f9aa76.js +1 -0
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +11 -11
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +11 -11
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +11 -11
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +11 -11
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +11 -11
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +11 -11
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +11 -11
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +11 -11
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +11 -11
- package/package.json +1 -1
- package/dist/web/_next/static/chunks/867-b64c436f5b25f39e.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-854e3dde672e38a2.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-cc99401da5173b36.js +0 -1
- /package/dist/web/_next/static/{bcBE4tY_aNh6kzLzN1luP → NosxirjkOgV-Boyq8dGPl}/_buildManifest.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
720
|
-
const
|
|
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:
|
|
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) {
|
package/dist/src/plan/brief.js
CHANGED
|
@@ -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;
|
package/dist/src/plan/schema.js
CHANGED
|
@@ -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)
|
|
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',
|
package/dist/src/plan/script.js
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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: {
|