copilot-operator 0.1.42 → 0.1.43
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 +131 -0
- package/dist/src/api/operator.service.js +335 -3
- package/dist/src/api/server.js +16 -0
- package/dist/src/cli.js +12 -0
- package/dist/src/config/schema.js +10 -0
- package/dist/src/orchestrator/taskRunner.js +109 -38
- package/dist/src/plan/brief.js +57 -20
- package/dist/src/plan/changes.js +217 -0
- package/dist/src/plan/schema.js +1 -1
- package/dist/src/plan/systemGuide.js +597 -12
- package/dist/src/session/compose.js +15 -1
- package/dist/src/session/model.js +1 -1
- package/dist/src/session/store.js +60 -0
- package/dist/src/vcs/taskVcs.js +110 -1
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/WmzBz4QN36pcz4PL_cfEW/_buildManifest.js +1 -0
- package/dist/web/_next/static/chunks/499-751e0be9d4f86e2d.js +1 -0
- package/dist/web/_next/static/chunks/549-2415dd26f4e53d07.js +1 -0
- package/dist/web/_next/static/chunks/699-9d031f8c145a0465.js +1 -0
- package/dist/web/_next/static/chunks/867-aea30f560bc18eaf.js +1 -0
- package/dist/web/_next/static/chunks/897-5a34109533c3da50.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-56134fbb8cc7554f.js +1 -0
- package/dist/web/_next/static/chunks/app/history/{page-47973539ceff81d6.js → page-0bb537407fed5944.js} +1 -1
- package/dist/web/_next/static/chunks/app/import/{page-999d6cd958989723.js → page-8ef75fc08638e75f.js} +1 -1
- package/dist/web/_next/static/chunks/app/layout-dc602b727480a653.js +1 -0
- package/dist/web/_next/static/chunks/app/page-d73a8ff300862cd8.js +1 -0
- package/dist/web/_next/static/chunks/app/{page-e557d58476ae6383.js → sessions/page-28376d2e4a9b4738.js} +1 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-afc235a9171535d8.js +1 -0
- package/dist/web/_next/static/css/{0e55721108c300f2.css → 72c85e4322c21223.css} +1 -1
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +25 -28
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +25 -28
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +25 -28
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +25 -28
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +25 -28
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +25 -28
- package/dist/web/presets.html +3 -3
- package/dist/web/presets.txt +25 -28
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +25 -28
- package/dist/web/sessions.html +16 -0
- package/dist/web/sessions.txt +26 -0
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +25 -28
- package/package.json +4 -2
- package/dist/web/_next/static/chunks/204-db82ca47ff211e21.js +0 -1
- package/dist/web/_next/static/chunks/499-39b2c00bda3656f0.js +0 -1
- package/dist/web/_next/static/chunks/549-348b0a1404407e5a.js +0 -1
- package/dist/web/_next/static/chunks/867-7d86a692260006f1.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-a25c40436fc312db.js +0 -1
- package/dist/web/_next/static/chunks/app/layout-6ee612d25e78b246.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-872d8b9fe8eedbbf.js +0 -1
- package/dist/web/_next/static/lDmqS7pWU_B_Ac5idPvdS/_buildManifest.js +0 -1
- /package/dist/web/_next/static/{lDmqS7pWU_B_Ac5idPvdS → WmzBz4QN36pcz4PL_cfEW}/_ssgManifest.js +0 -0
|
@@ -188,6 +188,16 @@ let OperatorController = class OperatorController {
|
|
|
188
188
|
* A body with `title`, `prompt` or `level2` edits the task on the way: the attempt that
|
|
189
189
|
* already ran keeps the text it ran with, and the next one uses the new text.
|
|
190
190
|
*/
|
|
191
|
+
/** A note to the chat about this task: sent with its next message, at once while it runs. */
|
|
192
|
+
addTaskNote(id, taskId, body = {}) {
|
|
193
|
+
if (typeof body?.text !== 'string' || !body.text.trim())
|
|
194
|
+
throw new BadRequestException('text is required');
|
|
195
|
+
return this.ops.addTaskNote(id, taskId, body.text).catch(fail);
|
|
196
|
+
}
|
|
197
|
+
/** The operator accepts a task as done that did not end so. */
|
|
198
|
+
acceptTask(id, taskId, body = {}) {
|
|
199
|
+
return this.ops.acceptTask(id, taskId, typeof body?.note === 'string' ? body.note : undefined).catch(fail);
|
|
200
|
+
}
|
|
191
201
|
/** A task that stopped at the runner's limit carries on in its own conversation and branch. */
|
|
192
202
|
continueTask(id, taskId, body = {}) {
|
|
193
203
|
// The operator's new prompt, when one is given: see `TaskContinuation.instruction`.
|
|
@@ -216,6 +226,19 @@ let OperatorController = class OperatorController {
|
|
|
216
226
|
restore(id, taskId) {
|
|
217
227
|
return this.ops.restore(id, taskId).catch(fail);
|
|
218
228
|
}
|
|
229
|
+
/** What undoing this task would do, both ways: only this task, or back to before it. */
|
|
230
|
+
undoPreview(id, taskId) {
|
|
231
|
+
return this.ops.undoPreview(id, taskId).catch(fail);
|
|
232
|
+
}
|
|
233
|
+
/** Does it: `this` takes the task's work out (queued again); `back` goes back to before it (it and the later ones queued). */
|
|
234
|
+
async undoTask(id, taskId, body) {
|
|
235
|
+
if (body?.how !== 'this' && body?.how !== 'back')
|
|
236
|
+
throw new BadRequestException('how must be "this" or "back"');
|
|
237
|
+
const r = await this.ops.undoTask(id, taskId, body.how).catch(fail);
|
|
238
|
+
if (!r.ok)
|
|
239
|
+
throw new BadRequestException(r.problem);
|
|
240
|
+
return r;
|
|
241
|
+
}
|
|
219
242
|
/** What starting the run again from this task would re-queue, and do to the code. */
|
|
220
243
|
restartPlan(id, taskId) {
|
|
221
244
|
return this.ops.restartPlan(id, taskId).catch(fail);
|
|
@@ -501,6 +524,33 @@ let OperatorController = class OperatorController {
|
|
|
501
524
|
resetContext(kind, lang) {
|
|
502
525
|
return this.ops.resetContext(asContextKind(kind), lang ?? 'en');
|
|
503
526
|
}
|
|
527
|
+
/** One folder of the session's repository at its branch (or the task's commit): read-only, for the workbench. */
|
|
528
|
+
repoTree(id, path, task) {
|
|
529
|
+
return this.ops.repoTree(id, path ?? '', task?.trim() || undefined).catch(fail);
|
|
530
|
+
}
|
|
531
|
+
/** One file of it, as text. */
|
|
532
|
+
repoFile(id, path, task) {
|
|
533
|
+
if (!path)
|
|
534
|
+
throw new BadRequestException('path is required');
|
|
535
|
+
return this.ops.repoFile(id, path, task?.trim() || undefined).catch(fail);
|
|
536
|
+
}
|
|
537
|
+
/** The commits on the session's line, each with the task that made it. */
|
|
538
|
+
repoLog(id) {
|
|
539
|
+
return this.ops.repoLog(id).catch(fail);
|
|
540
|
+
}
|
|
541
|
+
/** What happened in these sessions, for Kerrigan, with the operator's note and the form of her answer. */
|
|
542
|
+
kerriganReport(body = {}) {
|
|
543
|
+
const ids = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []);
|
|
544
|
+
return this.ops.kerriganReport({ sessionIds: ids(body?.sessionIds), taskIds: ids(body?.taskIds), note: typeof body?.note === 'string' ? body.note : undefined }).catch(fail);
|
|
545
|
+
}
|
|
546
|
+
/** Kerrigan's changes to existing work, read against it as it is now; nothing is changed. */
|
|
547
|
+
checkChanges(body = {}) {
|
|
548
|
+
return this.ops.checkChanges(typeof body?.text === 'string' ? body.text : '').catch(fail);
|
|
549
|
+
}
|
|
550
|
+
/** Applies them one by one, stopping at the first refused; nothing is started. */
|
|
551
|
+
applyChanges(body = {}) {
|
|
552
|
+
return this.ops.applyChanges(typeof body?.text === 'string' ? body.text : '').catch(fail);
|
|
553
|
+
}
|
|
504
554
|
/** Checks a pasted plan without importing anything, and says what it would duplicate. */
|
|
505
555
|
checkPlan(body) {
|
|
506
556
|
return this.ops.checkPlan(body?.text ?? '').catch(fail);
|
|
@@ -807,6 +857,24 @@ __decorate([
|
|
|
807
857
|
__metadata("design:paramtypes", [String, String]),
|
|
808
858
|
__metadata("design:returntype", Promise)
|
|
809
859
|
], OperatorController.prototype, "deleteTask", null);
|
|
860
|
+
__decorate([
|
|
861
|
+
Post('sessions/:id/tasks/:taskId/notes'),
|
|
862
|
+
__param(0, Param('id')),
|
|
863
|
+
__param(1, Param('taskId')),
|
|
864
|
+
__param(2, Body()),
|
|
865
|
+
__metadata("design:type", Function),
|
|
866
|
+
__metadata("design:paramtypes", [String, String, Object]),
|
|
867
|
+
__metadata("design:returntype", Promise)
|
|
868
|
+
], OperatorController.prototype, "addTaskNote", null);
|
|
869
|
+
__decorate([
|
|
870
|
+
Post('sessions/:id/tasks/:taskId/accept'),
|
|
871
|
+
__param(0, Param('id')),
|
|
872
|
+
__param(1, Param('taskId')),
|
|
873
|
+
__param(2, Body()),
|
|
874
|
+
__metadata("design:type", Function),
|
|
875
|
+
__metadata("design:paramtypes", [String, String, Object]),
|
|
876
|
+
__metadata("design:returntype", Promise)
|
|
877
|
+
], OperatorController.prototype, "acceptTask", null);
|
|
810
878
|
__decorate([
|
|
811
879
|
Post('sessions/:id/tasks/:taskId/continue'),
|
|
812
880
|
__param(0, Param('id')),
|
|
@@ -858,6 +926,23 @@ __decorate([
|
|
|
858
926
|
__metadata("design:paramtypes", [String, String]),
|
|
859
927
|
__metadata("design:returntype", Promise)
|
|
860
928
|
], OperatorController.prototype, "restore", null);
|
|
929
|
+
__decorate([
|
|
930
|
+
Get('sessions/:id/tasks/:taskId/undo'),
|
|
931
|
+
__param(0, Param('id')),
|
|
932
|
+
__param(1, Param('taskId')),
|
|
933
|
+
__metadata("design:type", Function),
|
|
934
|
+
__metadata("design:paramtypes", [String, String]),
|
|
935
|
+
__metadata("design:returntype", Promise)
|
|
936
|
+
], OperatorController.prototype, "undoPreview", null);
|
|
937
|
+
__decorate([
|
|
938
|
+
Post('sessions/:id/tasks/:taskId/undo'),
|
|
939
|
+
__param(0, Param('id')),
|
|
940
|
+
__param(1, Param('taskId')),
|
|
941
|
+
__param(2, Body()),
|
|
942
|
+
__metadata("design:type", Function),
|
|
943
|
+
__metadata("design:paramtypes", [String, String, Object]),
|
|
944
|
+
__metadata("design:returntype", Promise)
|
|
945
|
+
], OperatorController.prototype, "undoTask", null);
|
|
861
946
|
__decorate([
|
|
862
947
|
Get('sessions/:id/tasks/:taskId/restart'),
|
|
863
948
|
__param(0, Param('id')),
|
|
@@ -1111,6 +1196,52 @@ __decorate([
|
|
|
1111
1196
|
__metadata("design:paramtypes", [String, String]),
|
|
1112
1197
|
__metadata("design:returntype", Promise)
|
|
1113
1198
|
], OperatorController.prototype, "resetContext", null);
|
|
1199
|
+
__decorate([
|
|
1200
|
+
Get('sessions/:id/repo/tree'),
|
|
1201
|
+
__param(0, Param('id')),
|
|
1202
|
+
__param(1, Query('path')),
|
|
1203
|
+
__param(2, Query('task')),
|
|
1204
|
+
__metadata("design:type", Function),
|
|
1205
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
1206
|
+
__metadata("design:returntype", Promise)
|
|
1207
|
+
], OperatorController.prototype, "repoTree", null);
|
|
1208
|
+
__decorate([
|
|
1209
|
+
Get('sessions/:id/repo/file'),
|
|
1210
|
+
__param(0, Param('id')),
|
|
1211
|
+
__param(1, Query('path')),
|
|
1212
|
+
__param(2, Query('task')),
|
|
1213
|
+
__metadata("design:type", Function),
|
|
1214
|
+
__metadata("design:paramtypes", [String, String, String]),
|
|
1215
|
+
__metadata("design:returntype", Promise)
|
|
1216
|
+
], OperatorController.prototype, "repoFile", null);
|
|
1217
|
+
__decorate([
|
|
1218
|
+
Get('sessions/:id/repo/log'),
|
|
1219
|
+
__param(0, Param('id')),
|
|
1220
|
+
__metadata("design:type", Function),
|
|
1221
|
+
__metadata("design:paramtypes", [String]),
|
|
1222
|
+
__metadata("design:returntype", Promise)
|
|
1223
|
+
], OperatorController.prototype, "repoLog", null);
|
|
1224
|
+
__decorate([
|
|
1225
|
+
Post('kerrigan/report'),
|
|
1226
|
+
__param(0, Body()),
|
|
1227
|
+
__metadata("design:type", Function),
|
|
1228
|
+
__metadata("design:paramtypes", [Object]),
|
|
1229
|
+
__metadata("design:returntype", Promise)
|
|
1230
|
+
], OperatorController.prototype, "kerriganReport", null);
|
|
1231
|
+
__decorate([
|
|
1232
|
+
Post('changes/check'),
|
|
1233
|
+
__param(0, Body()),
|
|
1234
|
+
__metadata("design:type", Function),
|
|
1235
|
+
__metadata("design:paramtypes", [Object]),
|
|
1236
|
+
__metadata("design:returntype", Promise)
|
|
1237
|
+
], OperatorController.prototype, "checkChanges", null);
|
|
1238
|
+
__decorate([
|
|
1239
|
+
Post('changes/apply'),
|
|
1240
|
+
__param(0, Body()),
|
|
1241
|
+
__metadata("design:type", Function),
|
|
1242
|
+
__metadata("design:paramtypes", [Object]),
|
|
1243
|
+
__metadata("design:returntype", Promise)
|
|
1244
|
+
], OperatorController.prototype, "applyChanges", null);
|
|
1114
1245
|
__decorate([
|
|
1115
1246
|
Post('plan/check'),
|
|
1116
1247
|
__param(0, Body()),
|
|
@@ -45,10 +45,11 @@ function sameFolder(a, b) {
|
|
|
45
45
|
const norm = (p) => p.trim().replace(/[\\/]+$/, '').replace(/\//g, '\\').toLowerCase();
|
|
46
46
|
return a.trim() !== '' && norm(a) === norm(b);
|
|
47
47
|
}
|
|
48
|
-
import { importPlan, plannedSessionSignature, taskSignature } from '../plan/importPlan.js';
|
|
48
|
+
import { composeLevel2, composePrompt, importPlan, personaBlock, plannedSessionSignature, taskSignature } from '../plan/importPlan.js';
|
|
49
|
+
import { checkChanges, kerriganReport } from '../plan/changes.js';
|
|
49
50
|
import { readInterruption } from '../session/interruption.js';
|
|
50
|
-
import { baseRefOf, branchAt, commitInterrupted, nextAttemptBranch, repoDirOf, trackedRepoOf, vcsPreflight, restorePoint, restorePreview, restoreToBase, sessionBranches, sessionBranchName } from '../vcs/taskVcs.js';
|
|
51
|
-
import { branchExists, changedFilesBetween, commitsBetween, fileAt, freeBranchName, git, gitAvailable, localBranches, plannedBranchName, repoUnusableReason } from '../vcs/git.js';
|
|
51
|
+
import { baseRefOf, branchAt, commitInterrupted, nextAttemptBranch, repoDirOf, trackedRepoOf, vcsPreflight, restorePoint, restorePreview, restoreToBase, revertTaskWork, undoThisPreview, sessionBranches, sessionBranchName } from '../vcs/taskVcs.js';
|
|
52
|
+
import { branchExists, changedFilesBetween, commitsBetween, fileAt, freeBranchName, git, gitAvailable, localBranches, looksLikeRepo, plannedBranchName, repoUnusableReason } from '../vcs/git.js';
|
|
52
53
|
import { makeAuthorizer } from '../exec/authorizer.js';
|
|
53
54
|
import { pickFolder } from './folderPicker.js';
|
|
54
55
|
import { findEdgeUsingProfile } from '../transport/profileLock.js';
|
|
@@ -80,6 +81,22 @@ import { saveAndReveal } from './saveToDesktop.js';
|
|
|
80
81
|
import { settingsOf, SettingsUnusableError } from './settings.js';
|
|
81
82
|
import { PolicyLockUnusableError } from '../config/lockedPolicy.js';
|
|
82
83
|
/** Why a session set aside on the Sessions page does not run: one sentence for every start that refuses it. */
|
|
84
|
+
/** The id of the task a change names by id, number or title; see `plan/changes.ts`. */
|
|
85
|
+
function checkTaskRef(session, ref) {
|
|
86
|
+
if (typeof ref === 'number' || /^\d+$/.test(String(ref)))
|
|
87
|
+
return session.tasks[Number(ref) - 1]?.id;
|
|
88
|
+
return (session.tasks.find((t) => t.id === ref) ?? session.tasks.find((t) => t.title === ref) ?? session.tasks.find((t) => t.title.toLowerCase() === String(ref).toLowerCase()))?.id;
|
|
89
|
+
}
|
|
90
|
+
/** Why this task cannot be undone now, or null: only a task that ran and is not running or queued. */
|
|
91
|
+
function undoRefusalOf(task) {
|
|
92
|
+
if (task.status === 'queued')
|
|
93
|
+
return 'This task is queued and has not run since; there is nothing of it to undo.';
|
|
94
|
+
if (task.status === 'running' || task.status === 'waiting-approval')
|
|
95
|
+
return 'This task is running. Stop it first.';
|
|
96
|
+
if (!task.startedAt)
|
|
97
|
+
return 'This task has not run; there is nothing of it to undo.';
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
83
100
|
const INACTIVE_REASON = 'the session is inactive; make it active on the Sessions page to run it';
|
|
84
101
|
/**
|
|
85
102
|
* Why the browser cannot be had now, said in terms of what was pressed. The sentences a start, a
|
|
@@ -572,6 +589,34 @@ let OperatorService = class OperatorService {
|
|
|
572
589
|
await this.init();
|
|
573
590
|
await this.store.deleteTask(sessionId, taskId);
|
|
574
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* A note to the chat about a task (2026-10-14): queued, running or waiting, it goes out with the task's
|
|
594
|
+
* next message — at once, in effect, for a task that is running. See `Task.notes`.
|
|
595
|
+
*/
|
|
596
|
+
async addTaskNote(sessionId, taskId, text) {
|
|
597
|
+
await this.init();
|
|
598
|
+
const task = await this.store.addTaskNote(sessionId, taskId, text);
|
|
599
|
+
const running = this.running.has(sessionId) && ['running', 'waiting-approval'].includes(task.status);
|
|
600
|
+
this.bus.publish({ sessionId, taskId, type: 'task-note-added', level: 'info',
|
|
601
|
+
message: running ? 'a note from the operator; it goes to the chat with the next report' : 'a note from the operator; it goes to the chat when the task runs next',
|
|
602
|
+
data: { length: text.trim().length, running } });
|
|
603
|
+
return task;
|
|
604
|
+
}
|
|
605
|
+
/** The operator accepts a task as done that did not end so (2026-10-14). */
|
|
606
|
+
async acceptTask(sessionId, taskId, note) {
|
|
607
|
+
await this.init();
|
|
608
|
+
if (this.running.has(sessionId)) {
|
|
609
|
+
const now = (await this.store.getSession(sessionId))?.tasks.find((t) => t.id === taskId);
|
|
610
|
+
if (now && ['running', 'waiting-approval'].includes(now.status))
|
|
611
|
+
throw new Error('This task is running. Stop it first.');
|
|
612
|
+
}
|
|
613
|
+
const task = await this.store.acceptTask(sessionId, taskId, note);
|
|
614
|
+
const message = `"${task.title}" accepted as done by the operator (it had ended ${task.accepted?.was})${task.accepted?.note ? `: ${task.accepted.note}` : ''}`;
|
|
615
|
+
this.bus.publish({ sessionId, taskId, type: 'task-accepted', level: 'info', message, data: { was: task.accepted?.was } });
|
|
616
|
+
const cfg = await this.settings.load();
|
|
617
|
+
await appendSessionLog(cfg.resolved.runsDir, sessionId, { type: 'accept', message, data: { taskId, was: task.accepted?.was } }).catch(() => undefined);
|
|
618
|
+
return task;
|
|
619
|
+
}
|
|
575
620
|
/**
|
|
576
621
|
* Puts a finished task back in the queue, keeping the record of what it already did.
|
|
577
622
|
*
|
|
@@ -2194,6 +2239,194 @@ let OperatorService = class OperatorService {
|
|
|
2194
2239
|
duplicates: check.duplicates,
|
|
2195
2240
|
};
|
|
2196
2241
|
}
|
|
2242
|
+
// --- the repository, read-only, for the workbench (2026-10-14) ------------------------------
|
|
2243
|
+
/**
|
|
2244
|
+
* Where a session's work is in its repository: the session's branch in per-session mode, the task's own
|
|
2245
|
+
* branch when one is named, else what is checked out. Read-only; nothing here moves anything.
|
|
2246
|
+
*/
|
|
2247
|
+
async repoRefOf(sessionId, taskId) {
|
|
2248
|
+
await this.init();
|
|
2249
|
+
const session = await this.store.getSession(sessionId);
|
|
2250
|
+
if (!session)
|
|
2251
|
+
throw new Error('No such session.');
|
|
2252
|
+
const dir = repoDirOf(session);
|
|
2253
|
+
if (!dir || !looksLikeRepo(dir))
|
|
2254
|
+
throw new Error('This session has no repository to show.');
|
|
2255
|
+
const task = taskId ? session.tasks.find((t) => t.id === taskId) : undefined;
|
|
2256
|
+
const candidates = [
|
|
2257
|
+
...(task?.vcs?.commit ? [task.vcs.commit] : []),
|
|
2258
|
+
...(session.vcs?.enabled && session.vcs.branchMode === 'per-session' ? [sessionBranchName(session)] : []),
|
|
2259
|
+
'HEAD',
|
|
2260
|
+
];
|
|
2261
|
+
for (const ref of candidates) {
|
|
2262
|
+
const r = await git(dir, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
|
|
2263
|
+
if (r.ok && r.stdout)
|
|
2264
|
+
return { dir, ref: r.stdout, label: ref === 'HEAD' ? 'HEAD' : ref.length >= 40 ? ref.slice(0, 8) : ref };
|
|
2265
|
+
}
|
|
2266
|
+
throw new Error('The repository has no commit to show yet.');
|
|
2267
|
+
}
|
|
2268
|
+
/** One folder of the repository at that point: its folders and files, folders first. */
|
|
2269
|
+
async repoTree(sessionId, path = '', taskId) {
|
|
2270
|
+
const clean = path.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
2271
|
+
if (clean.split('/').some((p) => p === '..'))
|
|
2272
|
+
throw new Error('That path is not inside the repository.');
|
|
2273
|
+
const { dir, ref, label } = await this.repoRefOf(sessionId, taskId);
|
|
2274
|
+
const r = await git(dir, ['ls-tree', '-l', clean ? `${ref}:${clean}` : ref]);
|
|
2275
|
+
if (!r.ok)
|
|
2276
|
+
throw new Error(clean ? `There is no folder ${clean} there.` : 'The repository could not be read.');
|
|
2277
|
+
const entries = r.stdout
|
|
2278
|
+
.split('\n')
|
|
2279
|
+
.filter(Boolean)
|
|
2280
|
+
.map((line) => {
|
|
2281
|
+
const [meta, name] = line.split('\t');
|
|
2282
|
+
const [, type, , size] = (meta ?? '').split(/\s+/);
|
|
2283
|
+
return { name: name ?? '', path: clean ? `${clean}/${name}` : (name ?? ''), type: type === 'tree' ? 'dir' : 'file', ...(type === 'blob' && size && size !== '-' ? { size: Number(size) } : {}) };
|
|
2284
|
+
})
|
|
2285
|
+
.filter((e) => e.name)
|
|
2286
|
+
.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === 'dir' ? -1 : 1));
|
|
2287
|
+
return { ref, label, path: clean, entries };
|
|
2288
|
+
}
|
|
2289
|
+
/** One file of the repository at that point, as text; a binary or very large file is said to be one. */
|
|
2290
|
+
async repoFile(sessionId, path, taskId) {
|
|
2291
|
+
const clean = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
2292
|
+
if (!clean || clean.split('/').some((p) => p === '..'))
|
|
2293
|
+
throw new Error('That path is not inside the repository.');
|
|
2294
|
+
const { dir, ref, label } = await this.repoRefOf(sessionId, taskId);
|
|
2295
|
+
const got = await fileAt(dir, ref, clean, 2 * 1024 * 1024);
|
|
2296
|
+
if (!got.bytes && !got.tooLarge)
|
|
2297
|
+
throw new Error(`There is no file ${clean} there.`);
|
|
2298
|
+
const binary = !!got.bytes && got.bytes.subarray(0, 8000).includes(0);
|
|
2299
|
+
return { ref, label, path: clean, text: got.bytes && !binary ? got.bytes.toString('utf8').replace(/^/, '') : null, binary, tooLarge: got.tooLarge };
|
|
2300
|
+
}
|
|
2301
|
+
/** The commits on the session's line, newest first, each with the task that made it when one did. */
|
|
2302
|
+
async repoLog(sessionId, limit = 60) {
|
|
2303
|
+
const { dir, ref, label } = await this.repoRefOf(sessionId);
|
|
2304
|
+
const session = (await this.store.getSession(sessionId));
|
|
2305
|
+
const r = await git(dir, ['log', `-${Math.max(1, Math.min(limit, 300))}`, '--format=%H%x09%an%x09%aI%x09%s', ref]);
|
|
2306
|
+
const byCommit = new Map();
|
|
2307
|
+
for (const t of session.tasks) {
|
|
2308
|
+
for (const v of [t.vcs, ...(t.attempts ?? []).map((a) => a.vcs)])
|
|
2309
|
+
if (v?.commit)
|
|
2310
|
+
byCommit.set(v.commit, t);
|
|
2311
|
+
}
|
|
2312
|
+
const commits = (r.ok ? r.stdout.split('\n').filter(Boolean) : []).map((line) => {
|
|
2313
|
+
const [sha = '', author = '', at = '', subject = ''] = line.split('\t');
|
|
2314
|
+
const task = byCommit.get(sha);
|
|
2315
|
+
return { sha, subject, author, at, ...(task ? { taskId: task.id, taskTitle: task.title } : {}) };
|
|
2316
|
+
});
|
|
2317
|
+
return { ref, label, commits };
|
|
2318
|
+
}
|
|
2319
|
+
// --- Kerrigan: what happened, and her changes to existing work (2026-10-14) ---------------------
|
|
2320
|
+
/** What happened in these sessions (all, when none are named), for Kerrigan, with the operator's note. */
|
|
2321
|
+
async kerriganReport(opts = {}) {
|
|
2322
|
+
await this.init();
|
|
2323
|
+
const all = await this.store.listSessions();
|
|
2324
|
+
const wanted = (opts.sessionIds ?? []).filter(Boolean);
|
|
2325
|
+
const taskIds = (opts.taskIds ?? []).filter(Boolean);
|
|
2326
|
+
const sessions = all.filter((s) => (wanted.length > 0 ? wanted.includes(s.id) : taskIds.length > 0 ? s.tasks.some((t) => taskIds.includes(t.id)) : true));
|
|
2327
|
+
if (sessions.length === 0)
|
|
2328
|
+
throw new Error('None of these sessions exists.');
|
|
2329
|
+
if ((opts.note ?? '').length > MAX_INSTRUCTION_CHARS)
|
|
2330
|
+
throw new Error(`The note is ${(opts.note ?? '').length} characters; it can be at most ${MAX_INSTRUCTION_CHARS}.`);
|
|
2331
|
+
return { text: kerriganReport(sessions, { note: opts.note, taskIds }), sessions: sessions.length };
|
|
2332
|
+
}
|
|
2333
|
+
/** Kerrigan's answer read against the sessions as they are now; nothing is changed. */
|
|
2334
|
+
async checkChanges(text) {
|
|
2335
|
+
await this.init();
|
|
2336
|
+
return checkChanges(text ?? '', await this.store.listSessions());
|
|
2337
|
+
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Applies Kerrigan's changes one by one, through the paths the buttons use, and stops at the first that is
|
|
2340
|
+
* refused, saying which were done. Nothing is started.
|
|
2341
|
+
*/
|
|
2342
|
+
async applyChanges(text) {
|
|
2343
|
+
const check = await this.checkChanges(text);
|
|
2344
|
+
if (!check.ok)
|
|
2345
|
+
return { ok: false, check, applied: [] };
|
|
2346
|
+
const cfg = await this.settings.load();
|
|
2347
|
+
const persona = (await this.getContext('persona', 'en')).content;
|
|
2348
|
+
const applied = [];
|
|
2349
|
+
for (const r of check.changes) {
|
|
2350
|
+
const c = r.change;
|
|
2351
|
+
try {
|
|
2352
|
+
switch (c.op) {
|
|
2353
|
+
case 'addTask': {
|
|
2354
|
+
const session = await this.store.getSession(r.sessionId);
|
|
2355
|
+
if (!session)
|
|
2356
|
+
throw new Error('The session is gone.');
|
|
2357
|
+
const afterId = c.after !== undefined ? checkTaskRef(session, c.after) : undefined;
|
|
2358
|
+
const reference = afterId ? session.tasks.find((t) => t.id === afterId) : session.tasks.at(-1);
|
|
2359
|
+
const level2 = c.task.level2.trim()
|
|
2360
|
+
? composeLevel2({ level2: '', goal: '' }, c.task, persona)
|
|
2361
|
+
: (reference?.level2 ?? personaBlock(persona));
|
|
2362
|
+
const added = await this.store.addTask(r.sessionId, {
|
|
2363
|
+
title: c.task.title,
|
|
2364
|
+
level2,
|
|
2365
|
+
prompt: composePrompt(c.task),
|
|
2366
|
+
vcsPlan: c.task.vcs,
|
|
2367
|
+
checks: c.task.checks,
|
|
2368
|
+
reviewEnabled: c.task.review,
|
|
2369
|
+
readOnly: c.task.readOnly || undefined,
|
|
2370
|
+
scope: c.task.scope.length > 0 ? c.task.scope : undefined,
|
|
2371
|
+
outputs: c.task.outputs.length > 0 ? c.task.outputs : undefined,
|
|
2372
|
+
});
|
|
2373
|
+
if (afterId) {
|
|
2374
|
+
await this.store.updateSession(r.sessionId, (s) => {
|
|
2375
|
+
const from = s.tasks.findIndex((t) => t.id === added.id);
|
|
2376
|
+
const [moved] = s.tasks.splice(from, 1);
|
|
2377
|
+
s.tasks.splice(s.tasks.findIndex((t) => t.id === afterId) + 1, 0, moved);
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
break;
|
|
2381
|
+
}
|
|
2382
|
+
case 'editTask':
|
|
2383
|
+
await this.updateTask(r.sessionId, r.taskId, {
|
|
2384
|
+
...(c.title ? { title: c.title } : {}),
|
|
2385
|
+
...(c.prompt ? { prompt: c.prompt } : {}),
|
|
2386
|
+
...(c.checks ? { checks: c.checks } : {}),
|
|
2387
|
+
...(c.scope ? { scope: c.scope } : {}),
|
|
2388
|
+
});
|
|
2389
|
+
break;
|
|
2390
|
+
case 'removeTask':
|
|
2391
|
+
await this.deleteTask(r.sessionId, r.taskId);
|
|
2392
|
+
break;
|
|
2393
|
+
case 'continue':
|
|
2394
|
+
await this.continueTask(r.sessionId, r.taskId, c.instruction);
|
|
2395
|
+
break;
|
|
2396
|
+
case 'newPrompt':
|
|
2397
|
+
await this.rerunTask(r.sessionId, r.taskId, { prompt: c.prompt }, { buildOnFinished: true });
|
|
2398
|
+
break;
|
|
2399
|
+
case 'runAgain':
|
|
2400
|
+
await this.rerunTask(r.sessionId, r.taskId, c.prompt ? { prompt: c.prompt } : {});
|
|
2401
|
+
break;
|
|
2402
|
+
case 'undo': {
|
|
2403
|
+
const done = await this.undoTask(r.sessionId, r.taskId, c.how);
|
|
2404
|
+
if (!done.ok)
|
|
2405
|
+
throw new Error(done.problem ?? 'it could not be undone');
|
|
2406
|
+
break;
|
|
2407
|
+
}
|
|
2408
|
+
case 'accept':
|
|
2409
|
+
await this.acceptTask(r.sessionId, r.taskId, c.note);
|
|
2410
|
+
break;
|
|
2411
|
+
case 'note':
|
|
2412
|
+
await this.addTaskNote(r.sessionId, r.taskId, c.text);
|
|
2413
|
+
break;
|
|
2414
|
+
}
|
|
2415
|
+
applied.push({ index: r.index, says: r.says });
|
|
2416
|
+
await appendSessionLog(cfg.resolved.runsDir, r.sessionId, { type: 'kerrigan-change', message: r.says, data: { op: c.op, ...(r.taskId ? { taskId: r.taskId } : {}) } }).catch(() => undefined);
|
|
2417
|
+
this.bus.publish({ sessionId: r.sessionId, ...(r.taskId ? { taskId: r.taskId } : {}), type: 'kerrigan-change', level: 'info', message: `from Kerrigan's answer: ${r.says}` });
|
|
2418
|
+
}
|
|
2419
|
+
catch (e) {
|
|
2420
|
+
return {
|
|
2421
|
+
ok: false,
|
|
2422
|
+
applied,
|
|
2423
|
+
failedAt: r.index,
|
|
2424
|
+
problem: `Change ${r.index + 1} (${r.says}) was refused: ${e.message}${applied.length > 0 ? ` The ${applied.length} before it were made.` : ' Nothing was changed.'}`,
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return { ok: true, applied };
|
|
2429
|
+
}
|
|
2197
2430
|
// --- approvals ------------------------------------------------------------------------
|
|
2198
2431
|
/**
|
|
2199
2432
|
* Asks the operator to choose the model in the Copilot window, when the runner could not (operator's
|
|
@@ -2802,6 +3035,105 @@ let OperatorService = class OperatorService {
|
|
|
2802
3035
|
const refused = await this.restoreRefusal(session);
|
|
2803
3036
|
return refused ? { ...preview, ok: false, problem: refused } : preview;
|
|
2804
3037
|
}
|
|
3038
|
+
/**
|
|
3039
|
+
* What undoing this task would do, both ways (operator's request, 2026-10-14): "Undo only this task" — its
|
|
3040
|
+
* work taken out of the session's line, what came after it kept — and "Go back to before this task" — the
|
|
3041
|
+
* line put back at its start, it and the tasks after it queued again, nothing started. See `undoTask`.
|
|
3042
|
+
*/
|
|
3043
|
+
async undoPreview(sessionId, taskId) {
|
|
3044
|
+
await this.init();
|
|
3045
|
+
const session = await this.store.getSession(sessionId);
|
|
3046
|
+
if (!session)
|
|
3047
|
+
throw new Error('No such session.');
|
|
3048
|
+
const task = session.tasks.find((t) => t.id === taskId);
|
|
3049
|
+
if (!task)
|
|
3050
|
+
throw new Error('No such task.');
|
|
3051
|
+
const notYet = undoRefusalOf(task);
|
|
3052
|
+
const refused = notYet ?? (await this.restoreRefusal(session));
|
|
3053
|
+
const own = await undoThisPreview(session, task);
|
|
3054
|
+
const back = await this.restartPlan(sessionId, taskId);
|
|
3055
|
+
return {
|
|
3056
|
+
this: refused ? { ...own, ok: false, problem: refused } : own,
|
|
3057
|
+
back: refused ? { ...back, ok: false, problem: refused } : back,
|
|
3058
|
+
...(refused ? { problem: refused } : {}),
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Undoes a task that ran. `this`: its work is taken out of the session's line by a revert (per-session),
|
|
3063
|
+
* or the folder is put back at its start (per-task); the task goes back to the queue. `back`: "Run again
|
|
3064
|
+
* from here" without the run — the line goes back to the task's start and it and the tasks after it are
|
|
3065
|
+
* queued. Nothing is started and nothing in git is deleted: the old branch or the reverted commits stay.
|
|
3066
|
+
*/
|
|
3067
|
+
async undoTask(sessionId, taskId, how) {
|
|
3068
|
+
await this.init();
|
|
3069
|
+
const session = await this.store.getSession(sessionId);
|
|
3070
|
+
if (!session)
|
|
3071
|
+
throw new Error('No such session.');
|
|
3072
|
+
const task = session.tasks.find((t) => t.id === taskId);
|
|
3073
|
+
if (!task)
|
|
3074
|
+
throw new Error('No such task.');
|
|
3075
|
+
const notYet = undoRefusalOf(task);
|
|
3076
|
+
if (notYet)
|
|
3077
|
+
return { ok: false, problem: notYet };
|
|
3078
|
+
const refused = await this.restoreRefusal(session);
|
|
3079
|
+
if (refused)
|
|
3080
|
+
return { ok: false, problem: refused };
|
|
3081
|
+
const cfg = await this.settings.load();
|
|
3082
|
+
const at = new Date().toISOString();
|
|
3083
|
+
if (how === 'back') {
|
|
3084
|
+
const plan = await this.restartPlan(sessionId, taskId);
|
|
3085
|
+
const r = await this.restartFrom(sessionId, taskId, { restore: true, start: false });
|
|
3086
|
+
if (r.reason && r.requeued === 0)
|
|
3087
|
+
return { ok: false, problem: r.reason };
|
|
3088
|
+
for (const t of plan.tasks.filter((x) => !x.alreadyQueued)) {
|
|
3089
|
+
await this.store
|
|
3090
|
+
.updateSession(t.sessionId, (s) => {
|
|
3091
|
+
const last = s.tasks.find((x) => x.id === t.taskId)?.attempts?.at(-1);
|
|
3092
|
+
if (last)
|
|
3093
|
+
last.undone = { how: 'back', at, ...(r.restored[0] ? { branch: r.restored[0] } : {}), ...(t.taskId !== taskId ? { byTask: taskId } : {}) };
|
|
3094
|
+
})
|
|
3095
|
+
.catch(() => undefined);
|
|
3096
|
+
}
|
|
3097
|
+
const message = `went back to before "${task.title}": ${r.requeued} task(s) queued again${r.restored.length > 0 ? `, the code on ${r.restored.join(', ')}` : ''}; nothing was started`;
|
|
3098
|
+
await appendSessionLog(cfg.resolved.runsDir, sessionId, { type: 'undo', message, data: { taskId, how, requeued: r.requeued, restored: r.restored } }).catch(() => undefined);
|
|
3099
|
+
this.bus.publish({ sessionId, taskId, type: 'task-undone', level: 'info', message, data: { how, requeued: r.requeued } });
|
|
3100
|
+
return { ok: true, message, requeued: r.requeued };
|
|
3101
|
+
}
|
|
3102
|
+
let done = {};
|
|
3103
|
+
if (session.vcs?.enabled && session.vcs.branchMode === 'per-task') {
|
|
3104
|
+
const preview = await undoThisPreview(session, task);
|
|
3105
|
+
if (!preview.ok)
|
|
3106
|
+
return { ok: false, problem: preview.problem };
|
|
3107
|
+
if (!preview.nothing) {
|
|
3108
|
+
const back = await restoreToBase(session, task, this.bus);
|
|
3109
|
+
if (!back.ok)
|
|
3110
|
+
return { ok: false, problem: back.problem };
|
|
3111
|
+
done = { branch: back.branch };
|
|
3112
|
+
}
|
|
3113
|
+
else
|
|
3114
|
+
done = { nothing: preview.nothing };
|
|
3115
|
+
}
|
|
3116
|
+
else {
|
|
3117
|
+
const r = await revertTaskWork(session, task, this.bus);
|
|
3118
|
+
if (!r.ok)
|
|
3119
|
+
return { ok: false, problem: r.problem };
|
|
3120
|
+
done = { commit: r.commit, branch: r.branch, nothing: r.nothing };
|
|
3121
|
+
}
|
|
3122
|
+
await this.store.rerunTask(sessionId, taskId, {});
|
|
3123
|
+
await this.store.updateSession(sessionId, (s) => {
|
|
3124
|
+
const last = s.tasks.find((x) => x.id === taskId)?.attempts?.at(-1);
|
|
3125
|
+
if (last)
|
|
3126
|
+
last.undone = { how: 'this', at, ...(done.commit ? { commit: done.commit } : {}), ...(done.branch ? { branch: done.branch } : {}) };
|
|
3127
|
+
});
|
|
3128
|
+
const message = done.commit
|
|
3129
|
+
? `took the work of "${task.title}" out of ${done.branch} in ${done.commit.slice(0, 8)}; the task is queued again, nothing was started`
|
|
3130
|
+
: done.branch
|
|
3131
|
+
? `put the folder back as it was before "${task.title}", on ${done.branch}; its own branch is kept, the task is queued again`
|
|
3132
|
+
: `${done.nothing ?? 'nothing in git to take out'} The task is queued again.`;
|
|
3133
|
+
await appendSessionLog(cfg.resolved.runsDir, sessionId, { type: 'undo', message, data: { taskId, how, ...done } }).catch(() => undefined);
|
|
3134
|
+
this.bus.publish({ sessionId, taskId, type: 'task-undone', level: 'info', message, data: { how, ...done } });
|
|
3135
|
+
return { ok: true, message, requeued: 1 };
|
|
3136
|
+
}
|
|
2805
3137
|
async restore(sessionId, taskId) {
|
|
2806
3138
|
await this.init();
|
|
2807
3139
|
const session = await this.store.getSession(sessionId);
|
package/dist/src/api/server.js
CHANGED
|
@@ -108,6 +108,22 @@ export async function startApi(opts = {}) {
|
|
|
108
108
|
const allowedOrigins = [WEB_ORIGIN, WEB_ORIGIN.replace('localhost', '127.0.0.1'), `http://127.0.0.1:${PORT}`, `http://localhost:${PORT}`];
|
|
109
109
|
app.use(localApiGuard({ token, port: PORT, allowedOrigins, servesInterface: webDir !== null }));
|
|
110
110
|
if (webDir) {
|
|
111
|
+
/*
|
|
112
|
+
* A page whose name is also a folder of pages: `/sessions` is `sessions.html`, and `sessions/` holds
|
|
113
|
+
* `view.html` (2026-10-14, when the list moved there). The static server finds the folder first and
|
|
114
|
+
* redirects to it, where there is no page; the page is served instead.
|
|
115
|
+
*/
|
|
116
|
+
const root = webDir;
|
|
117
|
+
app.use((req, res, next) => {
|
|
118
|
+
if ((req.method === 'GET' || req.method === 'HEAD') && /^\/[a-z0-9-]+$/i.test(req.path)) {
|
|
119
|
+
const page = join(root, `${req.path.slice(1)}.html`);
|
|
120
|
+
if (existsSync(page) && existsSync(join(root, req.path.slice(1)))) {
|
|
121
|
+
res.sendFile(page);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
next();
|
|
126
|
+
});
|
|
111
127
|
// The prebuilt interface, from the package: one process, one port, no dev server.
|
|
112
128
|
app.useStaticAssets(webDir, { extensions: ['html'], index: 'index.html' });
|
|
113
129
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
17
17
|
import { stdin, stdout } from 'node:process';
|
|
18
18
|
import { loadConfig, expandPath } from './config/schema.js';
|
|
19
19
|
import { CopilotTransport } from './transport/copilotTransport.js';
|
|
20
|
+
import { parseChatId } from './transport/chatSession.js';
|
|
20
21
|
import { runSession } from './orchestrator/taskRunner.js';
|
|
21
22
|
import { SessionStore } from './session/store.js';
|
|
22
23
|
import { settingsOf, SettingsUnusableError } from './api/settings.js';
|
|
@@ -384,6 +385,7 @@ program
|
|
|
384
385
|
.option('--json', 'print the raw list, for pasting into an issue')
|
|
385
386
|
.option('--set <name>', 'also switch the chat to this model, by its exact name')
|
|
386
387
|
.option('--steps', 'with --set: print each step of the choice with its time, and keep a picture of each in runs/_models')
|
|
388
|
+
.option('--chat <id-or-address>', 'first open this existing conversation (its id, or its address from the browser), as a task carried on does')
|
|
387
389
|
.action(async (opts) => {
|
|
388
390
|
const transport = new CopilotTransport({
|
|
389
391
|
profileDir: opts.profile,
|
|
@@ -400,6 +402,16 @@ program
|
|
|
400
402
|
try {
|
|
401
403
|
await transport.open();
|
|
402
404
|
await transport.ensureSignedIn();
|
|
405
|
+
/*
|
|
406
|
+
* Back in a conversation that already ran, as a failed task carried on is: the case the operator saw
|
|
407
|
+
* the model check go wrong in (2026-10-14). What the picker button reads there is printed first.
|
|
408
|
+
*/
|
|
409
|
+
if (opts.chat) {
|
|
410
|
+
const id = parseChatId(opts.chat) ?? opts.chat.trim();
|
|
411
|
+
const opened = await transport.openConversation(id);
|
|
412
|
+
console.log(opened ? `opened the conversation ${id}` : `the conversation ${id} could not be opened; going on in the page as it is`);
|
|
413
|
+
console.log(`the picker button reads: ${(await transport.currentModel()) ?? '(nothing)'}`);
|
|
414
|
+
}
|
|
403
415
|
const { options, current, note } = await transport.listModels();
|
|
404
416
|
if (opts.json) {
|
|
405
417
|
console.log(JSON.stringify({ current, note, options }, null, 2));
|
|
@@ -381,6 +381,16 @@ export const RunConfigSchema = z.object({
|
|
|
381
381
|
* its record. Zero turns it off.
|
|
382
382
|
*/
|
|
383
383
|
retryBlockedInFreshChat: z.number().int().nonnegative().default(2),
|
|
384
|
+
/**
|
|
385
|
+
* How many times a task a limit stopped is carried on at once, in its own chat and from where it got
|
|
386
|
+
* to, before it waits for the operator's "Continue" (operator's request, 2026-10-14: keep going until
|
|
387
|
+
* the task is done, like Claude Code, with the limits as the bound). Every limit above counts —
|
|
388
|
+
* messages, minutes, the reply wait, replies in the wrong shape, rounds of fixing checks or review
|
|
389
|
+
* findings — so a task gets at most (autoContinue + 1) times each of them. The loop guards (repeats,
|
|
390
|
+
* no progress) do not carry on: they end a task as blocked, which a fresh chat may cure, then a person.
|
|
391
|
+
* Zero turns it off.
|
|
392
|
+
*/
|
|
393
|
+
autoContinue: z.number().int().min(0).max(20).default(3),
|
|
384
394
|
/*
|
|
385
395
|
* How many genuinely different approaches the chat must have tried before it may end a task
|
|
386
396
|
* "blocked". The reply format's own floor is 2 (replySchema.ts); this raises it. A "blocked"
|