copilot-operator 0.1.13 → 0.1.15
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/README.md +9 -14
- package/dist/src/api/operator.controller.js +31 -42
- package/dist/src/api/operator.service.js +663 -367
- package/dist/src/api/saveToDesktop.js +1 -1
- package/dist/src/api/security.js +40 -10
- package/dist/src/api/server.js +75 -12
- package/dist/src/api/settings.js +156 -15
- package/dist/src/cli.js +108 -97
- package/dist/src/config/handWritten.js +36 -0
- package/dist/src/config/layout.js +13 -4
- package/dist/src/config/lockedPolicy.js +56 -22
- package/dist/src/config/schema.js +13 -84
- package/dist/src/context/desktopDir.js +44 -0
- package/dist/src/exec/authorizer.js +9 -1
- package/dist/src/exec/checks.js +240 -23
- package/dist/src/exec/dangerous.js +29 -5
- package/dist/src/exec/network.js +105 -28
- package/dist/src/exec/redaction.js +52 -9
- package/dist/src/exec/reportFile.js +4 -5
- package/dist/src/exec/runner.js +146 -13
- package/dist/src/exec/shells.js +41 -6
- package/dist/src/exec/workDir.js +4 -4
- package/dist/src/orchestrator/derivedChecks.js +1 -0
- package/dist/src/orchestrator/review.js +25 -3
- package/dist/src/orchestrator/taskRunner.js +476 -163
- package/dist/src/plan/brief.js +37 -44
- package/dist/src/plan/importPlan.js +15 -7
- package/dist/src/plan/schema.js +64 -29
- package/dist/src/plan/script.js +0 -15
- package/dist/src/plan/systemGuide.js +2 -177
- package/dist/src/protocol/parser.js +5 -1
- package/dist/src/session/debugExport.js +1 -1
- package/dist/src/session/exportRecord.js +1 -1
- package/dist/src/session/exports.js +49 -9
- package/dist/src/session/store.js +215 -80
- package/dist/src/vcs/scope.js +29 -6
- package/dist/src/vcs/taskVcs.js +78 -10
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/36-94660f1cde2da529.js +1 -0
- package/dist/web/_next/static/chunks/549-9548d9a132c8c7d4.js +1 -0
- package/dist/web/_next/static/chunks/867-8b95ae984befc0e2.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-71a43ed69134b48d.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-886f5fc99e146408.js +1 -0
- package/dist/web/_next/static/css/{a0642fdb8adf9edb.css → e2a2c9eb3c226313.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 +18 -10
- package/prompts/level1.md +2 -1
- package/prompts/organisation.bg.md +0 -1
- package/prompts/organisation.en.md +0 -1
- package/dist/src/context/contextFiles.js +0 -295
- package/dist/src/context/desktopMirror.js +0 -173
- package/dist/src/context/projectMirror.js +0 -432
- package/dist/web/_next/static/chunks/295-04151ded0741d18d.js +0 -1
- package/dist/web/_next/static/chunks/36-c3dd06c6a459c114.js +0 -1
- package/dist/web/_next/static/chunks/549-a5a59a94f92bb3eb.js +0 -1
- package/dist/web/_next/static/chunks/867-84c19f047972ffc3.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-5f180a88398be4c9.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-34d7988849f9aa76.js +0 -1
- /package/dist/web/_next/static/{NosxirjkOgV-Boyq8dGPl → IwNZiBrEMAcXEUFj2miiU}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{NosxirjkOgV-Boyq8dGPl → IwNZiBrEMAcXEUFj2miiU}/_ssgManifest.js +0 -0
package/dist/src/plan/brief.js
CHANGED
|
@@ -92,20 +92,29 @@ ${lines.join('\n')}
|
|
|
92
92
|
* Whether a run with nobody watching can start on this machine — and when it cannot, what to press.
|
|
93
93
|
*
|
|
94
94
|
* Kerrigan's phase 2 script offers the unattended button first, because on a machine that allows it
|
|
95
|
-
* that is the ordinary way to run. On one that does not — no isolation claimed, or no
|
|
96
|
-
* the runner refuses it at the entrance, and a persona that recommends it has sent the
|
|
97
|
-
* a button that says no. She cannot see the machine; the app can, so it tells her, and
|
|
98
|
-
* matters. The labels are the ones on the screens, so check:guide holds them.
|
|
95
|
+
* that is the ordinary way to run. On one that does not — a policy lock, no isolation claimed, or no
|
|
96
|
+
* allowlist — the runner refuses it at the entrance, and a persona that recommends it has sent the
|
|
97
|
+
* operator to a button that says no. She cannot see the machine; the app can, so it tells her, and
|
|
98
|
+
* only when it matters. The labels are the ones on the screens, so check:guide holds them.
|
|
99
99
|
*/
|
|
100
100
|
function machineSection(unattendedBlocked, lang) {
|
|
101
101
|
if (!unattendedBlocked)
|
|
102
102
|
return '';
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
/*
|
|
104
|
+
* Each reason named as it is, because each sends the operator somewhere different. A lock blamed
|
|
105
|
+
* on the allowlist sent them to fill in a list that was already full, and the run was refused all
|
|
106
|
+
* the same: the lock is the machine's, set beside the install, and no page changes it.
|
|
107
|
+
*/
|
|
108
|
+
const whyBg = {
|
|
109
|
+
lock: 'файлът `policy.lock.json` до инсталацията на тази машина забранява пускания без надзор, и никоя настройка на страниците не го променя',
|
|
110
|
+
isolation: '**„Къде върви ботът“** в `/defaults` още казва, че ботът върви в акаунта на оператора, без изолация',
|
|
111
|
+
allowlist: 'списъкът с позволени програми е празен, а без него нищо не ограничава стъпка, която никой не гледа',
|
|
112
|
+
}[unattendedBlocked];
|
|
113
|
+
const whyEn = {
|
|
114
|
+
lock: "this machine's `policy.lock.json`, beside the install, forbids runs with nobody watching, and no setting on any page changes it",
|
|
115
|
+
isolation: '**"Where the bot runs"** on `/defaults` still says the bot runs in the operator\'s own account, with no isolation',
|
|
116
|
+
allowlist: 'the list of allowed programs is empty, and without it nothing limits a step that nobody is watching',
|
|
117
|
+
}[unattendedBlocked];
|
|
109
118
|
return lang === 'bg'
|
|
110
119
|
? `
|
|
111
120
|
## На тази машина пускане без надзор се отказва
|
|
@@ -248,14 +257,6 @@ export function planExample() {
|
|
|
248
257
|
baseBranch: 'main',
|
|
249
258
|
},
|
|
250
259
|
review: { enabled: true, model: '' },
|
|
251
|
-
mirror: {
|
|
252
|
-
enabled: true,
|
|
253
|
-
rootDir: root,
|
|
254
|
-
includeDirs: ['src/invoices'],
|
|
255
|
-
excludeDirs: [],
|
|
256
|
-
respectGitignore: true,
|
|
257
|
-
includeEnvFiles: false,
|
|
258
|
-
},
|
|
259
260
|
tasks: firstTasks,
|
|
260
261
|
},
|
|
261
262
|
{
|
|
@@ -308,6 +309,12 @@ const FIELD_ROWS_EN = [
|
|
|
308
309
|
'no',
|
|
309
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.',
|
|
310
311
|
],
|
|
312
|
+
[
|
|
313
|
+
'projectDir',
|
|
314
|
+
'session',
|
|
315
|
+
'no',
|
|
316
|
+
'The project folder of a session with version control off: its commands run there. Leave it out for the default project. With version control on, `vcs.repoDir` is required and the commands run in it.',
|
|
317
|
+
],
|
|
311
318
|
[
|
|
312
319
|
'review',
|
|
313
320
|
'session',
|
|
@@ -317,7 +324,6 @@ const FIELD_ROWS_EN = [
|
|
|
317
324
|
['review', 'task', 'no', 'Set to `false` to skip the review for one task. Absent means the session decides.'],
|
|
318
325
|
['readOnly', 'task', 'no', '`true` for a task that must not change files — an audit, a smoke test, a report. The runner fails it if the tree changed, and still commits the change on its branch so nothing is lost.'],
|
|
319
326
|
['scope', 'task', 'no', 'The paths the task may change: files, folders (`tests/e2e/`) or `*`/`**` patterns. With version control on, the runner puts back any change outside them. Give it when the task is about one suite or module.'],
|
|
320
|
-
['mirror', 'session', 'no, but ask', 'Whether project files are copied and attached to the first message as context, and which: root, directories in and out, gitignore, env files. Off when left out.'],
|
|
321
327
|
['tasks', 'session', 'yes', 'At least one, in the order they must run.'],
|
|
322
328
|
['title', 'task', 'yes', '3 to 120 characters. Short, latin, hyphenated.'],
|
|
323
329
|
['prompt', 'task', 'yes', 'The task itself, at least 30 characters. This is what Copilot reads.'],
|
|
@@ -373,6 +379,12 @@ const FIELD_ROWS_BG = [
|
|
|
373
379
|
'не',
|
|
374
380
|
'Отменя настройката горе само за тази сесия: дай едно и също име на сесиите, които да споделят чат, и остави "" на онази, която да е сама.',
|
|
375
381
|
],
|
|
382
|
+
[
|
|
383
|
+
'projectDir',
|
|
384
|
+
'сесия',
|
|
385
|
+
'не',
|
|
386
|
+
'Папката на проекта на сесия с изключен контрол на версиите: командите ѝ се изпълняват там. Пропусни го за проекта по подразбиране. С включен контрол на версиите `vcs.repoDir` е задължително и командите се изпълняват в него.',
|
|
387
|
+
],
|
|
376
388
|
[
|
|
377
389
|
'review',
|
|
378
390
|
'сесия',
|
|
@@ -382,7 +394,6 @@ const FIELD_ROWS_BG = [
|
|
|
382
394
|
['review', 'задача', 'не', 'Сложи `false`, за да се пропусне рецензията само за тази задача. Липсата значи каквото казва сесията.'],
|
|
383
395
|
['readOnly', 'задача', 'не', '`true` за задача, която не бива да променя файлове — одит, smoke тест, доклад. Runner-ът я проваля, ако дървото е променено, и пак комитва промяната на клона ѝ, за да не се губи нищо.'],
|
|
384
396
|
['scope', 'задача', 'не', 'Пътищата, които задачата може да променя: файлове, папки (`tests/e2e/`) или шаблони с `*`/`**`. При включен контрол на версиите runner-ът връща всяка промяна извън тях. Давай го, когато задачата е за един suite или модул.'],
|
|
385
|
-
['mirror', 'сесия', 'не, но питай', 'Дали файлове от проекта се копират и прикачат към първото съобщение като контекст, и кои: корен, директории вътре и вън, gitignore, env файлове. Изключено, когато липсва.'],
|
|
386
397
|
['tasks', 'сесия', 'да', 'Поне една, в реда, в който трябва да се изпълнят.'],
|
|
387
398
|
['title', 'задача', 'да', 'От 3 до 120 знака. Кратко, латиница, с тирета.'],
|
|
388
399
|
['prompt', 'задача', 'да', 'Самата задача, поне 30 знака. Това чете Copilot.'],
|
|
@@ -465,7 +476,9 @@ commit is written: imperative, short, about the change rather than about the tas
|
|
|
465
476
|
\`"vcs": { "enabled": false, "repoDir": "", "branchMode": "per-task", "commitOnFinish": false, "branchPrefix": "cop/" }\`.
|
|
466
477
|
Then give its tasks no \`vcs\` at all — no branch, no commit message — because there is nothing
|
|
467
478
|
to name. Read-only work, audits and reports are the usual reason. Say in \`notes\` why you turned
|
|
468
|
-
it off, so the operator can disagree with you.
|
|
479
|
+
it off, so the operator can disagree with you. Its commands run in the default project; when its
|
|
480
|
+
work is in another one, give that project's path as the session's \`projectDir\`, since the empty
|
|
481
|
+
\`repoDir\` cannot say it.
|
|
469
482
|
|
|
470
483
|
**Do not leave \`vcs\` out of a session.** The system refuses the whole document and hands the
|
|
471
484
|
user a line saying so, and you will have to ask the question then anyway — after wasting their
|
|
@@ -521,7 +534,9 @@ reset или merge. \`"updateFromRemote": false\` го изключва — са
|
|
|
521
534
|
\`"vcs": { "enabled": false, "repoDir": "", "branchMode": "per-task", "commitOnFinish": false, "branchPrefix": "cop/" }\`.
|
|
522
535
|
На нейните задачи не давай \`vcs\` изобщо — нито клон, нито текст на комит — защото няма какво да
|
|
523
536
|
се именува. Обичайната причина е работа само за четене, одит или отчет. Напиши в \`notes\` защо си
|
|
524
|
-
го изключил, за да може операторът да не се съгласи с теб.
|
|
537
|
+
го изключил, за да може операторът да не се съгласи с теб. Командите ѝ се изпълняват в проекта по
|
|
538
|
+
подразбиране; когато работата ѝ е в друг, дай пътя на този проект като \`projectDir\` на сесията,
|
|
539
|
+
защото празното \`repoDir\` не може да го каже.
|
|
525
540
|
|
|
526
541
|
**Не пропускай \`vcs\` в сесия.** Системата отказва целия документ и връща на потребителя ред,
|
|
527
542
|
който го казва, а ти пак ще трябва да зададеш въпроса — след като си му загубил времето. Задай
|
|
@@ -625,16 +640,6 @@ const CHECKS_BG = `
|
|
|
625
640
|
трите минават без свършена работа.
|
|
626
641
|
Всеки критерий от заданието е покрит поне от една проверка, която пада, докато работата не е свършена.
|
|
627
642
|
`.trim();
|
|
628
|
-
const MIRROR_EN = `
|
|
629
|
-
**mirror**: \`enabled\`, \`rootDir\` (absolute), \`includeDirs\` and \`excludeDirs\` (paths relative to
|
|
630
|
-
the root, forward slashes), \`respectGitignore\` (leave it true), \`includeEnvFiles\` (leave it false
|
|
631
|
-
unless the user insists — it copies secrets into a chat).
|
|
632
|
-
`.trim();
|
|
633
|
-
const MIRROR_BG = `
|
|
634
|
-
**mirror**: \`enabled\`, \`rootDir\` (абсолютен), \`includeDirs\` и \`excludeDirs\` (пътища спрямо корена,
|
|
635
|
-
с наклонени черти напред), \`respectGitignore\` (остави го true), \`includeEnvFiles\` (остави го false,
|
|
636
|
-
освен ако потребителят не настоява — копира тайни в чат).
|
|
637
|
-
`.trim();
|
|
638
643
|
const VCS_RULE_EN = '- **Version control is yours to ask about, and there is no default.** With it on, the runner makes a branch\n' +
|
|
639
644
|
' before each task and a commit after it, using the names you give, and it never pushes; Copilot must not touch\n' +
|
|
640
645
|
' git itself. With it off, files are changed where they lie and there is no way back. Ask the user which it is,\n' +
|
|
@@ -1523,10 +1528,6 @@ asking for both — the first is what makes the next plan quicker to write than
|
|
|
1523
1528
|
judges it. Ask whether it stays on and on which model; a model different from the working
|
|
1524
1529
|
one catches more. \`model\` for the work itself only if the user names one from the chat's
|
|
1525
1530
|
own picker.
|
|
1526
|
-
- \`mirror\` (optional, off by default): whether project files are copied and attached to the
|
|
1527
|
-
first message as context — from which root, which directories in and out, whether
|
|
1528
|
-
\`.gitignore\` is respected (yes), whether \`.env\` files go in (no: they are secrets). Ask;
|
|
1529
|
-
never assume.
|
|
1530
1531
|
3. **The tasks.** For each: \`title\` (required), \`prompt\` (required — what Copilot reads:
|
|
1531
1532
|
precise, with paths, ports and commands), \`expected\` (the bar, one sentence), \`checks\`
|
|
1532
1533
|
(the same bar written so the runner can decide it; write one wherever a command or a file can
|
|
@@ -1611,8 +1612,6 @@ ${table(['Field', 'Where', 'Required', 'What it is'], rows)}
|
|
|
1611
1612
|
|
|
1612
1613
|
${VCS_DETAIL_EN}
|
|
1613
1614
|
|
|
1614
|
-
${MIRROR_EN}
|
|
1615
|
-
|
|
1616
1615
|
${CHECKS_EN}
|
|
1617
1616
|
|
|
1618
1617
|
### A filled-in example
|
|
@@ -1746,10 +1745,6 @@ ${systemGuideSection('bg')}
|
|
|
1746
1745
|
работата и я оценява. Питай дали остава включен и на кой модел; модел, различен от
|
|
1747
1746
|
работния, хваща повече. \`model\` за самата работа — само ако потребителят назове такъв
|
|
1748
1747
|
от менюто на чата.
|
|
1749
|
-
- \`mirror\` (незадължително, изключено по подразбиране): дали файлове от проекта се копират
|
|
1750
|
-
и прикачат към първото съобщение като контекст — от кой корен, кои директории вътре и вън,
|
|
1751
|
-
дали се спазва \`.gitignore\` (да), дали влизат \`.env\` файлове (не: те са тайни). Питай;
|
|
1752
|
-
никога не предполагай.
|
|
1753
1748
|
3. **Задачите.** За всяка: \`title\` (задължително), \`prompt\` (задължително — това чете
|
|
1754
1749
|
Copilot: точно, с пътища, портове и команди), \`expected\` (летвата, едно изречение),
|
|
1755
1750
|
\`checks\` (същата летва, написана така, че runner-ът да я решава сам; пиши по една навсякъде,
|
|
@@ -1835,8 +1830,6 @@ ${table(['Поле', 'Къде', 'Задължително', 'Какво е'], r
|
|
|
1835
1830
|
|
|
1836
1831
|
${VCS_DETAIL_BG}
|
|
1837
1832
|
|
|
1838
|
-
${MIRROR_BG}
|
|
1839
|
-
|
|
1840
1833
|
${CHECKS_BG}
|
|
1841
1834
|
|
|
1842
1835
|
### Попълнен пример
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_VCS, applyDefaultProject } from '../session/store.js';
|
|
2
2
|
import { availableShells, detectShells } from '../exec/shells.js';
|
|
3
3
|
/** The heading under which a task's acceptance bar reaches Copilot. English, like level 1. */
|
|
4
4
|
const EXPECTED_HEADER = '### Expected result';
|
|
@@ -145,7 +145,13 @@ function unavailableShellWarnings(plan) {
|
|
|
145
145
|
*/
|
|
146
146
|
export async function importPlan(store, plan, defaultModel = '', defaultReviewModel = '',
|
|
147
147
|
/** The operator's persona at the moment of import, written into every task's level 2. */
|
|
148
|
-
persona = ''
|
|
148
|
+
persona = '',
|
|
149
|
+
/**
|
|
150
|
+
* The default project on the Project page, for a session that names no folder of its own. The
|
|
151
|
+
* service passes it, so an imported session works where one made by hand would; see
|
|
152
|
+
* `applyDefaultProject`. Empty leaves such a session without a folder.
|
|
153
|
+
*/
|
|
154
|
+
defaultProjectDir = '') {
|
|
149
155
|
const sessions = [];
|
|
150
156
|
const warnings = [...unavailableShellWarnings(plan)];
|
|
151
157
|
/*
|
|
@@ -160,12 +166,12 @@ persona = '') {
|
|
|
160
166
|
? (plan.plan.trim() || `plan-${new Date().toISOString().slice(0, 16).replace(/[:T-]/g, '')}`).slice(0, 60)
|
|
161
167
|
: '';
|
|
162
168
|
for (const planned of plan.sessions) {
|
|
163
|
-
|
|
164
|
-
if (mirror
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
// Attaching project files to the chat was removed; an old plan's root is still its project folder.
|
|
170
|
+
if (planned.mirror?.enabled) {
|
|
171
|
+
warnings.push(`Session "${planned.name}" asks for project files to be attached to the chat. That no longer exists: ` +
|
|
172
|
+
'the chat reads the project by running commands. "mirror" was ignored.');
|
|
167
173
|
}
|
|
168
|
-
const created = await store.createSession(planned.name, mirror);
|
|
174
|
+
const created = await store.createSession(planned.name, planned.projectDir || planned.mirror?.rootDir || '');
|
|
169
175
|
const vcs = { ...DEFAULT_VCS, ...(planned.vcs ?? {}) };
|
|
170
176
|
vcs.branchPrefix = vcs.branchPrefix.trim() || DEFAULT_VCS.branchPrefix;
|
|
171
177
|
vcs.repoDir = vcs.repoDir.trim();
|
|
@@ -183,6 +189,8 @@ persona = '') {
|
|
|
183
189
|
s.model = (planned.model || defaultModel).trim() || undefined;
|
|
184
190
|
s.planName = plan.plan.trim() || undefined;
|
|
185
191
|
s.vcs = vcs;
|
|
192
|
+
// After the plan's own folders are in place, so only a session that named none is pointed at it.
|
|
193
|
+
applyDefaultProject(s, defaultProjectDir);
|
|
186
194
|
});
|
|
187
195
|
const titles = [];
|
|
188
196
|
for (const task of planned.tasks) {
|
package/dist/src/plan/schema.js
CHANGED
|
@@ -167,15 +167,12 @@ const TaskVcsInput = z.object({
|
|
|
167
167
|
branch: z.string().trim().optional(),
|
|
168
168
|
commitMessage: z.string().trim().optional(),
|
|
169
169
|
});
|
|
170
|
-
/**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
respectGitignore: z.boolean().optional(),
|
|
177
|
-
includeEnvFiles: z.boolean().optional(),
|
|
178
|
-
});
|
|
170
|
+
/**
|
|
171
|
+
* What was once the project files copied into the chat, removed on 2026-09-30. Still read, so a plan
|
|
172
|
+
* written before then imports: its `rootDir` becomes the session's project folder, and the rest is
|
|
173
|
+
* ignored with a warning. See `importPlan`.
|
|
174
|
+
*/
|
|
175
|
+
const LegacyMirrorInput = z.object({ enabled: z.boolean().optional(), rootDir: z.string().optional() }).passthrough();
|
|
179
176
|
const TaskInput = z.object({
|
|
180
177
|
title: z
|
|
181
178
|
.string()
|
|
@@ -271,7 +268,14 @@ const SessionInput = z.object({
|
|
|
271
268
|
conversationGroup: z.string().trim().default(''),
|
|
272
269
|
vcs: VcsInput,
|
|
273
270
|
review: ReviewInput.optional(),
|
|
274
|
-
|
|
271
|
+
/**
|
|
272
|
+
* The project folder of a session with version control off: where its commands run. Absent means
|
|
273
|
+
* the default project on the Project page. With version control on, `vcs.repoDir` is required (see
|
|
274
|
+
* `checkPlan`) and the commands run there, so this folder decides nothing for such a session.
|
|
275
|
+
*/
|
|
276
|
+
projectDir: z.string().trim().optional(),
|
|
277
|
+
/** Read only for old plans; see `LegacyMirrorInput`. */
|
|
278
|
+
mirror: LegacyMirrorInput.optional(),
|
|
275
279
|
tasks: z.array(TaskInput).min(1, 'A session with no tasks would import as an empty queue.'),
|
|
276
280
|
});
|
|
277
281
|
export const PlanSchema = z.object({
|
|
@@ -350,7 +354,6 @@ const KNOWN_KEYS = {
|
|
|
350
354
|
vcs: keysOf(VcsInput),
|
|
351
355
|
taskVcs: keysOf(TaskVcsInput),
|
|
352
356
|
review: keysOf(ReviewInput),
|
|
353
|
-
mirror: keysOf(MirrorInput),
|
|
354
357
|
};
|
|
355
358
|
function unknownKeys(value, kind, where, out) {
|
|
356
359
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -361,31 +364,46 @@ function unknownKeys(value, kind, where, out) {
|
|
|
361
364
|
}
|
|
362
365
|
}
|
|
363
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* A value of the raw document as an object whose fields can be read, or nothing.
|
|
369
|
+
*
|
|
370
|
+
* The warning walk runs on the document as it arrived, before the schema has said anything about
|
|
371
|
+
* it, so every level of it may be null, a number or a list where an object belongs. A cast does
|
|
372
|
+
* not make it one: `sessions: [null]` read `null.vcs` and threw, where the schema would have
|
|
373
|
+
* refused it by its path. What is not an object has no fields to warn about, and is left for the
|
|
374
|
+
* schema to refuse.
|
|
375
|
+
*/
|
|
376
|
+
function fieldsOf(value) {
|
|
377
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
378
|
+
}
|
|
364
379
|
/** Fields a model invented, walked by hand because the schema drops them silently. */
|
|
365
380
|
function collectWarnings(raw) {
|
|
366
381
|
const out = [];
|
|
367
|
-
|
|
382
|
+
const plan = fieldsOf(raw);
|
|
383
|
+
if (!plan)
|
|
368
384
|
return out;
|
|
369
|
-
unknownKeys(
|
|
370
|
-
|
|
371
|
-
if (!Array.isArray(sessions))
|
|
385
|
+
unknownKeys(plan, 'plan', 'plan', out);
|
|
386
|
+
if (!Array.isArray(plan.sessions))
|
|
372
387
|
return out;
|
|
373
|
-
sessions.forEach((s, i) => {
|
|
374
|
-
|
|
375
|
-
|
|
388
|
+
plan.sessions.forEach((s, i) => {
|
|
389
|
+
const session = fieldsOf(s);
|
|
390
|
+
if (!session)
|
|
391
|
+
return;
|
|
392
|
+
unknownKeys(session, 'session', `sessions[${i}]`, out);
|
|
376
393
|
unknownKeys(session.vcs, 'vcs', `sessions[${i}].vcs`, out);
|
|
377
394
|
unknownKeys(session.review, 'review', `sessions[${i}].review`, out);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
395
|
+
if (!Array.isArray(session.tasks))
|
|
396
|
+
return;
|
|
397
|
+
session.tasks.forEach((t, j) => {
|
|
398
|
+
const task = fieldsOf(t);
|
|
399
|
+
if (!task)
|
|
400
|
+
return;
|
|
401
|
+
unknownKeys(task, 'task', `sessions[${i}].tasks[${j}]`, out);
|
|
402
|
+
unknownKeys(task.vcs, 'taskVcs', `sessions[${i}].tasks[${j}].vcs`, out);
|
|
403
|
+
if (Array.isArray(task.checks)) {
|
|
404
|
+
task.checks.forEach((c, k) => unknownKeys(c, 'check', `sessions[${i}].tasks[${j}].checks[${k}]`, out));
|
|
405
|
+
}
|
|
406
|
+
});
|
|
389
407
|
});
|
|
390
408
|
return out;
|
|
391
409
|
}
|
|
@@ -428,8 +446,25 @@ export function summarise(plan) {
|
|
|
428
446
|
*
|
|
429
447
|
* Never throws. Every way this can fail — not JSON at all, the wrong version, a task with no
|
|
430
448
|
* prompt — is an answer the user is meant to see and hand back to the chat model.
|
|
449
|
+
*
|
|
450
|
+
* That promise is kept here, around the whole reading, and not only by each walk being careful:
|
|
451
|
+
* the walks run on whatever a chat produced, and one of them reading a field of null once turned
|
|
452
|
+
* a malformed plan into a request error carrying a JavaScript message, which neither the page nor
|
|
453
|
+
* the chat that wrote the plan can do anything with. A walk that throws again is an answer too.
|
|
431
454
|
*/
|
|
432
455
|
export function checkPlan(text) {
|
|
456
|
+
try {
|
|
457
|
+
return readPlan(text);
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
warnings: [],
|
|
463
|
+
issues: [{ path: '', message: `This plan could not be read: ${e.message}. Check that every session, task and check is a JSON object.` }],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function readPlan(text) {
|
|
433
468
|
const source = extractJson(text ?? '');
|
|
434
469
|
if (!source.trim()) {
|
|
435
470
|
return { ok: false, warnings: [], issues: [{ path: '', message: 'There is nothing here to import.' }] };
|
package/dist/src/plan/script.js
CHANGED
|
@@ -173,14 +173,6 @@ export const SCRIPT = [
|
|
|
173
173
|
},
|
|
174
174
|
],
|
|
175
175
|
},
|
|
176
|
-
{
|
|
177
|
-
choose: { en: 'How do I read its code from this chat?', bg: 'Как да чета кода му от този чат?' },
|
|
178
|
-
options: [
|
|
179
|
-
{ en: 'As files attached to the first message of a task', bg: 'Като прикачени файлове към първото съобщение на задачата' },
|
|
180
|
-
{ en: 'Through the Desktop mirror (Desktop/copilot-operator-context/)', bg: 'През огледалото на Desktop-а (Desktop/copilot-operator-context/)' },
|
|
181
|
-
{ en: 'Both', bg: 'И двете' },
|
|
182
|
-
],
|
|
183
|
-
},
|
|
184
176
|
{
|
|
185
177
|
choose: {
|
|
186
178
|
en: 'May the bot make git branches and commits in this project?',
|
|
@@ -704,13 +696,6 @@ export const SCRIPT = [
|
|
|
704
696
|
{ en: 'Off', bg: 'Изключена' },
|
|
705
697
|
],
|
|
706
698
|
},
|
|
707
|
-
{
|
|
708
|
-
choose: { en: 'Project files for the chat (`mirror`):', bg: 'Файлове на проекта към чата (`mirror`):' },
|
|
709
|
-
options: [
|
|
710
|
-
{ en: 'No', bg: 'Не' },
|
|
711
|
-
{ en: 'Yes — give the root and the folders', bg: 'Да — напиши корена и папките' },
|
|
712
|
-
],
|
|
713
|
-
},
|
|
714
699
|
{
|
|
715
700
|
choose: { en: 'Name of the run (`plan`):', bg: 'Име на пускането (`plan`):' },
|
|
716
701
|
options: [{ en: '{the name you propose from the goal}', bg: '{името, което предлагаш от целта}' }],
|
|
@@ -1241,157 +1241,6 @@ export const SYSTEM_GUIDE = [
|
|
|
1241
1241
|
},
|
|
1242
1242
|
],
|
|
1243
1243
|
},
|
|
1244
|
-
{
|
|
1245
|
-
headingKey: 'mirror.title',
|
|
1246
|
-
en: 'Project files for the chat',
|
|
1247
|
-
bg: 'Файлове на проекта за чата',
|
|
1248
|
-
controls: [
|
|
1249
|
-
{
|
|
1250
|
-
key: 'mirror.enable',
|
|
1251
|
-
kind: 'checkbox',
|
|
1252
|
-
en: 'attach project files',
|
|
1253
|
-
bg: 'прикачай файловете на проекта',
|
|
1254
|
-
doesEn: 'Attaches the selected files to the first message of every task; it takes effect only after Save below.',
|
|
1255
|
-
doesBg: 'Прикача избраните файлове към първото съобщение на всяка задача; влиза в сила чак след Запази отдолу.',
|
|
1256
|
-
},
|
|
1257
|
-
{
|
|
1258
|
-
key: 'mirror.root',
|
|
1259
|
-
kind: 'field',
|
|
1260
|
-
en: 'Project root',
|
|
1261
|
-
bg: 'Корен на проекта',
|
|
1262
|
-
doesEn: 'The folder the include and exclude lists below are relative to.',
|
|
1263
|
-
doesBg: 'Папката, спрямо която са списъците за включване и изключване отдолу.',
|
|
1264
|
-
},
|
|
1265
|
-
{
|
|
1266
|
-
key: 'mirror.listDirs',
|
|
1267
|
-
kind: 'button',
|
|
1268
|
-
en: 'List directories',
|
|
1269
|
-
bg: 'Покажи директориите',
|
|
1270
|
-
doesEn: 'Lists the root\'s folders, to pick what to include.',
|
|
1271
|
-
doesBg: 'Изброява папките в корена, за да се избере какво да се включи.',
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
key: 'mirror.rules',
|
|
1275
|
-
kind: 'note',
|
|
1276
|
-
en: 'What is copied from it',
|
|
1277
|
-
bg: 'Какво се копира от него',
|
|
1278
|
-
doesEn: 'The heading above the include and exclude lists.',
|
|
1279
|
-
doesBg: 'Заглавието над списъците за включване и изключване.',
|
|
1280
|
-
},
|
|
1281
|
-
{
|
|
1282
|
-
key: 'mirror.include',
|
|
1283
|
-
kind: 'field',
|
|
1284
|
-
en: 'Include (one per line; a directory includes everything beneath it)',
|
|
1285
|
-
bg: 'Включи (по една на ред; директория включва всичко под нея)',
|
|
1286
|
-
doesEn: 'One folder or file per line; a folder brings everything beneath it.',
|
|
1287
|
-
doesBg: 'По една папка или файл на ред; папка включва всичко под нея.',
|
|
1288
|
-
},
|
|
1289
|
-
{
|
|
1290
|
-
key: 'mirror.exclude',
|
|
1291
|
-
kind: 'field',
|
|
1292
|
-
en: 'Exclude (one per line)',
|
|
1293
|
-
bg: 'Изключи (по една на ред)',
|
|
1294
|
-
doesEn: 'One per line: what to leave out even inside an included folder.',
|
|
1295
|
-
doesBg: 'По един на ред: какво да се изостави дори в включена папка.',
|
|
1296
|
-
},
|
|
1297
|
-
{
|
|
1298
|
-
key: 'tree.load',
|
|
1299
|
-
kind: 'button',
|
|
1300
|
-
en: 'Show the folders',
|
|
1301
|
-
bg: 'Покажи папките',
|
|
1302
|
-
doesEn: 'Reads the project folder and draws its tree, so folders can be ticked instead of typed.',
|
|
1303
|
-
doesBg: 'Прочита папката на проекта и показва дървото ѝ, за да се избират папки с клик, вместо да се пишат.',
|
|
1304
|
-
},
|
|
1305
|
-
{
|
|
1306
|
-
key: 'tree.reload',
|
|
1307
|
-
kind: 'button',
|
|
1308
|
-
en: 'Read the folders again',
|
|
1309
|
-
bg: 'Прочети папките отново',
|
|
1310
|
-
doesEn: 'Reads the folder tree again from disk.',
|
|
1311
|
-
doesBg: 'Прочита отново дървото с папки от диска.',
|
|
1312
|
-
},
|
|
1313
|
-
{
|
|
1314
|
-
key: 'tree.exclude',
|
|
1315
|
-
kind: 'button',
|
|
1316
|
-
en: 'carve this folder out',
|
|
1317
|
-
bg: 'изрежи тази папка',
|
|
1318
|
-
doesEn: 'The "−" on a tree row: carves that folder out of an included one.',
|
|
1319
|
-
doesBg: '„−“ на ред в дървото: изважда тази папка от включена.',
|
|
1320
|
-
},
|
|
1321
|
-
{
|
|
1322
|
-
key: 'tree.unexclude',
|
|
1323
|
-
kind: 'button',
|
|
1324
|
-
en: 'stop carving this folder out',
|
|
1325
|
-
bg: 'спри да изрязваш тази папка',
|
|
1326
|
-
doesEn: 'Puts a carved-out folder back.',
|
|
1327
|
-
doesBg: 'Връща изважданата папка обратно.',
|
|
1328
|
-
},
|
|
1329
|
-
{
|
|
1330
|
-
key: 'tree.fold',
|
|
1331
|
-
kind: 'button',
|
|
1332
|
-
en: 'fold',
|
|
1333
|
-
bg: 'сгъни',
|
|
1334
|
-
doesEn: 'Folds a tree row.',
|
|
1335
|
-
doesBg: 'Сгъва ред в дървото.',
|
|
1336
|
-
},
|
|
1337
|
-
{
|
|
1338
|
-
key: 'tree.unfold',
|
|
1339
|
-
kind: 'button',
|
|
1340
|
-
en: 'unfold',
|
|
1341
|
-
bg: 'разгъни',
|
|
1342
|
-
doesEn: 'Unfolds a tree row.',
|
|
1343
|
-
doesBg: 'Разгъва ред в дървото.',
|
|
1344
|
-
},
|
|
1345
|
-
{
|
|
1346
|
-
key: 'mirror.check',
|
|
1347
|
-
kind: 'button',
|
|
1348
|
-
en: 'Check what would be copied',
|
|
1349
|
-
bg: 'Провери какво ще се копира',
|
|
1350
|
-
doesEn: 'Counts what would be copied, without copying: files and size.',
|
|
1351
|
-
doesBg: 'Преброява какво би се копирало, без да копира: файлове и размер.',
|
|
1352
|
-
},
|
|
1353
|
-
{
|
|
1354
|
-
key: 'mirror.showFiles',
|
|
1355
|
-
kind: 'disclosure',
|
|
1356
|
-
en: 'show the list',
|
|
1357
|
-
bg: 'покажи списъка',
|
|
1358
|
-
doesEn: 'Under that count: the list of files.',
|
|
1359
|
-
doesBg: 'Под това преброяване: списъкът с файлове.',
|
|
1360
|
-
},
|
|
1361
|
-
{
|
|
1362
|
-
key: 'mirror.showSkipped',
|
|
1363
|
-
kind: 'disclosure',
|
|
1364
|
-
en: 'show what was left out',
|
|
1365
|
-
bg: 'покажи какво остава навън',
|
|
1366
|
-
doesEn: 'Under that count: what was left out, and why.',
|
|
1367
|
-
doesBg: 'Под това преброяване: какво е изоставено и защо.',
|
|
1368
|
-
},
|
|
1369
|
-
{
|
|
1370
|
-
key: 'mirror.gitignore',
|
|
1371
|
-
kind: 'checkbox',
|
|
1372
|
-
en: 'Leave out what .gitignore lists',
|
|
1373
|
-
bg: 'Не добавяй това, което е в .gitignore',
|
|
1374
|
-
doesEn: 'Keeps everything .gitignore names out of the files sent to the chat.',
|
|
1375
|
-
doesBg: 'Държи всичко от .gitignore извън файловете, пратени в чата.',
|
|
1376
|
-
},
|
|
1377
|
-
{
|
|
1378
|
-
key: 'mirror.env',
|
|
1379
|
-
kind: 'checkbox',
|
|
1380
|
-
en: 'Copy .env files as well',
|
|
1381
|
-
bg: 'Копирай и .env файловете',
|
|
1382
|
-
doesEn: 'Sends the .env files too; it asks for confirmation first, because they usually hold passwords and keys.',
|
|
1383
|
-
doesBg: 'Праща и .env файловете; първо иска потвърждение, защото в тях обикновено има пароли и ключове.',
|
|
1384
|
-
},
|
|
1385
|
-
{
|
|
1386
|
-
key: 'mirror.save',
|
|
1387
|
-
kind: 'button',
|
|
1388
|
-
en: 'Save',
|
|
1389
|
-
bg: 'Запази',
|
|
1390
|
-
doesEn: 'Stores the switch, the project root and both lists onto this session.',
|
|
1391
|
-
doesBg: 'Записва отметката, корена на проекта и двата списъка върху тази сесия.',
|
|
1392
|
-
},
|
|
1393
|
-
],
|
|
1394
|
-
},
|
|
1395
1244
|
{
|
|
1396
1245
|
headingKey: 'tasks.title',
|
|
1397
1246
|
en: 'Tasks',
|
|
@@ -1538,8 +1387,8 @@ export const SYSTEM_GUIDE = [
|
|
|
1538
1387
|
kind: 'button',
|
|
1539
1388
|
en: 'Run again from here',
|
|
1540
1389
|
bg: 'Пусни отново оттук',
|
|
1541
|
-
doesEn: 'Puts the code back, queues that task and every task after it — later sessions of its run included — and starts at once, in the mode that run had.',
|
|
1542
|
-
doesBg: 'Връща кода, нарежда отново тази задача и всички след нея — включително следващите сесии от пускането ѝ — и тръгва веднага, в режима на онова
|
|
1390
|
+
doesEn: 'Puts the code back, queues that task and every task after it — later sessions of its run included — and starts at once, in the mode that run had: step by step when that run left no record of its own, or went on its own and this machine no longer allows that.',
|
|
1391
|
+
doesBg: 'Връща кода, нарежда отново тази задача и всички след нея — включително следващите сесии от пускането ѝ — и тръгва веднага, в режима на онова пускане: стъпка по стъпка, когато то не е оставило запис за себе си или е вървяло самостоятелно, а тази машина вече не го позволява.',
|
|
1543
1392
|
},
|
|
1544
1393
|
{
|
|
1545
1394
|
key: 'task.delete',
|
|
@@ -2736,30 +2585,6 @@ export const SYSTEM_GUIDE = [
|
|
|
2736
2585
|
doesEn: 'Empties that field, so new sessions start with nothing chosen.',
|
|
2737
2586
|
doesBg: 'Изпразва полето, така че новите сесии тръгват без избрано.',
|
|
2738
2587
|
},
|
|
2739
|
-
{
|
|
2740
|
-
key: 'proj.mirrorToDesktop',
|
|
2741
|
-
kind: 'checkbox',
|
|
2742
|
-
en: 'Keep the projects on the Desktop',
|
|
2743
|
-
bg: 'Дръж проектите на Desktop-а',
|
|
2744
|
-
doesEn: 'The master switch: copies the projects on this page to their own Desktop folders before a run and after every round of steps, which is how the files reach the chat.',
|
|
2745
|
-
doesBg: 'Главният ключ: копира проектите от тази страница в собствени папки на Desktop-а преди пускане и след всеки кръг стъпки — така файловете стигат до чата.',
|
|
2746
|
-
},
|
|
2747
|
-
{
|
|
2748
|
-
key: 'proj.desktopThis',
|
|
2749
|
-
kind: 'checkbox',
|
|
2750
|
-
en: 'Keep a copy of this project on the Desktop',
|
|
2751
|
-
bg: 'Дръж копие на този проект на Desktop-а',
|
|
2752
|
-
doesEn: 'Under each project, on by default: untick to keep that one off the Desktop while the switch is on. Greyed out while the switch is off.',
|
|
2753
|
-
doesBg: 'Под всеки проект, включено по подразбиране: махнете отметката, за да не се копира точно той, докато ключът е включен. Неактивно, докато ключът е изключен.',
|
|
2754
|
-
},
|
|
2755
|
-
{
|
|
2756
|
-
key: 'proj.folders',
|
|
2757
|
-
kind: 'disclosure',
|
|
2758
|
-
en: 'Folders that go to the Desktop',
|
|
2759
|
-
bg: 'Папки, които отиват на Desktop-а',
|
|
2760
|
-
doesEn: 'On each project: which of its folders go to the Desktop copy — the same include and exclude lists and folder tree as on a session page.',
|
|
2761
|
-
doesBg: 'На всеки проект: кои папки отиват в копието на Desktop-а — същите списъци за включване и изключване и дърво с папки като на страницата на сесия.',
|
|
2762
|
-
},
|
|
2763
2588
|
{
|
|
2764
2589
|
key: 'proj.affects',
|
|
2765
2590
|
kind: 'note',
|
|
@@ -169,7 +169,11 @@ export function parseReply(markdown, opts) {
|
|
|
169
169
|
reply.notes = stripCitations(reply.notes);
|
|
170
170
|
reply.summary = stripCitations(reply.summary);
|
|
171
171
|
const steps = reply.steps.map((s) => ({ ...s, shell: s.shell ?? opts.defaultShell }));
|
|
172
|
-
|
|
172
|
+
// A blank stop word is no stop word. The setting is free text, and every reply contains the
|
|
173
|
+
// empty string and nearly every one a space, so searching for either would read each
|
|
174
|
+
// `continue` that carries a summary as done and close the task after one round.
|
|
175
|
+
const marker = opts.stopMarker.trim();
|
|
176
|
+
const markerHit = marker !== '' && markdown.includes(marker);
|
|
173
177
|
const hasSummary = (reply.summary ?? '').trim().length > 0;
|
|
174
178
|
// The stop word alone is not enough to end a task any more: the summary is the
|
|
175
179
|
// deliverable, and a marker without one would let a task close with nothing to show.
|
|
@@ -145,7 +145,7 @@ export async function buildDebugExport(input) {
|
|
|
145
145
|
chat: session.chat ? { name: session.chat.name, url: session.chat.url } : undefined,
|
|
146
146
|
vcs: session.vcs,
|
|
147
147
|
vcsBaseCommit: session.vcsBaseCommit,
|
|
148
|
-
|
|
148
|
+
projectDir: session.projectDir,
|
|
149
149
|
tasks,
|
|
150
150
|
});
|
|
151
151
|
}
|
|
@@ -245,7 +245,7 @@ export async function buildExport(input) {
|
|
|
245
245
|
`chat : ${session.chat?.url ?? '(no conversation was opened)'}`,
|
|
246
246
|
`model : ${session.model || 'whatever the chat was set to'}${session.modelInUse ? ` (ran on ${session.modelInUse})` : ''}`,
|
|
247
247
|
`repository : ${session.vcs?.enabled
|
|
248
|
-
? `${session.vcs.repoDir || session.
|
|
248
|
+
? `${session.vcs.repoDir || session.projectDir || '(none set)'} — ${session.vcs.branchMode === 'per-session' ? 'one branch for the whole session' : 'a branch of its own for every task'}, prefix ${session.vcs.branchPrefix}${session.vcs.commitOnFinish ? '' : ', commits off'}`
|
|
249
249
|
: 'version control is off for this session'}`,
|
|
250
250
|
`tasks : ${tasks.length} of ${session.tasks.length}`,
|
|
251
251
|
`exported : ${when(new Date().toISOString())}`,
|