copilot-operator 0.1.12 → 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 +692 -364
- 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 +482 -150
- package/dist/src/plan/brief.js +56 -44
- package/dist/src/plan/importPlan.js +18 -7
- package/dist/src/plan/schema.js +90 -31
- package/dist/src/plan/script.js +79 -90
- package/dist/src/plan/systemGuide.js +39 -182
- 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 +52 -9
- package/dist/src/session/store.js +215 -80
- package/dist/src/vcs/git.js +77 -2
- package/dist/src/vcs/scope.js +29 -6
- package/dist/src/vcs/taskVcs.js +149 -23
- 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/history/page-d7314ac22d47a3ab.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 -135
- 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-b64c436f5b25f39e.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-5f180a88398be4c9.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-854e3dde672e38a2.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-cc99401da5173b36.js +0 -1
- /package/dist/web/_next/static/{bcBE4tY_aNh6kzLzN1luP → IwNZiBrEMAcXEUFj2miiU}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{bcBE4tY_aNh6kzLzN1luP → 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.'],
|
|
@@ -432,6 +443,16 @@ is clean: the runner commits after the checks, and its own commit is what leaves
|
|
|
432
443
|
- \`"previous-session"\` — the session carries on from the end of the branch of the session that
|
|
433
444
|
ran before it **in the same repository**, so the sessions form a chain. The first session of
|
|
434
445
|
such a chain starts from \`baseBranch\`. Sessions in other repositories do not count.
|
|
446
|
+
- \`"existing-branch"\` with \`existingBranch\` — no new branch at all: every task of the session
|
|
447
|
+
works on that existing local branch, exactly as named (\`"existingBranch": "recovery/apz-migration"\`),
|
|
448
|
+
one after another, building on what is already on it. For new work that continues a branch the
|
|
449
|
+
user already has. The branch must exist when the plan is imported; \`branchMode\`, \`branchName\`
|
|
450
|
+
and the prefix do not apply.
|
|
451
|
+
|
|
452
|
+
Before a session's first branch the runner fetches and fast-forwards the branch it starts from
|
|
453
|
+
(\`baseBranch\`, or \`existingBranch\`), so the work begins from the server's code; it never resets or
|
|
454
|
+
merges. \`"updateFromRemote": false\` turns that off — only when the user asks to work offline or from
|
|
455
|
+
local commits on purpose.
|
|
435
456
|
|
|
436
457
|
Put the same \`startFrom\` on every session that works in one repository unless the user wants it
|
|
437
458
|
mixed. Leaving it out keeps the old behaviour — whatever branch the repository is on — which is
|
|
@@ -455,7 +476,9 @@ commit is written: imperative, short, about the change rather than about the tas
|
|
|
455
476
|
\`"vcs": { "enabled": false, "repoDir": "", "branchMode": "per-task", "commitOnFinish": false, "branchPrefix": "cop/" }\`.
|
|
456
477
|
Then give its tasks no \`vcs\` at all — no branch, no commit message — because there is nothing
|
|
457
478
|
to name. Read-only work, audits and reports are the usual reason. Say in \`notes\` why you turned
|
|
458
|
-
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.
|
|
459
482
|
|
|
460
483
|
**Do not leave \`vcs\` out of a session.** The system refuses the whole document and hands the
|
|
461
484
|
user a line saying so, and you will have to ask the question then anyway — after wasting their
|
|
@@ -479,6 +502,15 @@ const VCS_DETAIL_BG = `
|
|
|
479
502
|
- \`"previous-session"\` — сесията продължава от края на клона на сесията, пусната преди нея **в
|
|
480
503
|
същото хранилище**, така че сесиите образуват верига. Първата сесия от веригата тръгва от
|
|
481
504
|
\`baseBranch\`. Сесии в други хранилища не се броят.
|
|
505
|
+
- \`"existing-branch"\` с \`existingBranch\` — без нов клон: всяка задача от сесията работи на този
|
|
506
|
+
съществуващ локален клон, точно с това име (\`"existingBranch": "recovery/apz-migration"\`), една след
|
|
507
|
+
друга, върху това, което вече е там. За нова работа, която продължава клон, който потребителят вече
|
|
508
|
+
има. Клонът трябва да съществува при импорта; \`branchMode\`, \`branchName\` и представката не се прилагат.
|
|
509
|
+
|
|
510
|
+
Преди първия клон на сесията runner-ът прави fetch и fast-forward на клона, от който тя тръгва
|
|
511
|
+
(\`baseBranch\` или \`existingBranch\`), така че работата започва от кода на сървъра; никога не прави
|
|
512
|
+
reset или merge. \`"updateFromRemote": false\` го изключва — само ако потребителят поиска да работи
|
|
513
|
+
офлайн или нарочно от локални commit-и.
|
|
482
514
|
|
|
483
515
|
Сложи един и същ \`startFrom\` на всички сесии в едно хранилище, освен ако потребителят не иска
|
|
484
516
|
различни. Ако го пропуснеш, остава старото поведение — от който клон е хранилището в момента —
|
|
@@ -502,7 +534,9 @@ const VCS_DETAIL_BG = `
|
|
|
502
534
|
\`"vcs": { "enabled": false, "repoDir": "", "branchMode": "per-task", "commitOnFinish": false, "branchPrefix": "cop/" }\`.
|
|
503
535
|
На нейните задачи не давай \`vcs\` изобщо — нито клон, нито текст на комит — защото няма какво да
|
|
504
536
|
се именува. Обичайната причина е работа само за четене, одит или отчет. Напиши в \`notes\` защо си
|
|
505
|
-
го изключил, за да може операторът да не се съгласи с теб.
|
|
537
|
+
го изключил, за да може операторът да не се съгласи с теб. Командите ѝ се изпълняват в проекта по
|
|
538
|
+
подразбиране; когато работата ѝ е в друг, дай пътя на този проект като \`projectDir\` на сесията,
|
|
539
|
+
защото празното \`repoDir\` не може да го каже.
|
|
506
540
|
|
|
507
541
|
**Не пропускай \`vcs\` в сесия.** Системата отказва целия документ и връща на потребителя ред,
|
|
508
542
|
който го казва, а ти пак ще трябва да зададеш въпроса — след като си му загубил времето. Задай
|
|
@@ -606,16 +640,6 @@ const CHECKS_BG = `
|
|
|
606
640
|
трите минават без свършена работа.
|
|
607
641
|
Всеки критерий от заданието е покрит поне от една проверка, която пада, докато работата не е свършена.
|
|
608
642
|
`.trim();
|
|
609
|
-
const MIRROR_EN = `
|
|
610
|
-
**mirror**: \`enabled\`, \`rootDir\` (absolute), \`includeDirs\` and \`excludeDirs\` (paths relative to
|
|
611
|
-
the root, forward slashes), \`respectGitignore\` (leave it true), \`includeEnvFiles\` (leave it false
|
|
612
|
-
unless the user insists — it copies secrets into a chat).
|
|
613
|
-
`.trim();
|
|
614
|
-
const MIRROR_BG = `
|
|
615
|
-
**mirror**: \`enabled\`, \`rootDir\` (абсолютен), \`includeDirs\` и \`excludeDirs\` (пътища спрямо корена,
|
|
616
|
-
с наклонени черти напред), \`respectGitignore\` (остави го true), \`includeEnvFiles\` (остави го false,
|
|
617
|
-
освен ако потребителят не настоява — копира тайни в чат).
|
|
618
|
-
`.trim();
|
|
619
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' +
|
|
620
644
|
' before each task and a commit after it, using the names you give, and it never pushes; Copilot must not touch\n' +
|
|
621
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' +
|
|
@@ -1504,10 +1528,6 @@ asking for both — the first is what makes the next plan quicker to write than
|
|
|
1504
1528
|
judges it. Ask whether it stays on and on which model; a model different from the working
|
|
1505
1529
|
one catches more. \`model\` for the work itself only if the user names one from the chat's
|
|
1506
1530
|
own picker.
|
|
1507
|
-
- \`mirror\` (optional, off by default): whether project files are copied and attached to the
|
|
1508
|
-
first message as context — from which root, which directories in and out, whether
|
|
1509
|
-
\`.gitignore\` is respected (yes), whether \`.env\` files go in (no: they are secrets). Ask;
|
|
1510
|
-
never assume.
|
|
1511
1531
|
3. **The tasks.** For each: \`title\` (required), \`prompt\` (required — what Copilot reads:
|
|
1512
1532
|
precise, with paths, ports and commands), \`expected\` (the bar, one sentence), \`checks\`
|
|
1513
1533
|
(the same bar written so the runner can decide it; write one wherever a command or a file can
|
|
@@ -1592,8 +1612,6 @@ ${table(['Field', 'Where', 'Required', 'What it is'], rows)}
|
|
|
1592
1612
|
|
|
1593
1613
|
${VCS_DETAIL_EN}
|
|
1594
1614
|
|
|
1595
|
-
${MIRROR_EN}
|
|
1596
|
-
|
|
1597
1615
|
${CHECKS_EN}
|
|
1598
1616
|
|
|
1599
1617
|
### A filled-in example
|
|
@@ -1727,10 +1745,6 @@ ${systemGuideSection('bg')}
|
|
|
1727
1745
|
работата и я оценява. Питай дали остава включен и на кой модел; модел, различен от
|
|
1728
1746
|
работния, хваща повече. \`model\` за самата работа — само ако потребителят назове такъв
|
|
1729
1747
|
от менюто на чата.
|
|
1730
|
-
- \`mirror\` (незадължително, изключено по подразбиране): дали файлове от проекта се копират
|
|
1731
|
-
и прикачат към първото съобщение като контекст — от кой корен, кои директории вътре и вън,
|
|
1732
|
-
дали се спазва \`.gitignore\` (да), дали влизат \`.env\` файлове (не: те са тайни). Питай;
|
|
1733
|
-
никога не предполагай.
|
|
1734
1748
|
3. **Задачите.** За всяка: \`title\` (задължително), \`prompt\` (задължително — това чете
|
|
1735
1749
|
Copilot: точно, с пътища, портове и команди), \`expected\` (летвата, едно изречение),
|
|
1736
1750
|
\`checks\` (същата летва, написана така, че runner-ът да я решава сам; пиши по една навсякъде,
|
|
@@ -1816,8 +1830,6 @@ ${table(['Поле', 'Къде', 'Задължително', 'Какво е'], r
|
|
|
1816
1830
|
|
|
1817
1831
|
${VCS_DETAIL_BG}
|
|
1818
1832
|
|
|
1819
|
-
${MIRROR_BG}
|
|
1820
|
-
|
|
1821
1833
|
${CHECKS_BG}
|
|
1822
1834
|
|
|
1823
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,15 +166,18 @@ 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();
|
|
178
|
+
// A branch to carry on, given alone, is that choice: see `VersionControl.startFrom`.
|
|
179
|
+
if (vcs.existingBranch?.trim())
|
|
180
|
+
vcs.startFrom = 'existing-branch';
|
|
172
181
|
const group = planned.conversationGroup.trim() || sharedGroup;
|
|
173
182
|
await store.updateSession(created.id, (s) => {
|
|
174
183
|
s.onFailure = planned.onFailure;
|
|
@@ -180,6 +189,8 @@ persona = '') {
|
|
|
180
189
|
s.model = (planned.model || defaultModel).trim() || undefined;
|
|
181
190
|
s.planName = plan.plan.trim() || undefined;
|
|
182
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);
|
|
183
194
|
});
|
|
184
195
|
const titles = [];
|
|
185
196
|
for (const task of planned.tasks) {
|
package/dist/src/plan/schema.js
CHANGED
|
@@ -44,16 +44,40 @@ const VcsInput = z
|
|
|
44
44
|
* Absent keeps the old behaviour, wherever the repository is. See `VersionControl.startFrom`.
|
|
45
45
|
*/
|
|
46
46
|
startFrom: z
|
|
47
|
-
.enum(['branch', 'previous-session', 'head'], {
|
|
48
|
-
error: 'startFrom must be "branch" (every session starts from the local branch in baseBranch, "main" unless said otherwise)
|
|
47
|
+
.enum(['branch', 'previous-session', 'head', 'existing-branch'], {
|
|
48
|
+
error: 'startFrom must be "branch" (every session starts from the local branch in baseBranch, "main" unless said otherwise), "previous-session" (each session carries on from the branch of the session before it in the same repository) or "existing-branch" (the session works on the existing branch named in existingBranch).',
|
|
49
49
|
})
|
|
50
50
|
.optional(),
|
|
51
51
|
/** The local branch `startFrom: "branch"` starts from, and `previous-session`'s fallback. */
|
|
52
52
|
baseBranch: z.string().trim().optional(),
|
|
53
|
+
/**
|
|
54
|
+
* The existing local branch the whole session works on, exactly as named: no new branch, no
|
|
55
|
+
* prefix. Given alone it means `startFrom: "existing-branch"`. See `VersionControl.startFrom`.
|
|
56
|
+
*/
|
|
57
|
+
existingBranch: z.string().trim().optional(),
|
|
58
|
+
/**
|
|
59
|
+
* Fetch and fast-forward the branch the session starts from before its first branch is cut.
|
|
60
|
+
* Absent means true. See `VersionControl.updateFromRemote`.
|
|
61
|
+
*/
|
|
62
|
+
updateFromRemote: z.boolean().optional(),
|
|
53
63
|
}, {
|
|
54
64
|
error: 'Every session needs a "vcs" object saying whether the runner does version control for it and, when it does, which git repository to work in. Ask the user both questions and write the answers here.',
|
|
55
65
|
})
|
|
56
66
|
.superRefine((vcs, ctx) => {
|
|
67
|
+
if (vcs.startFrom === 'existing-branch' && !vcs.existingBranch) {
|
|
68
|
+
ctx.addIssue({
|
|
69
|
+
code: 'custom',
|
|
70
|
+
path: ['existingBranch'],
|
|
71
|
+
message: 'startFrom is "existing-branch", so "existingBranch" must name the local branch to carry on, exactly as it is in git (for example "recovery/apz-migration").',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (vcs.existingBranch && vcs.startFrom && vcs.startFrom !== 'existing-branch') {
|
|
75
|
+
ctx.addIssue({
|
|
76
|
+
code: 'custom',
|
|
77
|
+
path: ['existingBranch'],
|
|
78
|
+
message: `"existingBranch" carries on a branch that exists, and startFrom "${vcs.startFrom}" cuts a new one. Keep one: startFrom "existing-branch" with existingBranch, or leave existingBranch out.`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
57
81
|
if (vcs.enabled && !vcs.repoDir.trim()) {
|
|
58
82
|
ctx.addIssue({
|
|
59
83
|
code: 'custom',
|
|
@@ -143,15 +167,12 @@ const TaskVcsInput = z.object({
|
|
|
143
167
|
branch: z.string().trim().optional(),
|
|
144
168
|
commitMessage: z.string().trim().optional(),
|
|
145
169
|
});
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
respectGitignore: z.boolean().optional(),
|
|
153
|
-
includeEnvFiles: z.boolean().optional(),
|
|
154
|
-
});
|
|
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();
|
|
155
176
|
const TaskInput = z.object({
|
|
156
177
|
title: z
|
|
157
178
|
.string()
|
|
@@ -247,7 +268,14 @@ const SessionInput = z.object({
|
|
|
247
268
|
conversationGroup: z.string().trim().default(''),
|
|
248
269
|
vcs: VcsInput,
|
|
249
270
|
review: ReviewInput.optional(),
|
|
250
|
-
|
|
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(),
|
|
251
279
|
tasks: z.array(TaskInput).min(1, 'A session with no tasks would import as an empty queue.'),
|
|
252
280
|
});
|
|
253
281
|
export const PlanSchema = z.object({
|
|
@@ -326,7 +354,6 @@ const KNOWN_KEYS = {
|
|
|
326
354
|
vcs: keysOf(VcsInput),
|
|
327
355
|
taskVcs: keysOf(TaskVcsInput),
|
|
328
356
|
review: keysOf(ReviewInput),
|
|
329
|
-
mirror: keysOf(MirrorInput),
|
|
330
357
|
};
|
|
331
358
|
function unknownKeys(value, kind, where, out) {
|
|
332
359
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -337,31 +364,46 @@ function unknownKeys(value, kind, where, out) {
|
|
|
337
364
|
}
|
|
338
365
|
}
|
|
339
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
|
+
}
|
|
340
379
|
/** Fields a model invented, walked by hand because the schema drops them silently. */
|
|
341
380
|
function collectWarnings(raw) {
|
|
342
381
|
const out = [];
|
|
343
|
-
|
|
382
|
+
const plan = fieldsOf(raw);
|
|
383
|
+
if (!plan)
|
|
344
384
|
return out;
|
|
345
|
-
unknownKeys(
|
|
346
|
-
|
|
347
|
-
if (!Array.isArray(sessions))
|
|
385
|
+
unknownKeys(plan, 'plan', 'plan', out);
|
|
386
|
+
if (!Array.isArray(plan.sessions))
|
|
348
387
|
return out;
|
|
349
|
-
sessions.forEach((s, i) => {
|
|
350
|
-
|
|
351
|
-
|
|
388
|
+
plan.sessions.forEach((s, i) => {
|
|
389
|
+
const session = fieldsOf(s);
|
|
390
|
+
if (!session)
|
|
391
|
+
return;
|
|
392
|
+
unknownKeys(session, 'session', `sessions[${i}]`, out);
|
|
352
393
|
unknownKeys(session.vcs, 'vcs', `sessions[${i}].vcs`, out);
|
|
353
394
|
unknownKeys(session.review, 'review', `sessions[${i}].review`, out);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
});
|
|
365
407
|
});
|
|
366
408
|
return out;
|
|
367
409
|
}
|
|
@@ -404,8 +446,25 @@ export function summarise(plan) {
|
|
|
404
446
|
*
|
|
405
447
|
* Never throws. Every way this can fail — not JSON at all, the wrong version, a task with no
|
|
406
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.
|
|
407
454
|
*/
|
|
408
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) {
|
|
409
468
|
const source = extractJson(text ?? '');
|
|
410
469
|
if (!source.trim()) {
|
|
411
470
|
return { ok: false, warnings: [], issues: [{ path: '', message: 'There is nothing here to import.' }] };
|