copilot-operator 0.1.41 → 0.1.42
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 +28 -6
- package/dist/src/api/operator.service.js +82 -7
- package/dist/src/config/schema.js +7 -0
- package/dist/src/orchestrator/planMode.js +102 -0
- package/dist/src/orchestrator/review.js +8 -4
- package/dist/src/orchestrator/taskRunner.js +137 -2
- package/dist/src/plan/brief.js +12 -0
- package/dist/src/plan/importPlan.js +2 -0
- package/dist/src/plan/schema.js +10 -0
- package/dist/src/plan/systemGuide.js +193 -2
- package/dist/src/protocol/parser.js +26 -2
- package/dist/src/protocol/replySchema.js +28 -4
- package/dist/src/session/compose.js +62 -7
- package/dist/src/session/exports.js +1 -0
- package/dist/src/session/model.js +38 -0
- package/dist/src/session/store.js +30 -3
- package/dist/src/vcs/startChoice.js +80 -8
- package/dist/src/vcs/taskVcs.js +16 -7
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/{584-578be94a0786334b.js → 204-db82ca47ff211e21.js} +1 -1
- package/dist/web/_next/static/chunks/499-39b2c00bda3656f0.js +1 -0
- package/dist/web/_next/static/chunks/549-348b0a1404407e5a.js +1 -0
- package/dist/web/_next/static/chunks/867-7d86a692260006f1.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-a25c40436fc312db.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-47973539ceff81d6.js +1 -0
- package/dist/web/_next/static/chunks/app/layout-6ee612d25e78b246.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-872d8b9fe8eedbbf.js +1 -0
- package/dist/web/_next/static/css/{cea7913ba416dbb7.css → 0e55721108c300f2.css} +1 -1
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +13 -13
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +13 -13
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +13 -13
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +13 -13
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +13 -13
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +13 -13
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +13 -13
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +13 -13
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +13 -13
- package/package.json +159 -158
- package/dist/web/_next/static/chunks/499-28e118e33ce07935.js +0 -1
- package/dist/web/_next/static/chunks/549-f295c8429e83b25b.js +0 -1
- package/dist/web/_next/static/chunks/867-8dc5bbb092ca2c5c.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-879158e9e550eea7.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-9f69ccde726c9be2.js +0 -1
- package/dist/web/_next/static/chunks/app/layout-73f28bc065a46ed3.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-0485ee7f781156ec.js +0 -1
- /package/dist/web/_next/static/{-htN5GTSrd3W3f5j_QC2y → lDmqS7pWU_B_Ac5idPvdS}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{-htN5GTSrd3W3f5j_QC2y → lDmqS7pWU_B_Ac5idPvdS}/_ssgManifest.js +0 -0
|
@@ -69,12 +69,14 @@ function startsFrom(raw) {
|
|
|
69
69
|
const x = c;
|
|
70
70
|
const branch = typeof x.branch === 'string' ? x.branch : '';
|
|
71
71
|
const onBranch = x.onBranch === true ? { onBranch: true } : {};
|
|
72
|
+
// Another start for a session that has run: see `StartChoice.rechoose`.
|
|
73
|
+
const rechoose = x.rechoose === true ? { rechoose: true } : {};
|
|
72
74
|
if (x.from === 'previous-session')
|
|
73
|
-
out[id] = { from: 'previous-session' };
|
|
75
|
+
out[id] = { from: 'previous-session', ...rechoose };
|
|
74
76
|
else if (x.from === 'local' && branch)
|
|
75
|
-
out[id] = { from: 'local', branch, ...onBranch };
|
|
77
|
+
out[id] = { from: 'local', branch, ...onBranch, ...rechoose };
|
|
76
78
|
else if (x.from === 'remote' && branch && typeof x.remote === 'string' && x.remote)
|
|
77
|
-
out[id] = { from: 'remote', remote: x.remote, branch, ...onBranch };
|
|
79
|
+
out[id] = { from: 'remote', remote: x.remote, branch, ...onBranch, ...rechoose };
|
|
78
80
|
}
|
|
79
81
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
80
82
|
}
|
|
@@ -187,8 +189,10 @@ let OperatorController = class OperatorController {
|
|
|
187
189
|
* already ran keeps the text it ran with, and the next one uses the new text.
|
|
188
190
|
*/
|
|
189
191
|
/** A task that stopped at the runner's limit carries on in its own conversation and branch. */
|
|
190
|
-
continueTask(id, taskId) {
|
|
191
|
-
|
|
192
|
+
continueTask(id, taskId, body = {}) {
|
|
193
|
+
// The operator's new prompt, when one is given: see `TaskContinuation.instruction`.
|
|
194
|
+
const instruction = typeof body?.instruction === 'string' ? body.instruction : undefined;
|
|
195
|
+
return this.ops.continueTask(id, taskId, instruction).catch(fail);
|
|
192
196
|
}
|
|
193
197
|
rerunTask(id, taskId, body) {
|
|
194
198
|
const { buildOnFinished, ...patch } = body ?? {};
|
|
@@ -537,6 +541,15 @@ let OperatorController = class OperatorController {
|
|
|
537
541
|
}
|
|
538
542
|
return this.ops.decide(id, body.action);
|
|
539
543
|
}
|
|
544
|
+
/** A plan waiting for approval (plan mode): approve it, with a note or not; ask for another; or stop the task. */
|
|
545
|
+
decidePlan(id, body) {
|
|
546
|
+
if (!['approve', 'revise', 'stop'].includes(body?.action ?? ''))
|
|
547
|
+
throw new BadRequestException('action must be approve, revise or stop');
|
|
548
|
+
const result = this.ops.decidePlan(id, body.action, typeof body?.note === 'string' ? body.note : undefined);
|
|
549
|
+
if (!result.ok)
|
|
550
|
+
throw new BadRequestException(result.problem);
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
540
553
|
/** Turns asking on or off in the middle of a run. */
|
|
541
554
|
setMode(id, body) {
|
|
542
555
|
if (body?.mode !== 'confirm' && body?.mode !== 'unattended') {
|
|
@@ -798,8 +811,9 @@ __decorate([
|
|
|
798
811
|
Post('sessions/:id/tasks/:taskId/continue'),
|
|
799
812
|
__param(0, Param('id')),
|
|
800
813
|
__param(1, Param('taskId')),
|
|
814
|
+
__param(2, Body()),
|
|
801
815
|
__metadata("design:type", Function),
|
|
802
|
-
__metadata("design:paramtypes", [String, String]),
|
|
816
|
+
__metadata("design:paramtypes", [String, String, Object]),
|
|
803
817
|
__metadata("design:returntype", Promise)
|
|
804
818
|
], OperatorController.prototype, "continueTask", null);
|
|
805
819
|
__decorate([
|
|
@@ -1140,6 +1154,14 @@ __decorate([
|
|
|
1140
1154
|
__metadata("design:paramtypes", [String, Object]),
|
|
1141
1155
|
__metadata("design:returntype", Object)
|
|
1142
1156
|
], OperatorController.prototype, "decide", null);
|
|
1157
|
+
__decorate([
|
|
1158
|
+
Post('approvals/:id/plan'),
|
|
1159
|
+
__param(0, Param('id')),
|
|
1160
|
+
__param(1, Body()),
|
|
1161
|
+
__metadata("design:type", Function),
|
|
1162
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
1163
|
+
__metadata("design:returntype", Object)
|
|
1164
|
+
], OperatorController.prototype, "decidePlan", null);
|
|
1143
1165
|
__decorate([
|
|
1144
1166
|
Post('sessions/:id/mode'),
|
|
1145
1167
|
__param(0, Param('id')),
|
|
@@ -20,7 +20,7 @@ import { join, resolve } from 'node:path';
|
|
|
20
20
|
import { installLayout } from '../config/layout.js';
|
|
21
21
|
import { SessionStore, DEFAULT_VCS, DEFAULT_REVIEW, applyDefaultProject, applyTaskPatch } from '../session/store.js';
|
|
22
22
|
import { EventBus } from '../session/events.js';
|
|
23
|
-
import { freshRetriesOfLatestRun, isContinuable, newId } from '../session/model.js';
|
|
23
|
+
import { freshRetriesOfLatestRun, isContinuable, MAX_INSTRUCTION_CHARS, newId } from '../session/model.js';
|
|
24
24
|
import { runSession, openBrowser, queuedToRun } from '../orchestrator/taskRunner.js';
|
|
25
25
|
import { buildExport } from '../session/exportRecord.js';
|
|
26
26
|
import { buildDebugExport } from '../session/debugExport.js';
|
|
@@ -436,6 +436,12 @@ let OperatorService = class OperatorService {
|
|
|
436
436
|
}
|
|
437
437
|
if (patch.projectDir !== undefined)
|
|
438
438
|
s.projectDir = patch.projectDir.trim();
|
|
439
|
+
if (patch.planMode !== undefined) {
|
|
440
|
+
if (patch.planMode === 'off' || patch.planMode === 'auto' || patch.planMode === 'approve')
|
|
441
|
+
s.planMode = patch.planMode;
|
|
442
|
+
else
|
|
443
|
+
delete s.planMode;
|
|
444
|
+
}
|
|
439
445
|
if (patch.vcs) {
|
|
440
446
|
const merged = { ...DEFAULT_VCS, ...s.vcs, ...patch.vcs };
|
|
441
447
|
// A prefix is what makes the bot's branches recognisable in `git branch`, so an empty
|
|
@@ -574,7 +580,12 @@ let OperatorService = class OperatorService {
|
|
|
574
580
|
* the old attempt's summary sitting under a question that was never asked.
|
|
575
581
|
*/
|
|
576
582
|
/** "Continue" on a task that stopped before it finished — a limit from the settings, the bot stopping, the operator: same chat, same branch, fresh count. */
|
|
577
|
-
async continueTask(sessionId, taskId
|
|
583
|
+
async continueTask(sessionId, taskId,
|
|
584
|
+
/**
|
|
585
|
+
* The operator's new prompt: what carries a task that failed on from where it got to, in its chat and
|
|
586
|
+
* on its branch (2026-10-13); optional with any other Continue. See `TaskContinuation.instruction`.
|
|
587
|
+
*/
|
|
588
|
+
instruction) {
|
|
578
589
|
await this.init();
|
|
579
590
|
/*
|
|
580
591
|
* An attempt the operator stopped: which of its last steps ran, and what they printed, read from its
|
|
@@ -584,15 +595,21 @@ let OperatorService = class OperatorService {
|
|
|
584
595
|
const before = (await this.store.getSession(sessionId))?.tasks.find((t) => t.id === taskId);
|
|
585
596
|
const cfg = await this.settings.load();
|
|
586
597
|
const stoppedAt = before?.status === 'aborted' && !before.interruption && before.runId ? await readInterruption(join(cfg.resolved.runsDir, before.runId)).catch(() => null) : null;
|
|
587
|
-
const task = await this.store.continueTask(sessionId, taskId, stoppedAt);
|
|
588
|
-
|
|
598
|
+
const task = await this.store.continueTask(sessionId, taskId, stoppedAt, instruction);
|
|
599
|
+
const after = task.continuing?.how === 'failed' ? ` after it ended ${task.continuing.ended?.status ?? 'failed'}` : '';
|
|
600
|
+
const withPrompt = task.continuing?.instruction ? ', with a new prompt' : '';
|
|
601
|
+
await appendSessionLog(cfg.resolved.runsDir, sessionId, {
|
|
602
|
+
type: 'continue',
|
|
603
|
+
message: `"${task.title}" queued to continue as attempt ${task.attempt ?? 1}${after}${withPrompt}`,
|
|
604
|
+
data: { taskId, attempt: task.attempt, ...(task.continuing?.instruction ? { instruction: task.continuing.instruction } : {}) },
|
|
605
|
+
}).catch(() => undefined);
|
|
589
606
|
this.bus.publish({
|
|
590
607
|
sessionId,
|
|
591
608
|
taskId,
|
|
592
609
|
type: 'task-requeued',
|
|
593
610
|
level: 'info',
|
|
594
|
-
message: `"${task.title}" is queued to continue where it stopped (attempt ${task.attempt ?? 1})`,
|
|
595
|
-
data: { attempt: task.attempt, continuing: true },
|
|
611
|
+
message: `"${task.title}" is queued to continue where it ${after ? 'ended' : 'stopped'}${after}${withPrompt} (attempt ${task.attempt ?? 1})`,
|
|
612
|
+
data: { attempt: task.attempt, continuing: true, ...(task.continuing?.instruction ? { instruction: true } : {}) },
|
|
596
613
|
});
|
|
597
614
|
return task;
|
|
598
615
|
}
|
|
@@ -1130,8 +1147,16 @@ let OperatorService = class OperatorService {
|
|
|
1130
1147
|
x.vcsBaseCommit = undefined;
|
|
1131
1148
|
x.vcsStart = undefined;
|
|
1132
1149
|
}
|
|
1150
|
+
// Chosen again after the session ran: a re-run starts from it too, and a per-session line goes on under a new name.
|
|
1151
|
+
if (applied.restart) {
|
|
1152
|
+
x.startRechosenAt = new Date().toISOString();
|
|
1153
|
+
if (applied.restart.freshBranch) {
|
|
1154
|
+
x.vcs.branchName = applied.restart.freshBranch;
|
|
1155
|
+
x.vcs.branchNameExact = true;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1133
1158
|
});
|
|
1134
|
-
const message = `${applied.said}${applied.note ? `; ${applied.note}` : ''}${applied.clearStart ? '; the start prepared before (a starting snapshot) no longer applies' : ''}`;
|
|
1159
|
+
const message = `${applied.said}${applied.note ? `; ${applied.note}` : ''}${applied.clearStart && !applied.restart ? '; the start prepared before (a starting snapshot) no longer applies' : ''}`;
|
|
1135
1160
|
entries.push({ at: new Date().toISOString(), type: 'start-chosen', message, data: { session: id, choice: starts[id], ...(applied.note ? { note: applied.note } : {}) } });
|
|
1136
1161
|
this.bus.publish({ sessionId: id, type: 'start-chosen', level: 'info', message, data: { runId, choice: starts[id] } });
|
|
1137
1162
|
await appendSessionLog(cfg.resolved.runsDir, id, { type: 'start-chosen', message, data: { runId, choice: starts[id] } }).catch(() => undefined);
|
|
@@ -1323,6 +1348,8 @@ let OperatorService = class OperatorService {
|
|
|
1323
1348
|
await this.store.saveModels({ options, current: current ?? undefined, readAt: new Date().toISOString() });
|
|
1324
1349
|
},
|
|
1325
1350
|
},
|
|
1351
|
+
// Plan mode `approve`: the plan is put to the operator on the approvals card.
|
|
1352
|
+
approvePlan: (ask) => this.askPlanApproval(ask, controller.signal),
|
|
1326
1353
|
})
|
|
1327
1354
|
.then(async (outcome) => {
|
|
1328
1355
|
const tally = { ...(await this.tally(sessionId, queuedIds)), paused: outcome.paused, ...(outcome.refused ? { refused: outcome.refused } : {}) };
|
|
@@ -2196,6 +2223,54 @@ let OperatorService = class OperatorService {
|
|
|
2196
2223
|
message: `choose "${ask.asked}" in the Copilot window, then answer on the card`, data: { approvalId: approval.id, model: approval.model } });
|
|
2197
2224
|
});
|
|
2198
2225
|
}
|
|
2226
|
+
/**
|
|
2227
|
+
* A task's plan, put to the operator (plan mode `approve`, in a run that asks before each command): it
|
|
2228
|
+
* waits on the approvals card, on every page, until it is approved, sent back for another, or the task
|
|
2229
|
+
* is stopped. See `PlanMode`.
|
|
2230
|
+
*/
|
|
2231
|
+
askPlanApproval(ask, signal) {
|
|
2232
|
+
return new Promise((resolveAnswer) => {
|
|
2233
|
+
if (signal.aborted) {
|
|
2234
|
+
resolveAnswer({ decision: 'stop' });
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
const approval = {
|
|
2238
|
+
id: newId('a-'),
|
|
2239
|
+
sessionId: ask.sessionId,
|
|
2240
|
+
taskId: ask.taskId,
|
|
2241
|
+
stepId: 0,
|
|
2242
|
+
description: `The plan of "${ask.title}" (version ${ask.plan.version})`,
|
|
2243
|
+
createdAt: new Date().toISOString(),
|
|
2244
|
+
plan: { title: ask.title, plan: ask.plan },
|
|
2245
|
+
};
|
|
2246
|
+
this.waiting.set(approval.id, {
|
|
2247
|
+
approval,
|
|
2248
|
+
resolve: (d) => resolveAnswer(d.action === 'run' ? { decision: 'approve' } : { decision: 'stop' }),
|
|
2249
|
+
resolvePlan: resolveAnswer,
|
|
2250
|
+
});
|
|
2251
|
+
this.bus.publish({ sessionId: ask.sessionId, taskId: ask.taskId, type: 'approval-requested', level: 'warn',
|
|
2252
|
+
message: `the plan of "${ask.title}" (version ${ask.plan.version}) is waiting for you to approve it`, data: { approvalId: approval.id, plan: true } });
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Answers a plan waiting for approval: approve it (with a note the chat is given beside it), ask for
|
|
2257
|
+
* another (saying what to change), or stop the task.
|
|
2258
|
+
*/
|
|
2259
|
+
decidePlan(approvalId, action, note) {
|
|
2260
|
+
const w = this.waiting.get(approvalId);
|
|
2261
|
+
if (!w || !w.resolvePlan)
|
|
2262
|
+
return { ok: false, problem: 'That plan is not waiting any more.' };
|
|
2263
|
+
const text = (note ?? '').trim();
|
|
2264
|
+
if (action === 'revise' && !text)
|
|
2265
|
+
return { ok: false, problem: 'Say what to change in the plan, so the chat knows what the next one must do differently.' };
|
|
2266
|
+
if (text.length > MAX_INSTRUCTION_CHARS)
|
|
2267
|
+
return { ok: false, problem: `The note is ${text.length} characters; it can be at most ${MAX_INSTRUCTION_CHARS}.` };
|
|
2268
|
+
this.waiting.delete(approvalId);
|
|
2269
|
+
w.resolvePlan({ decision: action, ...(text ? { note: text } : {}) });
|
|
2270
|
+
this.bus.publish({ sessionId: w.approval.sessionId, taskId: w.approval.taskId, type: 'approval-decided', level: 'info',
|
|
2271
|
+
message: `plan: ${action}${text ? ' (with a note)' : ''}`, data: { approvalId, action, plan: true } });
|
|
2272
|
+
return { ok: true };
|
|
2273
|
+
}
|
|
2199
2274
|
pendingApprovals(sessionId) {
|
|
2200
2275
|
return [...this.waiting.values()].map((w) => w.approval).filter((a) => !sessionId || a.sessionId === sessionId);
|
|
2201
2276
|
}
|
|
@@ -149,6 +149,13 @@ export const RunConfigSchema = z.object({
|
|
|
149
149
|
* there, a fetch included, is put in front of a person anyway.
|
|
150
150
|
*/
|
|
151
151
|
networkFetch: z.enum(['ask', 'refuse', 'run']).default('ask'),
|
|
152
|
+
/**
|
|
153
|
+
* Plan mode, for every session that does not set its own (see `PlanMode` in session/model.ts): a
|
|
154
|
+
* task's chat first looks at the project read-only and replies with a plan, and the work begins once
|
|
155
|
+
* the plan is approved — by the runner (`auto`), or by the operator (`approve`, in a run that asks
|
|
156
|
+
* before each command; a run without asking approves it itself). Off by default: it costs messages.
|
|
157
|
+
*/
|
|
158
|
+
planMode: z.enum(['off', 'auto', 'approve']).default('off'),
|
|
152
159
|
/**
|
|
153
160
|
* The shell a step or a check runs in when it names none of its own.
|
|
154
161
|
*
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Planning rounds after which the chat is reminded that a plan is what ends the phase. */
|
|
2
|
+
export const PLAN_NUDGE_AFTER = 12;
|
|
3
|
+
/** What the opening message says about the phase, for a task in plan mode. */
|
|
4
|
+
export function planModeNote(mode, enforced, asks) {
|
|
5
|
+
return [
|
|
6
|
+
'## Plan first (plan mode)',
|
|
7
|
+
'',
|
|
8
|
+
'Before you change anything in this task, look at the project and make a plan. In this phase you only',
|
|
9
|
+
'read: list the folders, open the files the task touches, read the git log, search for what the task',
|
|
10
|
+
'names — find what is already implemented, what is missing and what a change would affect.',
|
|
11
|
+
enforced
|
|
12
|
+
? 'Change nothing yet: the runner puts back any change made in this phase, and tells you.'
|
|
13
|
+
: 'Change nothing yet: nothing in this phase may change a file.',
|
|
14
|
+
'When you have seen enough, reply with status "plan" and no steps:',
|
|
15
|
+
'',
|
|
16
|
+
'```json',
|
|
17
|
+
'{"status": "plan", "notes": "…", "plan": {"existing": "what is already there that the task touches: files, functions, behaviour", "approach": "how you will do it", "steps": ["first stage", "second stage"], "files": ["the files you will create or change"], "verification": "how you will show the result works", "risks": "risks and open questions"}}',
|
|
18
|
+
'```',
|
|
19
|
+
'',
|
|
20
|
+
mode === 'approve' && asks
|
|
21
|
+
? 'The operator reads the plan, and approves it or asks for another with what to change.'
|
|
22
|
+
: 'The runner approves the plan and tells you so.',
|
|
23
|
+
'Begin the work only once you are told the plan is approved. Then carry it out; when it has to change,',
|
|
24
|
+
'say so in `notes` and why. The independent review checks the work against the approved plan.',
|
|
25
|
+
].join('\n');
|
|
26
|
+
}
|
|
27
|
+
/** The plan as the chat gave it, recorded. */
|
|
28
|
+
export function planFrom(reply, version, revisions = []) {
|
|
29
|
+
return {
|
|
30
|
+
existing: reply.existing.trim(),
|
|
31
|
+
approach: reply.approach.trim(),
|
|
32
|
+
steps: reply.steps.map((x) => x.trim()).filter(Boolean),
|
|
33
|
+
files: reply.files.map((x) => x.trim()).filter(Boolean),
|
|
34
|
+
verification: reply.verification.trim(),
|
|
35
|
+
...(reply.risks?.trim() ? { risks: reply.risks.trim() } : {}),
|
|
36
|
+
version,
|
|
37
|
+
at: new Date().toISOString(),
|
|
38
|
+
...(revisions.length > 0 ? { revisions } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The plan in words: for the record, the reviewer and the approval card's plain-text form. */
|
|
42
|
+
export function describePlan(plan) {
|
|
43
|
+
return [
|
|
44
|
+
`What is already there: ${plan.existing}`,
|
|
45
|
+
`Approach: ${plan.approach}`,
|
|
46
|
+
'Stages:',
|
|
47
|
+
...plan.steps.map((x, i) => `${i + 1}. ${x}`),
|
|
48
|
+
...(plan.files.length > 0 ? [`Files it will create or change: ${plan.files.join(', ')}`] : []),
|
|
49
|
+
`Verification: ${plan.verification}`,
|
|
50
|
+
...(plan.risks ? [`Risks and open questions: ${plan.risks}`] : []),
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
/** Approved: the work begins. */
|
|
54
|
+
export function planApprovedMessage(approval) {
|
|
55
|
+
const by = approval.by === 'operator'
|
|
56
|
+
? 'The operator approved your plan.'
|
|
57
|
+
: `Your plan is approved by the runner${approval.why ? ` (${approval.why})` : ''}.`;
|
|
58
|
+
return [
|
|
59
|
+
by,
|
|
60
|
+
...(approval.note?.trim() ? ['', 'With this from the operator, which wins where it and the plan disagree:', '', approval.note.trim()] : []),
|
|
61
|
+
'',
|
|
62
|
+
'Carry it out now: reply with status "continue" and the steps of its first stage. Keep to the plan; when it',
|
|
63
|
+
'has to change, say so in `notes` and why — a change the plan did not foresee is declared, not hidden.',
|
|
64
|
+
'Verify the result as the plan says before you report done. Step numbering restarts at 1.',
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
|
67
|
+
/** The operator asked for another plan. */
|
|
68
|
+
export function planReviseMessage(comment, version) {
|
|
69
|
+
return [
|
|
70
|
+
`The operator read version ${version} of your plan and asks for another. What to change:`,
|
|
71
|
+
'',
|
|
72
|
+
comment.trim(),
|
|
73
|
+
'',
|
|
74
|
+
'You are still in the planning phase: change nothing. Look at what you need to, then reply with status',
|
|
75
|
+
'"plan" and the whole revised plan — not only what changed.',
|
|
76
|
+
].join('\n');
|
|
77
|
+
}
|
|
78
|
+
/** A plan sent when none is asked for: the phase is over, or the session is not in plan mode. */
|
|
79
|
+
export const PLAN_NOT_NOW = 'No plan is asked for now: the planning phase is over and the plan stands. Carry on with the work — reply with status ' +
|
|
80
|
+
'"continue" and steps, or end the task. When the plan has to change, say so in `notes` and why.';
|
|
81
|
+
/** "done" in the planning phase: nothing has been done yet. */
|
|
82
|
+
export const PLAN_FIRST = 'Not yet: this task is in the planning phase, and nothing has been changed. Reply with status "plan" and the plan; ' +
|
|
83
|
+
'the work begins once it is approved.';
|
|
84
|
+
/** Said after many planning rounds. */
|
|
85
|
+
export function planNudge(rounds) {
|
|
86
|
+
return `You have read the project for ${rounds} rounds in the planning phase. Send the plan now, with status "plan"; what you have not looked at yet can go under "risks".`;
|
|
87
|
+
}
|
|
88
|
+
/** The plan section of the reviewer's brief. */
|
|
89
|
+
export function planForReviewer(plan) {
|
|
90
|
+
return [
|
|
91
|
+
'## The plan the work was to follow',
|
|
92
|
+
'',
|
|
93
|
+
`The implementer made this plan before it changed anything, and it was approved${plan.approved?.by === 'operator' ? ' by the operator' : ''}.`,
|
|
94
|
+
'Check that the work follows it: a stage left out, a file changed that the plan does not name, or another',
|
|
95
|
+
'approach — none of them declared in the implementer\'s notes or deviations with a reason — is a finding',
|
|
96
|
+
'with "about": "work". A departure the implementer declared and justified is not a fault in itself; judge',
|
|
97
|
+
'whether the reason holds. The work is still judged against the task as written first.',
|
|
98
|
+
...(plan.approved?.note ? ['', `The operator added: ${plan.approved.note}`] : []),
|
|
99
|
+
'',
|
|
100
|
+
describePlan(plan),
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
@@ -31,6 +31,7 @@ import { writeReport } from '../exec/reportFile.js';
|
|
|
31
31
|
import { mergeRedactions } from '../exec/redaction.js';
|
|
32
32
|
import { trackedRepoOf } from '../vcs/taskVcs.js';
|
|
33
33
|
import { buildCoveringMessage, assertSendable } from '../protocol/reporter.js';
|
|
34
|
+
import { planForReviewer } from './planMode.js';
|
|
34
35
|
import { inScope } from '../vcs/scope.js';
|
|
35
36
|
/** The sentence both sides get when a review left something listening. */
|
|
36
37
|
function leftoverNote(leftovers) {
|
|
@@ -52,7 +53,9 @@ export function deliverableFor(task, summary, tracked, changedFiles) {
|
|
|
52
53
|
return { summary: text, why: 'no-files-changed' };
|
|
53
54
|
return undefined;
|
|
54
55
|
}
|
|
55
|
-
export function reviewBrief(session, task, changedFiles, cwd, deviations = [], previous, disputes = [], derivedFailing = [], earlier = [], deliverable
|
|
56
|
+
export function reviewBrief(session, task, changedFiles, cwd, deviations = [], previous, disputes = [], derivedFailing = [], earlier = [], deliverable,
|
|
57
|
+
/** The approved plan, in plan mode: see `planForReviewer`. */
|
|
58
|
+
plan) {
|
|
56
59
|
// The same repository the runner branched and read the changed files from, however it is named.
|
|
57
60
|
const repo = trackedRepoOf(session);
|
|
58
61
|
// Each file marked against the task's scope, and the scope said: the reviewer was shown side effects as
|
|
@@ -75,6 +78,7 @@ export function reviewBrief(session, task, changedFiles, cwd, deviations = [], p
|
|
|
75
78
|
'## The project instructions that applied',
|
|
76
79
|
'',
|
|
77
80
|
task.level2.trim() || '(none)',
|
|
81
|
+
...(plan ? ['', planForReviewer(plan)] : []),
|
|
78
82
|
...(earlier.length > 0
|
|
79
83
|
? [
|
|
80
84
|
'',
|
|
@@ -257,14 +261,14 @@ export async function runReview(transport, session, task, deps) {
|
|
|
257
261
|
// The machine's shells, when they are not what the contract's example assumes — the same note
|
|
258
262
|
// the implementer's conversation opens with, for a conversation working to the same example.
|
|
259
263
|
const machineNote = shellNote(undefined, defaultShell);
|
|
260
|
-
let before = await transport.sendAndConfirm(machineNote ? `${contract}
|
|
261
|
-
|
|
264
|
+
let before = await transport.sendAndConfirm(machineNote ? `${contract}
|
|
265
|
+
|
|
262
266
|
${machineNote}` : contract);
|
|
263
267
|
await transport.waitForReply(before);
|
|
264
268
|
await pacer.throttleSend();
|
|
265
269
|
// The brief is saved as sent. Until it was, whether a later round had been told what the
|
|
266
270
|
// earlier one found could not be checked from the run folder at all.
|
|
267
|
-
const brief = reviewBrief(session, task, deps.changedFiles, deps.cwd, deps.deviations, deps.previous, deps.disputes, deps.derivedFailing, deps.earlier, deps.deliverable);
|
|
271
|
+
const brief = reviewBrief(session, task, deps.changedFiles, deps.cwd, deps.deviations, deps.previous, deps.disputes, deps.derivedFailing, deps.earlier, deps.deliverable, deps.plan);
|
|
268
272
|
await saveReply('00-brief', brief);
|
|
269
273
|
await deps.record(`REVIEW ${round} BRIEF`, brief);
|
|
270
274
|
before = await transport.sendAndConfirm(brief);
|
|
@@ -42,6 +42,7 @@ import { RunLog } from '../log/runLog.js';
|
|
|
42
42
|
import { composeOpening, readOnlyNote } from '../session/compose.js';
|
|
43
43
|
import { MIN_TRIED_APPROACHES } from '../protocol/replySchema.js';
|
|
44
44
|
import { enforceScope, scopeMessage, scopeNote } from '../vcs/scope.js';
|
|
45
|
+
import { PLAN_FIRST, PLAN_NOT_NOW, PLAN_NUDGE_AFTER, describePlan, planApprovedMessage, planFrom, planModeNote, planNudge, planReviseMessage } from './planMode.js';
|
|
45
46
|
import { composeHandoff } from '../session/handoff.js';
|
|
46
47
|
import { ProgressWatch } from './progress.js';
|
|
47
48
|
import { contractConflicts, readsTreeClean } from './contract.js';
|
|
@@ -650,6 +651,14 @@ async function withOperatorHand(transport, wanted, first, hooks, sessionId, bus,
|
|
|
650
651
|
return { result, goOnAsItIs: false };
|
|
651
652
|
}
|
|
652
653
|
/** Runs one task to completion inside an already-open transport. */
|
|
654
|
+
/** The approved plan a task works to: its own attempt's, else, for a continuation, the latest one before it. */
|
|
655
|
+
function approvedPlanOf(task) {
|
|
656
|
+
if (task.plan?.approved)
|
|
657
|
+
return task.plan;
|
|
658
|
+
if (!task.continuing)
|
|
659
|
+
return undefined;
|
|
660
|
+
return [...(task.attempts ?? [])].reverse().find((a) => a.plan?.approved)?.plan;
|
|
661
|
+
}
|
|
653
662
|
export async function runTask(transport, session, task, deps) {
|
|
654
663
|
const { cfg, store, bus, authorizer, signal } = deps;
|
|
655
664
|
// Every attempt gets its own folder. The first keeps the original name, so nothing that
|
|
@@ -1256,6 +1265,23 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1256
1265
|
const scopeEnforced = (scope.length > 0 || !!task.readOnly) && onOwnBranch;
|
|
1257
1266
|
if (scopeEnforced)
|
|
1258
1267
|
enforcement = { scope, readOnly: !!task.readOnly };
|
|
1268
|
+
/*
|
|
1269
|
+
* Plan mode (2026-10-13): the session's own, else Settings. A continuation carries on under the plan
|
|
1270
|
+
* its attempt already has — it is in the conversation — so only a task that starts anew plans; a new
|
|
1271
|
+
* prompt on finished work is a new instruction, and plans. The planning phase is read-only, enforced
|
|
1272
|
+
* the way a read-only task is wherever the task has a branch of its own.
|
|
1273
|
+
*/
|
|
1274
|
+
const planMode = session.planMode ?? cfg.execution.planMode ?? 'off';
|
|
1275
|
+
let planning = planMode !== 'off' && !task.continuing;
|
|
1276
|
+
const planEnforced = planning && onOwnBranch;
|
|
1277
|
+
let planVersion = 0;
|
|
1278
|
+
let planningRounds = 0;
|
|
1279
|
+
const planRevisions = [];
|
|
1280
|
+
/*
|
|
1281
|
+
* The approved plan the work follows, for the reviewer: one approved in this attempt, or the earlier
|
|
1282
|
+
* attempt's for a task carried on. Kept here, because `task` is the record as it was when the attempt began.
|
|
1283
|
+
*/
|
|
1284
|
+
let planInForce = approvedPlanOf(task);
|
|
1259
1285
|
if (watchesTree)
|
|
1260
1286
|
startState = await treeState(repoDirOf(session));
|
|
1261
1287
|
// The operator's input files, read-only on a branch of the task's own; see `vcs/inputs.ts`.
|
|
@@ -1384,6 +1410,8 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1384
1410
|
contractAlreadySent: session.contractSent && !!session.chat,
|
|
1385
1411
|
continuing,
|
|
1386
1412
|
buildsOn,
|
|
1413
|
+
// The operator's new prompt goes out whether or not the attempt it follows reached a conversation.
|
|
1414
|
+
...(task.continuing?.instruction ? { instruction: { text: task.continuing.instruction, fromAttempt: task.continuing.fromAttempt, ended: task.continuing.ended } } : {}),
|
|
1387
1415
|
workDirNote: workingDirNote(work),
|
|
1388
1416
|
vcsNote: prepared.note,
|
|
1389
1417
|
// What is said about a commit is true only where one happens (live run 2026-10-03: promised with commits off).
|
|
@@ -1409,6 +1437,9 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1409
1437
|
`each listed in "tried". With fewer, the runner asks you for another approach instead of accepting it.`
|
|
1410
1438
|
: undefined,
|
|
1411
1439
|
shellNote: shellNote(shells, defaultShell),
|
|
1440
|
+
...(planning && planMode !== 'off'
|
|
1441
|
+
? { planNote: planModeNote(planMode, planEnforced, planMode === 'approve' && !!deps.approvePlan && (deps.currentMode?.() ?? cfg.execution.mode) === 'confirm') }
|
|
1442
|
+
: {}),
|
|
1412
1443
|
});
|
|
1413
1444
|
await setTask((t) => {
|
|
1414
1445
|
t.firstMessage = opening.firstMessage;
|
|
@@ -1857,6 +1888,11 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1857
1888
|
deliverable,
|
|
1858
1889
|
deviations,
|
|
1859
1890
|
disputes,
|
|
1891
|
+
/*
|
|
1892
|
+
* The approved plan, for the review to hold the work to (plan mode, 2026-10-13): this attempt's, or —
|
|
1893
|
+
* carried on in its chat — the one the attempt it continues worked to.
|
|
1894
|
+
*/
|
|
1895
|
+
...(planInForce ? { plan: planInForce } : {}),
|
|
1860
1896
|
/*
|
|
1861
1897
|
* Each earlier finding with its evidence, the correction the implementer reports, and what the
|
|
1862
1898
|
* finding's own check came to at the last gate: the next reviewer judges the finding on the evidence,
|
|
@@ -2202,7 +2238,9 @@ export async function runTask(transport, session, task, deps) {
|
|
|
2202
2238
|
await setTask((t) => {
|
|
2203
2239
|
t.iterations = iterations;
|
|
2204
2240
|
});
|
|
2205
|
-
const { reply,
|
|
2241
|
+
const { reply, blocked } = parsed;
|
|
2242
|
+
// Nothing is done while the plan is being made: the stop word in a planning round ends nothing.
|
|
2243
|
+
const done = parsed.done && !planning;
|
|
2206
2244
|
sink.event('reply-parsed', { iteration: iterations, status: reply.status, steps: reply.steps.length, notes: reply.notes }, `iteration ${iterations}: ${reply.steps.length} step(s)${reply.notes ? ` — ${reply.notes}` : ''}`);
|
|
2207
2245
|
/*
|
|
2208
2246
|
* A deviation is recorded the moment it is declared, not when the task ends.
|
|
@@ -2262,6 +2300,81 @@ export async function runTask(transport, session, task, deps) {
|
|
|
2262
2300
|
* what the reply said, at greater length and one message later. The reason carries the
|
|
2263
2301
|
* approaches that were tried, and that is what the register shows.
|
|
2264
2302
|
*/
|
|
2303
|
+
/*
|
|
2304
|
+
* Plan mode: the reply that ends the planning phase — put to the operator or approved by the runner —
|
|
2305
|
+
* and the two that do not belong in it. Answered here, in a message of its own, as a repair of the
|
|
2306
|
+
* format is: no step runs and nothing is committed in between.
|
|
2307
|
+
*/
|
|
2308
|
+
if (reply.status === 'plan' || (planning && reply.status === 'done')) {
|
|
2309
|
+
let message;
|
|
2310
|
+
let tag;
|
|
2311
|
+
if (reply.status === 'done') {
|
|
2312
|
+
message = PLAN_FIRST;
|
|
2313
|
+
tag = 'plan-first';
|
|
2314
|
+
sink.event('plan-first', { iteration: iterations }, '"done" in the planning phase: nothing has been changed yet; the chat was asked for the plan', 'warn');
|
|
2315
|
+
}
|
|
2316
|
+
else if (!planning || !reply.plan) {
|
|
2317
|
+
message = PLAN_NOT_NOW;
|
|
2318
|
+
tag = 'plan-not-asked';
|
|
2319
|
+
sink.event('plan-not-asked', { iteration: iterations }, 'a plan arrived when none is asked for; the chat was told to carry on', 'warn');
|
|
2320
|
+
}
|
|
2321
|
+
else {
|
|
2322
|
+
planVersion += 1;
|
|
2323
|
+
tag = `plan-${planVersion}`;
|
|
2324
|
+
const plan = planFrom(reply.plan, planVersion, [...planRevisions]);
|
|
2325
|
+
await setTask((t) => {
|
|
2326
|
+
t.plan = plan;
|
|
2327
|
+
});
|
|
2328
|
+
await record(`PLAN ${planVersion}`, describePlan(plan));
|
|
2329
|
+
sink.event('plan-received', { version: planVersion, steps: plan.steps.length, files: plan.files }, `plan ${planVersion} received: ${plan.steps.length} stage(s)${plan.files.length > 0 ? `, ${plan.files.length} file(s)` : ''}`);
|
|
2330
|
+
// Asked of the operator only in a run that asks before each command: one that does not, approves it itself.
|
|
2331
|
+
const asks = planMode === 'approve' && !!deps.approvePlan && (deps.currentMode?.() ?? cfg.execution.mode) === 'confirm';
|
|
2332
|
+
let answer = { decision: 'approve' };
|
|
2333
|
+
if (asks) {
|
|
2334
|
+
sink.event('plan-waiting', { version: planVersion }, `plan ${planVersion} is waiting for the operator`, 'warn');
|
|
2335
|
+
answer = await deps.approvePlan({ sessionId: session.id, taskId: task.id, title: task.title, plan });
|
|
2336
|
+
}
|
|
2337
|
+
if (signal?.aborted || answer.decision === 'stop') {
|
|
2338
|
+
return await finish('aborted', answer.decision === 'stop' ? 'stopped by the operator at its plan' : 'stopped by the operator', undefined, lastMarkdown);
|
|
2339
|
+
}
|
|
2340
|
+
if (answer.decision === 'revise') {
|
|
2341
|
+
const comment = (answer.note ?? '').trim() || 'Make the plan more concrete: name the files and the stages.';
|
|
2342
|
+
planRevisions.push({ version: planVersion, comment, at: new Date().toISOString() });
|
|
2343
|
+
await setTask((t) => {
|
|
2344
|
+
if (t.plan)
|
|
2345
|
+
t.plan.revisions = [...planRevisions];
|
|
2346
|
+
});
|
|
2347
|
+
sink.event('plan-revise', { version: planVersion, comment }, `the operator asked for another plan: ${comment}`);
|
|
2348
|
+
message = planReviseMessage(comment, planVersion);
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
// Who approved it, and why the operator was not asked when they were not.
|
|
2352
|
+
const why = asks ? undefined : planMode === 'auto' ? 'plan mode approves plans automatically' : 'the run does not ask before each command';
|
|
2353
|
+
const approved = {
|
|
2354
|
+
by: asks ? 'operator' : 'auto',
|
|
2355
|
+
at: new Date().toISOString(),
|
|
2356
|
+
...(answer.note?.trim() ? { note: answer.note.trim() } : {}),
|
|
2357
|
+
...(why ? { why } : {}),
|
|
2358
|
+
};
|
|
2359
|
+
await setTask((t) => {
|
|
2360
|
+
if (t.plan)
|
|
2361
|
+
t.plan.approved = approved;
|
|
2362
|
+
});
|
|
2363
|
+
planInForce = { ...plan, approved };
|
|
2364
|
+
sink.event('plan-approved', { version: planVersion, by: approved.by, ...(approved.note ? { note: approved.note } : {}) }, `plan ${planVersion} approved ${asks ? 'by the operator' : `by the runner — ${why}`}${approved.note ? `, with a note` : ''}`);
|
|
2365
|
+
planning = false;
|
|
2366
|
+
message = planApprovedMessage(approved);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
await record(tag.toUpperCase().replace(/-/g, ' '), message);
|
|
2370
|
+
await pacer.throttleSend();
|
|
2371
|
+
const before = await transport.sendAndConfirm(message);
|
|
2372
|
+
const next = await transport.waitForReply(before);
|
|
2373
|
+
await saveReply(tag, next);
|
|
2374
|
+
lastMarkdown = next.markdown;
|
|
2375
|
+
await pacer.settle();
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2265
2378
|
if (!blocked && reply.steps.length > 0)
|
|
2266
2379
|
earlyBlocks = 0;
|
|
2267
2380
|
/*
|
|
@@ -2279,8 +2392,12 @@ export async function runTask(transport, session, task, deps) {
|
|
|
2279
2392
|
* usually lost the assignment, said "above": it is given again rather than told nothing ran (live run 2026-10-04).
|
|
2280
2393
|
*/
|
|
2281
2394
|
const message = task.continuing
|
|
2282
|
-
?
|
|
2395
|
+
? `Not yet: this task is being continued, and nothing has run since. The assignment is this one${task.continuing.instruction ? '' : ', unchanged'}:\n\n` +
|
|
2283
2396
|
`${task.prompt.length > 6000 ? `${task.prompt.slice(0, 6000)}…` : task.prompt}\n\n` +
|
|
2397
|
+
// With the operator's new prompt, when there is one: the reason the task is being carried on at all.
|
|
2398
|
+
(task.continuing.instruction
|
|
2399
|
+
? `With the operator's new instruction, which wins where the two disagree:\n\n${task.continuing.instruction.length > 6000 ? `${task.continuing.instruction.slice(0, 6000)}…` : task.continuing.instruction}\n\n`
|
|
2400
|
+
: '') +
|
|
2284
2401
|
'Carry on from where it stopped: send your next step now with status "continue", for example a command that shows ' +
|
|
2285
2402
|
'the state of the files the task works on. A task may end "blocked" only after steps that ran.'
|
|
2286
2403
|
: 'Not yet: nothing has been run in this task. You do not need to operate anything yourself — this runner executes ' +
|
|
@@ -2522,6 +2639,24 @@ export async function runTask(transport, session, task, deps) {
|
|
|
2522
2639
|
});
|
|
2523
2640
|
}
|
|
2524
2641
|
}
|
|
2642
|
+
/*
|
|
2643
|
+
* The planning phase changes nothing: what a round changed is put back before the chat reads the
|
|
2644
|
+
* results, as for a read-only task, and the chat is told. A reminder after many rounds that the
|
|
2645
|
+
* plan is what ends the phase.
|
|
2646
|
+
*/
|
|
2647
|
+
if (planning) {
|
|
2648
|
+
planningRounds += 1;
|
|
2649
|
+
if (planEnforced) {
|
|
2650
|
+
const check = await enforceScope(repoDirOf(session), [], true).catch(() => null);
|
|
2651
|
+
if (check && (check.outside.length > 0 || check.failed.length > 0)) {
|
|
2652
|
+
sink.event('plan-change-reverted', { iteration: iterations, reverted: check.reverted, failed: check.failed }, `changed in the planning phase, put back: ${check.reverted.join(', ') || '(none)'}`, 'warn');
|
|
2653
|
+
runnerNotes.push(`This is the planning phase, and nothing may change yet: the runner put back ${check.reverted.length > 0 ? check.reverted.join(', ') : 'nothing it could'}` +
|
|
2654
|
+
`${check.failed.length > 0 ? ` and could not put back ${check.failed.map((f) => f.path).join(', ')}` : ''}. Read, then send the plan.`);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
if (planningRounds === PLAN_NUDGE_AFTER)
|
|
2658
|
+
runnerNotes.push(planNudge(planningRounds));
|
|
2659
|
+
}
|
|
2525
2660
|
await guardInputs(iterations);
|
|
2526
2661
|
// --- report back ---------------------------------------------------------------
|
|
2527
2662
|
const report = await writeReport(results, {
|
package/dist/src/plan/brief.js
CHANGED
|
@@ -309,6 +309,12 @@ const FIELD_ROWS_EN = [
|
|
|
309
309
|
'no',
|
|
310
310
|
'Overrides the setting above for this one session: give the same name to the sessions that should share a chat, and leave it "" for a session that should have its own.',
|
|
311
311
|
],
|
|
312
|
+
[
|
|
313
|
+
'planMode',
|
|
314
|
+
'session',
|
|
315
|
+
'no',
|
|
316
|
+
'Plan first: "auto" — each task looks at the code read-only and makes a plan, approved by the runner, before it changes anything; "approve" — the same, approved by the operator in a run that asks before each command; "off". Leave it out to follow Settings. Ask when the work is large or the code base is new to the chat.',
|
|
317
|
+
],
|
|
312
318
|
[
|
|
313
319
|
'projectDir',
|
|
314
320
|
'session',
|
|
@@ -380,6 +386,12 @@ const FIELD_ROWS_BG = [
|
|
|
380
386
|
'не',
|
|
381
387
|
'Отменя настройката горе само за тази сесия: дай едно и също име на сесиите, които да споделят чат, и остави "" на онази, която да е сама.',
|
|
382
388
|
],
|
|
389
|
+
[
|
|
390
|
+
'planMode',
|
|
391
|
+
'сесия',
|
|
392
|
+
'не',
|
|
393
|
+
'Първо план: "auto" — всяка задача разглежда кода само с четене и прави план, одобрен от runner-а, преди да промени нещо; "approve" — същото, одобрено от оператора при пускане с питане преди всяка команда; "off". Пропусни го, за да следва Настройките. Питай, когато работата е голяма или кодът е непознат за чата.',
|
|
394
|
+
],
|
|
383
395
|
[
|
|
384
396
|
'projectDir',
|
|
385
397
|
'сесия',
|
|
@@ -231,6 +231,8 @@ defaultProjectDir = '') {
|
|
|
231
231
|
// the standing default, else the session's own.
|
|
232
232
|
s.review = { enabled: planned.review?.enabled !== false, model: (planned.review?.model || defaultReviewModel).trim() };
|
|
233
233
|
s.conversationGroup = group || undefined;
|
|
234
|
+
if (planned.planMode)
|
|
235
|
+
s.planMode = planned.planMode;
|
|
234
236
|
s.model = (planned.model || defaultModel).trim() || undefined;
|
|
235
237
|
// The plan's models are kept, and marked as the plan's: Settings outrank them (see `effectiveModels`).
|
|
236
238
|
if (planned.model?.trim())
|
package/dist/src/plan/schema.js
CHANGED
|
@@ -348,6 +348,16 @@ const SessionInput = z.object({
|
|
|
348
348
|
* the same name. Empty means it gets a conversation of its own.
|
|
349
349
|
*/
|
|
350
350
|
conversationGroup: z.string().trim().default(''),
|
|
351
|
+
/**
|
|
352
|
+
* Plan mode for the session's tasks (see `PlanMode` in session/model.ts): "auto" plans first and goes
|
|
353
|
+
* on, "approve" plans first and waits for the operator in a run that asks before each command, "off"
|
|
354
|
+
* does not plan apart. Absent follows Settings.
|
|
355
|
+
*/
|
|
356
|
+
planMode: z
|
|
357
|
+
.enum(['off', 'auto', 'approve'], {
|
|
358
|
+
error: 'planMode must be "off", "auto" (plan first, approved by the runner) or "approve" (plan first, approved by the operator in a run that asks before each command).',
|
|
359
|
+
})
|
|
360
|
+
.optional(),
|
|
351
361
|
vcs: VcsInput,
|
|
352
362
|
review: ReviewInput.optional(),
|
|
353
363
|
/**
|