copilot-operator 0.1.6 → 0.1.8
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.controller.js +32 -0
- package/dist/src/api/operator.service.js +91 -11
- package/dist/src/plan/brief.js +4 -4
- package/dist/src/plan/systemGuide.js +116 -11
- package/dist/src/session/compose.js +24 -19
- package/dist/src/session/interruption.js +104 -0
- package/dist/src/session/store.js +21 -5
- package/dist/src/session/story.js +1 -1
- package/dist/src/vcs/git.js +71 -0
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/549-33f0d3d53338b2f2.js +1 -0
- package/dist/web/_next/static/chunks/723-edc7b5709e5407ec.js +1 -0
- package/dist/web/_next/static/chunks/867-3d4ee08c5f9f0e71.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/{page-1747c931adc746fd.js → page-dfcbd748f454d2d7.js} +1 -1
- package/dist/web/_next/static/chunks/app/history/page-49413c3a8266a0b7.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-a82e5e801dabc423.js +1 -0
- package/dist/web/_next/static/css/{63fea96806e6b2cb.css → b67acb571615432b.css} +1 -1
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +12 -12
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +12 -12
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +12 -12
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +12 -12
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +12 -12
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +12 -12
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +12 -12
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +12 -12
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +12 -12
- package/package.json +4 -3
- package/dist/web/_next/static/chunks/549-afafecd6bfbb138e.js +0 -1
- package/dist/web/_next/static/chunks/56-91cd1709158ebf99.js +0 -1
- package/dist/web/_next/static/chunks/867-fc983d08916b4e28.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-b3b4af994c868b5d.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-d4213354730a945c.js +0 -1
- /package/dist/web/_next/static/{WJ_8hRZsp4VWiDZx8NMUw → kysrkUS5Y-ZTHsIIXgs3q}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{WJ_8hRZsp4VWiDZx8NMUw → kysrkUS5Y-ZTHsIIXgs3q}/_ssgManifest.js +0 -0
|
@@ -178,6 +178,19 @@ let OperatorController = class OperatorController {
|
|
|
178
178
|
throw new NotFoundException('This task has not run yet, or that attempt is not its own.');
|
|
179
179
|
return story;
|
|
180
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
|
+
}
|
|
181
194
|
taskFiles(id, taskId, run) {
|
|
182
195
|
return this.ops.taskFiles(id, taskId, run);
|
|
183
196
|
}
|
|
@@ -729,6 +742,25 @@ __decorate([
|
|
|
729
742
|
__metadata("design:paramtypes", [String, String, String]),
|
|
730
743
|
__metadata("design:returntype", Promise)
|
|
731
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);
|
|
732
764
|
__decorate([
|
|
733
765
|
Get('sessions/:id/tasks/:taskId/files'),
|
|
734
766
|
__param(0, Param('id')),
|
|
@@ -36,8 +36,9 @@ 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 {
|
|
40
|
-
import {
|
|
39
|
+
import { readInterruption } from '../session/interruption.js';
|
|
40
|
+
import { commitInterrupted, repoDirOf, vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
|
|
41
|
+
import { changedFilesBetween, fileAt, gitAvailable, repoUnusableReason } from '../vcs/git.js';
|
|
41
42
|
import { makeAuthorizer } from '../exec/authorizer.js';
|
|
42
43
|
import { listSelectableDirs, collectFiles, findSelectionConflicts, describeConflicts, DEFAULT_IGNORE_DIRS } from '../context/projectMirror.js';
|
|
43
44
|
import { pickFolder } from './folderPicker.js';
|
|
@@ -101,19 +102,23 @@ let OperatorService = class OperatorService {
|
|
|
101
102
|
if (!session || !task)
|
|
102
103
|
return;
|
|
103
104
|
const vcs = await commitInterrupted(session, task, task.reason ?? 'interrupted', this.bus);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
const cfg = await this.settings.load();
|
|
106
|
+
// Where it had got to, from its run folder, so "Continue" can tell the chat exactly that.
|
|
107
|
+
const interruption = task.runId ? await readInterruption(join(cfg.resolved.runsDir, task.runId)) : null;
|
|
108
|
+
const settled = await this.store.updateSession(sessionId, (s) => {
|
|
109
|
+
const t = s.tasks.find((x) => x.id === taskId);
|
|
110
|
+
if (!t)
|
|
111
|
+
return;
|
|
112
|
+
if (interruption)
|
|
113
|
+
t.interruption = interruption;
|
|
114
|
+
if (vcs?.commit) {
|
|
109
115
|
t.vcs = vcs;
|
|
110
116
|
t.reason = `${t.reason ?? ''} What it had changed is committed on ${vcs.branch} as ${vcs.commit?.slice(0, 8)}.`.trim();
|
|
111
|
-
}
|
|
112
|
-
|
|
117
|
+
}
|
|
118
|
+
});
|
|
113
119
|
const ended = settled.tasks.find((t) => t.id === taskId);
|
|
114
120
|
if (!ended)
|
|
115
121
|
return;
|
|
116
|
-
const cfg = await this.settings.load();
|
|
117
122
|
await writeAttemptRecord(settled, ended, cfg.resolved.runsDir, exportMachine(cfg));
|
|
118
123
|
}
|
|
119
124
|
/** Runs the startup work now, so it does not wait for the first request. */
|
|
@@ -314,7 +319,7 @@ let OperatorService = class OperatorService {
|
|
|
314
319
|
* the text it ran with, and only then is the new text applied. Editing in place would leave
|
|
315
320
|
* the old attempt's summary sitting under a question that was never asked.
|
|
316
321
|
*/
|
|
317
|
-
/** "Continue" on a task that stopped
|
|
322
|
+
/** "Continue" on a task that stopped before it finished — its limit, the bot stopping, the operator: same chat, same branch, fresh count. */
|
|
318
323
|
async continueTask(sessionId, taskId) {
|
|
319
324
|
await this.init();
|
|
320
325
|
const task = await this.store.continueTask(sessionId, taskId);
|
|
@@ -388,6 +393,78 @@ let OperatorService = class OperatorService {
|
|
|
388
393
|
return await buildStory(cfg.resolved.runsDir, chosen, view, live);
|
|
389
394
|
}
|
|
390
395
|
/** Files a task produced, so the UI can list reports and downloaded scripts. */
|
|
396
|
+
/**
|
|
397
|
+
* What a task changed in the repository: the files, between the commit it started from and the
|
|
398
|
+
* commit it produced. `runId` picks an earlier attempt. Read from git each time rather than kept
|
|
399
|
+
* on the record, so what is shown is the repository's own account; the record only says which
|
|
400
|
+
* two commits to compare.
|
|
401
|
+
*
|
|
402
|
+
* There is something to show only when version control was on and the task committed: a task
|
|
403
|
+
* that changed nothing, or ran with version control off, left no commit to compare, and the
|
|
404
|
+
* answer says which.
|
|
405
|
+
*/
|
|
406
|
+
async taskChanges(sessionId, taskId, runId) {
|
|
407
|
+
const found = await this.changesOf(sessionId, taskId, runId);
|
|
408
|
+
if ('problem' in found)
|
|
409
|
+
return { ok: false, problem: found.problem, files: [] };
|
|
410
|
+
const listed = await changedFilesBetween(found.dir, found.base, found.commit);
|
|
411
|
+
if (listed.problem)
|
|
412
|
+
return { ok: false, problem: `the repository no longer has these commits: ${listed.problem}`, files: [] };
|
|
413
|
+
return { ok: true, repoDir: found.dir, branch: found.branch, base: found.base, commit: found.commit, files: listed.files };
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* One changed file, before and after, as text. Only a file the task changed can be asked for:
|
|
417
|
+
* the path is checked against that list, so this cannot be used to read anything else in the
|
|
418
|
+
* repository's history. A binary file, or one over 2 MB on either side, is said to be so rather
|
|
419
|
+
* than returned.
|
|
420
|
+
*/
|
|
421
|
+
async taskChangeFile(sessionId, taskId, path, runId) {
|
|
422
|
+
const found = await this.changesOf(sessionId, taskId, runId);
|
|
423
|
+
if ('problem' in found)
|
|
424
|
+
throw new Error(found.problem);
|
|
425
|
+
const listed = await changedFilesBetween(found.dir, found.base, found.commit);
|
|
426
|
+
const file = listed.files.find((f) => f.path === path);
|
|
427
|
+
if (!file)
|
|
428
|
+
throw new Error('That file is not one this task changed.');
|
|
429
|
+
const MAX = 2 * 1024 * 1024;
|
|
430
|
+
const before = file.status === 'A' ? { bytes: null, tooLarge: false } : await fileAt(found.dir, found.base, file.oldPath ?? file.path, MAX);
|
|
431
|
+
const after = file.status === 'D' ? { bytes: null, tooLarge: false } : await fileAt(found.dir, found.commit, file.path, MAX);
|
|
432
|
+
// Git's own test for binary: a NUL byte in the first 8000.
|
|
433
|
+
const binary = [before.bytes, after.bytes].some((b) => !!b && b.subarray(0, 8000).includes(0));
|
|
434
|
+
const text = (b) => (b === null || binary ? null : b.toString('utf8').replace(/^\uFEFF/, ''));
|
|
435
|
+
return {
|
|
436
|
+
path: file.path,
|
|
437
|
+
...(file.oldPath ? { oldPath: file.oldPath } : {}),
|
|
438
|
+
status: file.status,
|
|
439
|
+
before: text(before.bytes),
|
|
440
|
+
after: text(after.bytes),
|
|
441
|
+
binary,
|
|
442
|
+
tooLarge: before.tooLarge || after.tooLarge,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/** The repository and the two commits a task's changes lie between, or why there are none. */
|
|
446
|
+
async changesOf(sessionId, taskId, runId) {
|
|
447
|
+
await this.init();
|
|
448
|
+
const session = await this.store.getSession(sessionId);
|
|
449
|
+
const task = session?.tasks.find((t) => t.id === taskId);
|
|
450
|
+
if (!session || !task)
|
|
451
|
+
return { problem: 'No such task.' };
|
|
452
|
+
const chosen = runId ? resolveRunId(task, runId) : task.runId;
|
|
453
|
+
if (runId && !chosen)
|
|
454
|
+
return { problem: 'That attempt is not one of this task\'s.' };
|
|
455
|
+
const vcs = !chosen || chosen === task.runId ? task.vcs : (task.attempts ?? []).find((a) => a.runId === chosen)?.vcs;
|
|
456
|
+
if (!vcs?.baseCommit || !vcs.commit) {
|
|
457
|
+
return {
|
|
458
|
+
problem: vcs?.branch
|
|
459
|
+
? 'This attempt committed nothing, so there are no changes to show.'
|
|
460
|
+
: 'Version control was not on for this attempt, so its changes were not recorded.',
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const dir = repoDirOf(session);
|
|
464
|
+
if (!dir)
|
|
465
|
+
return { problem: 'The session has no repository folder set.' };
|
|
466
|
+
return { dir, base: vcs.baseCommit, commit: vcs.commit, branch: vcs.branch };
|
|
467
|
+
}
|
|
391
468
|
async taskFiles(sessionId, taskId, runId) {
|
|
392
469
|
const cfg = await this.settings.load();
|
|
393
470
|
const s = await this.store.getSession(sessionId);
|
|
@@ -1947,6 +2024,8 @@ let OperatorService = class OperatorService {
|
|
|
1947
2024
|
readOnly: t.readOnly || undefined,
|
|
1948
2025
|
autoRetries: t.autoRetries || undefined,
|
|
1949
2026
|
attempt: t.attempt,
|
|
2027
|
+
changedFiles: t.vcs?.commit && t.vcs.baseCommit ? (t.vcs.files?.length ?? 0) : undefined,
|
|
2028
|
+
interrupted: t.status === 'aborted' && !!t.interruption ? true : undefined,
|
|
1950
2029
|
attempts: (t.attempts ?? []).map((a, n) => {
|
|
1951
2030
|
const from = a.startedAt ? Date.parse(a.startedAt) : undefined;
|
|
1952
2031
|
const to = a.finishedAt ? Date.parse(a.finishedAt) : undefined;
|
|
@@ -1961,6 +2040,7 @@ let OperatorService = class OperatorService {
|
|
|
1961
2040
|
reason: a.reason,
|
|
1962
2041
|
runId: a.runId,
|
|
1963
2042
|
branch: a.vcs?.branch,
|
|
2043
|
+
changedFiles: a.vcs?.commit && a.vcs.baseCommit ? (a.vcs.files?.length ?? 0) : undefined,
|
|
1964
2044
|
};
|
|
1965
2045
|
}),
|
|
1966
2046
|
});
|
package/dist/src/plan/brief.js
CHANGED
|
@@ -726,7 +726,7 @@ in those words, every time:
|
|
|
726
726
|
1. On \`/history\`, in the **"Flow"** view, press **"Fix the prompt and queue it again"** on that row.
|
|
727
727
|
2. Replace the text with the one above and press **"Save and queue again"**.
|
|
728
728
|
3. Under **"What is next"**, press **"Continue: run the {n} queued task(s) in {s} session(s)"**
|
|
729
|
-
(it reads **"Continue: run {f}
|
|
729
|
+
(it reads **"Continue: run {f} unfinished and {n} queued task(s) in {s} session(s)"** when a chain
|
|
730
730
|
has a failed task).
|
|
731
731
|
4. In **"Before it continues"** every task is ticked. To run only the fixed one, press
|
|
732
732
|
**"only this one"** beside it.
|
|
@@ -736,7 +736,7 @@ in those words, every time:
|
|
|
736
736
|
A check that is wrong rather than a prompt that is wrong is edited instead: **"Edit"** on the
|
|
737
737
|
task card, then **"What it checks"**, **"Must be"** and **"Command"** under **"Checks"**, then
|
|
738
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
|
|
739
|
+
A task that ended **limit-reached**, or **aborted** because the bot stopped under it, is carried on in its own chat instead: **"Continue in the
|
|
740
740
|
same chat"** on its card on the session page, then **"Run {n} task(s)"** at the top of that page.
|
|
741
741
|
When a failure shows up while the run is still going, **"Pause after this task"** holds the rest
|
|
742
742
|
while you work it out.
|
|
@@ -887,7 +887,7 @@ const PHASE3_BG = `
|
|
|
887
887
|
1. В \`/history\`, в изгледа **„Поток“**, натисни **„Поправи prompt-а и върни в опашката“** на този ред.
|
|
888
888
|
2. Замени текста с горния и натисни **„Запази и върни в опашката“**.
|
|
889
889
|
3. Под **„Какво следва“** натисни **„Продължи: пусни {n} чакащи задачи в {s} сесии“** (пише
|
|
890
|
-
**„Продължи: пусни {f}
|
|
890
|
+
**„Продължи: пусни {f} недовършени и {n} чакащи задачи в {s} сесии“**, когато във верига има
|
|
891
891
|
провалена задача).
|
|
892
892
|
4. В **„Преди да продължи“** всички задачи са отметнати. За да пуснеш само поправената, натисни
|
|
893
893
|
**„само тази“** до нея.
|
|
@@ -897,7 +897,7 @@ const PHASE3_BG = `
|
|
|
897
897
|
Когато е сгрешена проверката, а не prompt-ът, се редактира друго: **„Редактирай“** на картата
|
|
898
898
|
на задачата, после **„Какво проверява“**, **„Трябва“** и **„Команда“** под **„Проверки“**, после
|
|
899
899
|
**„Запази и върни в опашката“** — който, като всеки бутон за връщане в опашката, сам не пуска нищо.
|
|
900
|
-
Задача, завършила с **limit-reached**, продължава в собствения си чат: **„Продължи в същия чат“**
|
|
900
|
+
Задача, завършила с **limit-reached** или **aborted**, защото ботът е спрял под нея, продължава в собствения си чат: **„Продължи в същия чат“**
|
|
901
901
|
на картата ѝ на страницата на сесията, после **„Пусни {n} задача(и)“** горе на същата страница.
|
|
902
902
|
Когато провал се покаже, докато пускането още върви, **„Пауза след тази задача“** задържа
|
|
903
903
|
останалото, докато го разбереш.
|
|
@@ -1442,8 +1442,8 @@ export const SYSTEM_GUIDE = [
|
|
|
1442
1442
|
kind: 'button',
|
|
1443
1443
|
en: 'Continue in the same chat',
|
|
1444
1444
|
bg: 'Продължи в същия чат',
|
|
1445
|
-
doesEn: '
|
|
1446
|
-
doesBg: '
|
|
1445
|
+
doesEn: 'On a task that stopped before it finished (its limit, the bot stopping, the operator): queues it to carry on in the same chat and branch. Then "Run {n} task(s)".',
|
|
1446
|
+
doesBg: 'На задача, спряла преди да приключи (лимит, спрял бот, оператор): нарежда я да продължи в същия чат и клон. После „Пусни {n} задача(и)“.',
|
|
1447
1447
|
},
|
|
1448
1448
|
{
|
|
1449
1449
|
key: 'task.rerun',
|
|
@@ -1549,6 +1549,14 @@ export const SYSTEM_GUIDE = [
|
|
|
1549
1549
|
doesEn: 'The files the task\'s commit changed.',
|
|
1550
1550
|
doesBg: 'Файловете, които комитът на задачата е променил.',
|
|
1551
1551
|
},
|
|
1552
|
+
{
|
|
1553
|
+
key: 'diff.open',
|
|
1554
|
+
kind: 'button',
|
|
1555
|
+
en: 'See the changes ({n} file(s))',
|
|
1556
|
+
bg: 'Виж промените ({n} файла)',
|
|
1557
|
+
doesEn: 'Where the attempt committed: every file it changed, before on the left and after on the right, scrolling together. Also under each earlier attempt.',
|
|
1558
|
+
doesBg: 'Където опитът е комитнал: всеки променен файл, преди вляво и след вдясно, превъртани заедно. И под всеки по-ранен опит.',
|
|
1559
|
+
},
|
|
1552
1560
|
{
|
|
1553
1561
|
key: 'task.deviations',
|
|
1554
1562
|
kind: 'disclosure',
|
|
@@ -2102,6 +2110,14 @@ export const SYSTEM_GUIDE = [
|
|
|
2102
2110
|
doesEn: 'Downloads what the runner did: the environment, every event and step with its exit code, the review machinery.',
|
|
2103
2111
|
doesBg: 'Изтегля какво е направил runner-ът: средата, всяко събитие и стъпка с кода на изход, механиката на рецензията.',
|
|
2104
2112
|
},
|
|
2113
|
+
{
|
|
2114
|
+
key: 'diff.open',
|
|
2115
|
+
kind: 'button',
|
|
2116
|
+
en: 'See the changes ({n} file(s))',
|
|
2117
|
+
bg: 'Виж промените ({n} файла)',
|
|
2118
|
+
doesEn: 'Where the attempt committed: every file it changed, before on the left and after on the right, scrolling together.',
|
|
2119
|
+
doesBg: 'Където опитът е комитнал: всеки променен файл, преди вляво и след вдясно, превъртани заедно.',
|
|
2120
|
+
},
|
|
2105
2121
|
{
|
|
2106
2122
|
key: 'story.show',
|
|
2107
2123
|
kind: 'button',
|
|
@@ -2300,6 +2316,14 @@ export const SYSTEM_GUIDE = [
|
|
|
2300
2316
|
en: 'What is next',
|
|
2301
2317
|
bg: 'Какво следва',
|
|
2302
2318
|
controls: [
|
|
2319
|
+
{
|
|
2320
|
+
key: 'reg.interruptedTitle',
|
|
2321
|
+
kind: 'note',
|
|
2322
|
+
en: 'The bot stopped in the middle of the work.',
|
|
2323
|
+
bg: 'Ботът спря посред работата.',
|
|
2324
|
+
doesEn: 'After the bot stopped under a run (power, Ctrl+C, a crash): the stopped tasks\' work is kept, and "Continue" carries them on where they stopped.',
|
|
2325
|
+
doesBg: 'След като ботът е спрял посред пускане (ток, Ctrl+C, срив): свършеното е запазено, а „Продължи“ продължава спрелите задачи оттам, където са спрели.',
|
|
2326
|
+
},
|
|
2303
2327
|
{
|
|
2304
2328
|
key: 'batch.pause',
|
|
2305
2329
|
kind: 'button',
|
|
@@ -2335,8 +2359,8 @@ export const SYSTEM_GUIDE = [
|
|
|
2335
2359
|
{
|
|
2336
2360
|
key: 'reg.continueWithFailed',
|
|
2337
2361
|
kind: 'button',
|
|
2338
|
-
en: 'Continue: run {f}
|
|
2339
|
-
bg: 'Продължи: пусни {f}
|
|
2362
|
+
en: 'Continue: run {f} unfinished and {n} queued task(s) in {s} session(s)',
|
|
2363
|
+
bg: 'Продължи: пусни {f} недовършени и {n} чакащи задачи в {s} сесии',
|
|
2340
2364
|
doesEn: 'The same button when a failed task of a chained session would go back into the queue with the queued ones.',
|
|
2341
2365
|
doesBg: 'Същият бутон, когато провалена задача от сесия-верига би се върнала в опашката заедно с чакащите.',
|
|
2342
2366
|
},
|
|
@@ -2401,8 +2425,8 @@ export const SYSTEM_GUIDE = [
|
|
|
2401
2425
|
kind: 'button',
|
|
2402
2426
|
en: 'Continue without asking',
|
|
2403
2427
|
bg: 'Продължи без да пита',
|
|
2404
|
-
doesEn: 'Starts exactly the ticked tasks
|
|
2405
|
-
doesBg: 'Пуска точно отметнатите задачи
|
|
2428
|
+
doesEn: 'Starts exactly the ticked tasks without asking (after "are you sure" unless Settings say otherwise). Stopped ones (aborted, limit reached) continue where they stopped; failed or blocked ones start afresh.',
|
|
2429
|
+
doesBg: 'Пуска точно отметнатите задачи без питане (след „сигурен ли си“, освен ако Настройките казват друго). Спрелите (прекратени, с лимит) продължават откъдето спряха; провалените и блокираните тръгват отначало.',
|
|
2406
2430
|
},
|
|
2407
2431
|
{
|
|
2408
2432
|
key: 'reg.continueGo',
|
|
@@ -2476,6 +2500,77 @@ export const SYSTEM_GUIDE = [
|
|
|
2476
2500
|
},
|
|
2477
2501
|
],
|
|
2478
2502
|
},
|
|
2503
|
+
{
|
|
2504
|
+
headingKey: 'diff.title',
|
|
2505
|
+
en: 'Changes in "{title}"',
|
|
2506
|
+
bg: 'Промени в „{title}“',
|
|
2507
|
+
controls: [
|
|
2508
|
+
{
|
|
2509
|
+
key: 'diff.nextChange',
|
|
2510
|
+
kind: 'button',
|
|
2511
|
+
en: 'Next change',
|
|
2512
|
+
bg: 'Следваща промяна',
|
|
2513
|
+
doesEn: 'Jumps to the next edit (key n).',
|
|
2514
|
+
doesBg: 'Отива на следващата промяна (клавиш n).',
|
|
2515
|
+
},
|
|
2516
|
+
{
|
|
2517
|
+
key: 'diff.prevChange',
|
|
2518
|
+
kind: 'button',
|
|
2519
|
+
en: 'Previous change',
|
|
2520
|
+
bg: 'Предишна промяна',
|
|
2521
|
+
doesEn: 'The one before (key p).',
|
|
2522
|
+
doesBg: 'Предишната (клавиш p).',
|
|
2523
|
+
},
|
|
2524
|
+
{
|
|
2525
|
+
key: 'diff.nextFile',
|
|
2526
|
+
kind: 'button',
|
|
2527
|
+
en: 'Next file',
|
|
2528
|
+
bg: 'Следващ файл',
|
|
2529
|
+
doesEn: 'The next file in the list on the left (key ]).',
|
|
2530
|
+
doesBg: 'Следващият файл от списъка вляво (клавиш ]).',
|
|
2531
|
+
},
|
|
2532
|
+
{
|
|
2533
|
+
key: 'diff.prevFile',
|
|
2534
|
+
kind: 'button',
|
|
2535
|
+
en: 'Previous file',
|
|
2536
|
+
bg: 'Предишен файл',
|
|
2537
|
+
doesEn: 'The file before (key [).',
|
|
2538
|
+
doesBg: 'Предишният файл (клавиш [).',
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
key: 'diff.gap',
|
|
2542
|
+
kind: 'button',
|
|
2543
|
+
en: '⋯ {n} unchanged line(s) — show them',
|
|
2544
|
+
bg: '⋯ {n} непроменени реда — покажи ги',
|
|
2545
|
+
doesEn: 'Opens a folded stretch of unchanged lines.',
|
|
2546
|
+
doesBg: 'Отваря сгънат участък непроменени редове.',
|
|
2547
|
+
},
|
|
2548
|
+
{
|
|
2549
|
+
key: 'diff.whole',
|
|
2550
|
+
kind: 'checkbox',
|
|
2551
|
+
en: 'Show the whole file',
|
|
2552
|
+
bg: 'Покажи целия файл',
|
|
2553
|
+
doesEn: 'Every line, nothing folded.',
|
|
2554
|
+
doesBg: 'Всички редове, нищо сгънато.',
|
|
2555
|
+
},
|
|
2556
|
+
{
|
|
2557
|
+
key: 'diff.wrap',
|
|
2558
|
+
kind: 'checkbox',
|
|
2559
|
+
en: 'Wrap long lines',
|
|
2560
|
+
bg: 'Пренасяй дългите редове',
|
|
2561
|
+
doesEn: 'Off: long lines stay on one line and the table scrolls sideways.',
|
|
2562
|
+
doesBg: 'Изключено: дългите редове остават на един ред и таблицата се превърта настрани.',
|
|
2563
|
+
},
|
|
2564
|
+
{
|
|
2565
|
+
key: 'diff.close',
|
|
2566
|
+
kind: 'button',
|
|
2567
|
+
en: 'Close',
|
|
2568
|
+
bg: 'Затвори',
|
|
2569
|
+
doesEn: 'Closes the view (Esc).',
|
|
2570
|
+
doesBg: 'Затваря изгледа (Esc).',
|
|
2571
|
+
},
|
|
2572
|
+
],
|
|
2573
|
+
},
|
|
2479
2574
|
],
|
|
2480
2575
|
},
|
|
2481
2576
|
{
|
|
@@ -2702,6 +2797,14 @@ export const SYSTEM_GUIDE = [
|
|
|
2702
2797
|
doesEn: 'How many minutes one task may run (default 240) before it stops as limit reached; saved as you type.',
|
|
2703
2798
|
doesBg: 'Колко минути може да върви една задача (по подразбиране 240), преди да спре като достигнат лимит; запазва се, докато пишете.',
|
|
2704
2799
|
},
|
|
2800
|
+
{
|
|
2801
|
+
key: 'exec.replyTimeout',
|
|
2802
|
+
kind: 'field',
|
|
2803
|
+
en: 'How long to wait for a reply from the chat',
|
|
2804
|
+
bg: 'Колко да се чака отговор от чата',
|
|
2805
|
+
doesEn: 'Seconds to wait for a chat reply (default 900); a reply not finished by then fails the task with "Copilot did not finish a reply within …".',
|
|
2806
|
+
doesBg: 'Секунди чакане на отговор от чата (по подразбиране 900); незавършен дотогава отговор проваля задачата с „Copilot did not finish a reply within …“.',
|
|
2807
|
+
},
|
|
2705
2808
|
{
|
|
2706
2809
|
key: 'exec.retryBlocked',
|
|
2707
2810
|
kind: 'field',
|
|
@@ -3123,10 +3226,10 @@ const KIND_WORDS = {
|
|
|
3123
3226
|
tab: { en: 'tab', bg: 'раздел' },
|
|
3124
3227
|
badge: { en: 'badge', bg: 'етикет' },
|
|
3125
3228
|
disclosure: { en: 'fold', bg: 'разгъване' },
|
|
3126
|
-
note: { en: '
|
|
3229
|
+
note: { en: 'text', bg: 'текст' },
|
|
3127
3230
|
};
|
|
3128
|
-
const INTRO_EN = 'Every label in quotes below is the exact text on the screen. Name the control by that text — press "Check it" — instead of describing it, and give the route so the operator knows which page they are on. A label with {braces} in it is filled in with numbers or names at the time.';
|
|
3129
|
-
const INTRO_BG = 'Всеки надпис в кавички по-долу е точният текст на екрана. Назовавай контрола с този текст — натисни „Провери“ — вместо да го описваш, и казвай маршрута, за да знае операторът на коя страница е. Надпис с {скоби} се попълва с числа или имена в момента.';
|
|
3231
|
+
const INTRO_EN = 'Every label in quotes below is the exact text on the screen. Name the control by that text — press "Check it" — instead of describing it, and give the route so the operator knows which page they are on. A label with {braces} in it is filled in with numbers or names at the time. A label with no kind after it is a button.';
|
|
3232
|
+
const INTRO_BG = 'Всеки надпис в кавички по-долу е точният текст на екрана. Назовавай контрола с този текст — натисни „Провери“ — вместо да го описваш, и казвай маршрута, за да знае операторът на коя страница е. Надпис с {скоби} се попълва с числа или имена в момента. Надпис без вид след него е бутон.';
|
|
3130
3233
|
/**
|
|
3131
3234
|
* The guide as the Markdown section the brief embeds. Dense on purpose: the brief is pasted
|
|
3132
3235
|
* into a chat window by hand and read by a model, so the words and what they do earn their
|
|
@@ -3146,8 +3249,10 @@ export function systemGuideSection(lang) {
|
|
|
3146
3249
|
lines.push('');
|
|
3147
3250
|
lines.push(`**${bg ? section.bg : section.en}**`);
|
|
3148
3251
|
for (const control of section.controls) {
|
|
3149
|
-
|
|
3150
|
-
|
|
3252
|
+
// A button is the common case and is said once in the introduction instead of on 120 lines:
|
|
3253
|
+
// the brief is pasted into a chat whose composer has a limit, and this is where it goes.
|
|
3254
|
+
const kind = control.kind === 'button' ? '' : ` (${KIND_WORDS[control.kind][lang]})`;
|
|
3255
|
+
lines.push(`- "${bg ? control.bg : control.en}"${kind} — ${bg ? control.doesBg : control.doesEn}`);
|
|
3151
3256
|
}
|
|
3152
3257
|
}
|
|
3153
3258
|
}
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Builds the messages that open a task in the chat.
|
|
3
|
-
*
|
|
4
|
-
* Two shapes, because a conversation is opened once and then reused:
|
|
5
|
-
*
|
|
6
|
-
* first task in a session level 1 (the contract, sent on its own and acknowledged),
|
|
7
|
-
* then level 2 plus the task in one message
|
|
8
|
-
* later tasks a short reminder that the contract still applies,
|
|
9
|
-
* then level 2 plus the task
|
|
10
|
-
*
|
|
11
|
-
* Level 2 is always sent with the task, even when it did not change, because it is cheap
|
|
12
|
-
* and it keeps each task self-contained in the transcript. The priority of level 1 over
|
|
13
|
-
* level 2 is stated inside level 1 itself; the composition only makes the boundary visible.
|
|
14
|
-
*/
|
|
1
|
+
import { describeInterruption } from './interruption.js';
|
|
15
2
|
const LEVEL2_HEADER = '## Project instructions (level 2)';
|
|
16
3
|
const TASK_HEADER = '## Task';
|
|
17
4
|
/**
|
|
@@ -50,6 +37,25 @@ function level2Block(level2) {
|
|
|
50
37
|
* The messages to send, in order. The last one is the one whose reply starts the loop.
|
|
51
38
|
* `firstMessage` is what the UI shows as "the prompt that opened this task".
|
|
52
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* Why the attempt being continued stopped, and where it was, in the words the chat is sent. The
|
|
42
|
+
* chat was mid-conversation when it stopped, so what it most needs is to know that nothing it did
|
|
43
|
+
* failed, that the files are as its work left them, and — after the bot stopped under it — which of
|
|
44
|
+
* the steps it had just asked for actually ran.
|
|
45
|
+
*/
|
|
46
|
+
function whyItStopped(c) {
|
|
47
|
+
if (c.how === 'interrupted') {
|
|
48
|
+
const where = c.interruption ? `\n\n${describeInterruption(c.interruption)}\n\n` : ' ';
|
|
49
|
+
return ('The runner stopped unexpectedly while this task was in progress — the program was closed, the machine went off or it ' +
|
|
50
|
+
'crashed; nothing you did failed — and it has now started again. The files are as your work left them, and what had ' +
|
|
51
|
+
`changed is committed on this branch.${where}`.trimEnd());
|
|
52
|
+
}
|
|
53
|
+
if (c.how === 'stopped') {
|
|
54
|
+
return 'It was stopped by the operator before it finished — not because anything failed. The files are as you left them.';
|
|
55
|
+
}
|
|
56
|
+
const why = c.stoppedBecause ? ` (${c.stoppedBecause})` : '';
|
|
57
|
+
return `It stopped because the runner's limit was reached${why} — not because anything failed. The files are as you left them.`;
|
|
58
|
+
}
|
|
53
59
|
export function composeOpening(input) {
|
|
54
60
|
const taskBlock = `${TASK_HEADER} ${input.taskNumber}: ${input.taskTitle.trim() || 'untitled'}\n\n${input.prompt.trim()}`;
|
|
55
61
|
/*
|
|
@@ -58,18 +64,17 @@ export function composeOpening(input) {
|
|
|
58
64
|
* conversation was lost does the task go out in full, with a line saying it is a continuation.
|
|
59
65
|
*/
|
|
60
66
|
if (input.continuing && input.contractAlreadySent) {
|
|
61
|
-
const why = input.continuing.stoppedBecause ? ` (${input.continuing.stoppedBecause})` : '';
|
|
62
67
|
const message = `Continue task ${input.taskNumber}: ${input.taskTitle.trim() || 'untitled'}, in this same conversation. ` +
|
|
63
|
-
|
|
64
|
-
`
|
|
68
|
+
`${whyItStopped(input.continuing)} ` +
|
|
69
|
+
`Do not start over and do not repeat work that is done: start from ` +
|
|
65
70
|
`your plan, say in \`notes\` which stages are done and which one you are on, and carry on. ` +
|
|
66
71
|
`The assignment is the one you were given above; it has not changed. Step numbering restarts at 1.\n\n` +
|
|
67
72
|
`${runnerBlock(input)}`.trimEnd();
|
|
68
73
|
return { messages: [message], firstMessage: message };
|
|
69
74
|
}
|
|
70
75
|
const continuationLine = input.continuing
|
|
71
|
-
? `\n\nThis continues an earlier attempt at this task
|
|
72
|
-
`
|
|
76
|
+
? `\n\nThis continues an earlier attempt at this task. ${whyItStopped(input.continuing)} ` +
|
|
77
|
+
`Look at what is already done before changing anything, and carry on from there.`
|
|
73
78
|
: input.buildsOn
|
|
74
79
|
? `\n\nThis task was done once already (attempt ${input.buildsOn.fromAttempt}), and that work is in your ` +
|
|
75
80
|
`working tree. The text above is a new instruction for it: look at what is there first, build on it, and ` +
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an attempt was when the bot stopped under it — the power went, the machine shut down,
|
|
3
|
+
* `npm start` was stopped with Ctrl+C, the process crashed — read back from what it had written.
|
|
4
|
+
*
|
|
5
|
+
* Nothing in memory survives that, but the run folder does, and it was written as the work went:
|
|
6
|
+
* the transcript has every reply and every step as it was proposed, started and finished, and each
|
|
7
|
+
* step's output is in its own log under `steps/`. So the last reply the chat sent, which of the
|
|
8
|
+
* steps it asked for had finished (and how), which one was cut off while it ran, which never ran,
|
|
9
|
+
* and whether their results had already gone back to the chat — all of that is on disk. It is what
|
|
10
|
+
* the chat needs to be told to carry on rather than start over, and what the operator needs to see
|
|
11
|
+
* to trust that it can.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { parseStepLog } from './story.js';
|
|
16
|
+
const TAIL_CHARS = 1200;
|
|
17
|
+
/** Reads an attempt's run folder; null when there is no transcript to read. */
|
|
18
|
+
export async function readInterruption(runDir) {
|
|
19
|
+
const raw = await readFile(join(runDir, 'transcript.jsonl'), 'utf8').catch(() => null);
|
|
20
|
+
if (raw === null)
|
|
21
|
+
return null;
|
|
22
|
+
const events = [];
|
|
23
|
+
for (const line of raw.split('\n')) {
|
|
24
|
+
if (!line.trim())
|
|
25
|
+
continue;
|
|
26
|
+
try {
|
|
27
|
+
events.push(JSON.parse(line));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// A line cut in half by the stop itself: the rest is still good.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const lastActivity = events.at(-1)?.at;
|
|
34
|
+
let replyAt = -1;
|
|
35
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
36
|
+
if (events[i].type === 'reply-parsed') {
|
|
37
|
+
replyAt = i;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (replyAt < 0)
|
|
42
|
+
return { lastActivity, iteration: 0, steps: [], resultsSent: false };
|
|
43
|
+
const reply = events[replyAt];
|
|
44
|
+
const iteration = Number(reply.iteration) || 0;
|
|
45
|
+
const after = events.slice(replyAt + 1);
|
|
46
|
+
const count = Number(reply.steps) || 0;
|
|
47
|
+
const steps = [];
|
|
48
|
+
for (let id = 1; id <= count; id += 1) {
|
|
49
|
+
const finished = after.find((e) => e.type === 'step-finished' && e.id === id);
|
|
50
|
+
const skipped = after.find((e) => e.type === 'step-skipped' && e.id === id);
|
|
51
|
+
const started = after.some((e) => e.type === 'step-started' && e.id === id);
|
|
52
|
+
const proposed = after.find((e) => e.type === 'step-proposed' && e.id === id);
|
|
53
|
+
const state = finished ? 'finished' : skipped ? 'skipped' : started ? 'cut' : 'not-run';
|
|
54
|
+
const step = { id, state };
|
|
55
|
+
if (typeof proposed?.description === 'string')
|
|
56
|
+
step.command = proposed.description;
|
|
57
|
+
if (finished) {
|
|
58
|
+
step.outcome = String(finished.outcome ?? '');
|
|
59
|
+
if (typeof finished.exitCode === 'number')
|
|
60
|
+
step.exitCode = finished.exitCode;
|
|
61
|
+
}
|
|
62
|
+
if (state === 'finished' || state === 'cut') {
|
|
63
|
+
const log = await readFile(join(runDir, 'steps', `${iteration}-${id}.log`), 'utf8').catch(() => '');
|
|
64
|
+
const output = parseStepLog(log).output;
|
|
65
|
+
if (output)
|
|
66
|
+
step.outputTail = output.length > TAIL_CHARS ? `…${output.slice(-TAIL_CHARS)}` : output;
|
|
67
|
+
}
|
|
68
|
+
steps.push(step);
|
|
69
|
+
}
|
|
70
|
+
// Sent back only when the results were written *and* a message went out after them.
|
|
71
|
+
const reportAt = after.findIndex((e) => e.type === 'report-written' && Number(e.iteration) === iteration);
|
|
72
|
+
const resultsSent = reportAt >= 0 && after.slice(reportAt + 1).some((e) => e.type === 'message-sent');
|
|
73
|
+
return { lastActivity, iteration, replyStatus: typeof reply.status === 'string' ? reply.status : undefined, steps, resultsSent };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* What the chat is told about it, in the continuation message: which steps ran, with the end of
|
|
77
|
+
* their output, which was cut off, which never ran — or that the results were already sent.
|
|
78
|
+
*/
|
|
79
|
+
export function describeInterruption(i) {
|
|
80
|
+
if (i.iteration === 0)
|
|
81
|
+
return 'It stopped before your first reply arrived, so nothing of this task had been run yet.';
|
|
82
|
+
if (i.resultsSent) {
|
|
83
|
+
return `The results of the steps from your last reply were already sent to you (above), and your answer to them was not received. Carry on from those results.`;
|
|
84
|
+
}
|
|
85
|
+
if (i.replyStatus === 'done' && i.steps.length === 0) {
|
|
86
|
+
return 'Your last reply reported the task done; the runner was checking it when it stopped. If nothing is left, reply again with your summary.';
|
|
87
|
+
}
|
|
88
|
+
if (i.steps.length === 0)
|
|
89
|
+
return 'Your last reply asked for no steps.';
|
|
90
|
+
const lines = [`Of the ${i.steps.length} step(s) in your last reply:`];
|
|
91
|
+
for (const s of i.steps) {
|
|
92
|
+
const what = s.state === 'finished'
|
|
93
|
+
? `ran to the end (exit ${s.exitCode ?? '?'})`
|
|
94
|
+
: s.state === 'cut'
|
|
95
|
+
? 'was running when the runner stopped — it may have done only part of its work; check its effect before relying on it'
|
|
96
|
+
: s.state === 'skipped'
|
|
97
|
+
? 'was not run (refused or skipped)'
|
|
98
|
+
: 'did not run';
|
|
99
|
+
lines.push(`- step ${s.id} ${what}.`);
|
|
100
|
+
if (s.outputTail)
|
|
101
|
+
lines.push(' Its output ended with:', ' ```', ...s.outputTail.split('\n').map((l) => ` ${l}`), ' ```');
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|