copilot-operator 0.1.17 → 0.1.19
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.service.js +33 -2
- package/dist/src/cli.js +11 -29
- package/dist/src/config/version.js +30 -0
- package/dist/src/orchestrator/derivedChecks.js +19 -1
- package/dist/src/orchestrator/review.js +2 -2
- package/dist/src/orchestrator/taskRunner.js +130 -10
- package/dist/src/plan/brief.js +38 -4
- package/dist/src/plan/importPlan.js +42 -1
- package/dist/src/plan/schema.js +53 -0
- package/dist/src/plan/systemGuide.js +50 -2
- package/dist/src/protocol/replySchema.js +1 -1
- package/dist/src/protocol/reviewSchema.js +10 -5
- package/dist/src/session/exports.js +53 -13
- package/dist/src/session/model.js +20 -0
- package/dist/src/session/store.js +80 -0
- package/dist/src/vcs/artifacts.js +185 -0
- package/dist/src/vcs/inputs.js +449 -0
- package/dist/src/vcs/snapshot.js +174 -22
- package/dist/src/vcs/taskVcs.js +272 -37
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/549-5d7c845fb0a27dbe.js +1 -0
- package/dist/web/_next/static/chunks/867-d9c54f7a2b285b3f.js +1 -0
- package/dist/web/_next/static/chunks/app/appearance/page-a18019cdc395de3d.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-a0bd59986f7c8beb.js +1 -0
- package/dist/web/_next/static/chunks/app/layout-3efbb282292ccc38.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-99f9153fd75078d0.js +1 -0
- package/dist/web/_next/static/chunks/app/system/page-5ae1f8054208e6eb.js +1 -0
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +25 -26
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +25 -26
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +25 -26
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +25 -26
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +25 -26
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +25 -26
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +25 -26
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +25 -26
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +25 -26
- package/package.json +8 -3
- package/prompts/level1.md +547 -546
- package/dist/web/_next/static/chunks/549-a13ae394c5e6a837.js +0 -1
- package/dist/web/_next/static/chunks/867-7f27ef819d251cc5.js +0 -1
- package/dist/web/_next/static/chunks/app/appearance/page-9b13583bfee63b32.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-d7314ac22d47a3ab.js +0 -1
- package/dist/web/_next/static/chunks/app/layout-560bd6d173302e5a.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-d28825430ca280de.js +0 -1
- package/dist/web/_next/static/chunks/app/system/page-3aa2a0fef5454e69.js +0 -1
- /package/dist/web/_next/static/{kkxFcGzA9Uo1uRAgcoDti → ofnRLEgWx9th5w_qtuFnB}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{kkxFcGzA9Uo1uRAgcoDti → ofnRLEgWx9th5w_qtuFnB}/_ssgManifest.js +0 -0
package/dist/src/plan/schema.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* sending the user back to the chat; it is reported as a warning instead.
|
|
14
14
|
*/
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
import { namesEverything } from '../vcs/inputs.js';
|
|
17
|
+
import { coveredByArtifacts } from '../vcs/artifacts.js';
|
|
16
18
|
export const PLAN_VERSION = 1;
|
|
17
19
|
/**
|
|
18
20
|
* Version control, exactly as a session stores it, and required on every session.
|
|
@@ -77,6 +79,35 @@ const VcsInput = z
|
|
|
77
79
|
.optional(),
|
|
78
80
|
})
|
|
79
81
|
.optional(),
|
|
82
|
+
/**
|
|
83
|
+
* The operator's input files. See `UserInputs`. Accepts the spelling a chat model may write:
|
|
84
|
+
* `capture` can only be "baseline-commit", `allowUntracked`/`allowIgnored` only true (an input
|
|
85
|
+
* may be either), `readOnlyAfterCapture` is `readOnly`.
|
|
86
|
+
*/
|
|
87
|
+
userInputs: z
|
|
88
|
+
.object({
|
|
89
|
+
enabled: z.boolean().optional(),
|
|
90
|
+
paths: z.array(z.string().trim()).default([]),
|
|
91
|
+
capture: z.literal('baseline-commit', { error: 'userInputs.capture can only be "baseline-commit": input files go into the commit the session starts from. Leave the field out.' }).optional(),
|
|
92
|
+
allowUntracked: z.literal(true, { error: 'userInputs.allowUntracked can only be true: an input file may be untracked. Leave the field out.' }).optional(),
|
|
93
|
+
allowIgnored: z.literal(true, { error: 'userInputs.allowIgnored can only be true: an input file may be ignored by git. Leave the field out.' }).optional(),
|
|
94
|
+
readOnly: z.boolean().optional(),
|
|
95
|
+
readOnlyAfterCapture: z.boolean().optional(),
|
|
96
|
+
requireApproval: z.boolean().optional(),
|
|
97
|
+
})
|
|
98
|
+
.optional(),
|
|
99
|
+
/**
|
|
100
|
+
* Evidence kept with the run and never committed. See `VersionControl.artifacts`. `commit` can
|
|
101
|
+
* only be false and `attachToRun`/`allowIgnored` only true: that is what an artifact is.
|
|
102
|
+
*/
|
|
103
|
+
artifacts: z
|
|
104
|
+
.object({
|
|
105
|
+
paths: z.array(z.string().trim()).default([]),
|
|
106
|
+
allowIgnored: z.literal(true, { error: 'artifacts.allowIgnored can only be true. Leave the field out.' }).optional(),
|
|
107
|
+
attachToRun: z.literal(true, { error: 'artifacts.attachToRun can only be true: artifacts are always kept with the run. Leave the field out.' }).optional(),
|
|
108
|
+
commit: z.literal(false, { error: 'artifacts.commit can only be false: an artifact is never committed. Files that should be committed are the work, not artifacts.' }).optional(),
|
|
109
|
+
})
|
|
110
|
+
.optional(),
|
|
80
111
|
}, {
|
|
81
112
|
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.',
|
|
82
113
|
})
|
|
@@ -102,6 +133,23 @@ const VcsInput = z
|
|
|
102
133
|
message: '"tracked-only-snapshot" takes tracked changes only, and includeUntracked true asks for new files too. Keep one: policy "snapshot", or leave includeUntracked out.',
|
|
103
134
|
});
|
|
104
135
|
}
|
|
136
|
+
for (const [i, p] of (vcs.userInputs?.paths ?? []).entries()) {
|
|
137
|
+
if (namesEverything(p)) {
|
|
138
|
+
ctx.addIssue({ code: 'custom', path: ['userInputs', 'paths', i], message: `"${p}" names the whole repository; an input pattern names the operator's files, such as "rules-engine/test-data/schemas/*.yaml".` });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (vcs.userInputs && vcs.userInputs.enabled !== false && (vcs.userInputs.paths ?? []).length > 0 && !vcs.enabled) {
|
|
142
|
+
ctx.addIssue({ code: 'custom', path: ['userInputs'], message: 'userInputs needs version control on: the input files go into the commit the session starts from. Turn vcs.enabled on, or leave userInputs out.' });
|
|
143
|
+
}
|
|
144
|
+
const hidden = coveredByArtifacts(vcs.userInputs?.paths ?? [], vcs.artifacts?.paths ?? []);
|
|
145
|
+
if (hidden.length > 0) {
|
|
146
|
+
ctx.addIssue({
|
|
147
|
+
code: 'custom',
|
|
148
|
+
path: ['artifacts', 'paths'],
|
|
149
|
+
message: `An artifacts pattern covers the input files ${hidden.map((h) => `"${h}"`).join(', ')}: artifacts are kept out of git, so the inputs would be too. ` +
|
|
150
|
+
'Name only the folders the evidence is written to (for example "rules-engine/test-results/**"), not a whole project folder.',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
105
153
|
if (vcs.enabled && !vcs.repoDir.trim()) {
|
|
106
154
|
ctx.addIssue({
|
|
107
155
|
code: 'custom',
|
|
@@ -259,6 +307,11 @@ const TaskInput = z.object({
|
|
|
259
307
|
* chat told — which a sentence in the prompt cannot be. See `vcs/scope.ts`.
|
|
260
308
|
*/
|
|
261
309
|
scope: z.array(z.string().trim().min(1, 'A scope entry needs a path or a pattern.')).default([]),
|
|
310
|
+
/**
|
|
311
|
+
* Files the task is to produce as evidence (reports, archives), repository-relative patterns.
|
|
312
|
+
* Kept with the run's record when the task creates or changes them. See `Task.outputs`.
|
|
313
|
+
*/
|
|
314
|
+
outputs: z.array(z.string().trim().min(1, 'An outputs entry needs a path or a pattern.')).default([]),
|
|
262
315
|
/**
|
|
263
316
|
* What earlier attempts of this task were asked, written by the plan export for a task that has
|
|
264
317
|
* been run more than once (`earlierPlans` in `session/exports.ts`). History, not instruction:
|
|
@@ -1213,8 +1213,8 @@ export const SYSTEM_GUIDE = [
|
|
|
1213
1213
|
kind: 'note',
|
|
1214
1214
|
en: 'Uncommitted changes',
|
|
1215
1215
|
bg: 'Некомитнати промени',
|
|
1216
|
-
doesEn: 'What the first task does when the repository has uncommitted changes:
|
|
1217
|
-
doesBg: 'Какво прави първата задача, когато хранилището има некомитнати промени:
|
|
1216
|
+
doesEn: 'What the first task does when the repository has uncommitted changes: refuse to start until they are committed, or take them — all, or tracked only — as a starting snapshot: one commit on a branch of its own that the session starts from. In a plan: "dirtyWorktree": { "policy": "snapshot" }. Not shown for "Carry on an existing branch".',
|
|
1217
|
+
doesBg: 'Какво прави първата задача, когато хранилището има некомитнати промени: отказва старта, докато не са комитнати, или ги взема — всички или само tracked — като начална снимка: един комит на отделен клон, от който сесията тръгва. В план: "dirtyWorktree": { "policy": "snapshot" }. Не се показва при „Продължи съществуващ клон“.',
|
|
1218
1218
|
},
|
|
1219
1219
|
{
|
|
1220
1220
|
key: 'vcs.snapshotTake',
|
|
@@ -1224,6 +1224,46 @@ export const SYSTEM_GUIDE = [
|
|
|
1224
1224
|
doesEn: 'Under the list of uncommitted files, each set to "Include in the starting snapshot" or "Leave out of the run": commits the included ones on the snapshot branch; left-out files go to the repository\'s .git/info/exclude. Until it is pressed a run of the session is refused and nothing changes.',
|
|
1225
1225
|
doesBg: 'Под списъка с некомитнати файлове, всеки с „Включи в началната снимка“ или „Остави извън изпълнението“: комитва включените на клона на снимката; оставените извън отиват в .git/info/exclude на хранилището. Докато не е натиснат, пускане на сесията се отказва и нищо не се променя.',
|
|
1226
1226
|
},
|
|
1227
|
+
{
|
|
1228
|
+
key: 'vcs.inputs',
|
|
1229
|
+
kind: 'note',
|
|
1230
|
+
en: 'Input files',
|
|
1231
|
+
bg: 'Входни файлове',
|
|
1232
|
+
doesEn: 'The heading over the operator\'s input files and whether they are read-only.',
|
|
1233
|
+
doesBg: 'Заглавието над входните файлове на оператора и дали са само за четене.',
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
key: 'vcs.inputsField',
|
|
1237
|
+
kind: 'field',
|
|
1238
|
+
en: 'Your input files, one pattern per line (repository-relative)',
|
|
1239
|
+
bg: 'Твоите входни файлове, по един шаблон на ред (спрямо хранилището)',
|
|
1240
|
+
doesEn: 'Files the operator provides — schemas, test data. They go into the commit every session of the run starts from (new or ignored ones in the starting snapshot, listed for approval); a later session whose start lacks them has them committed on top of it; their SHA-256 sums are recorded. In a plan: "userInputs": { "paths": [...] }.',
|
|
1241
|
+
doesBg: 'Файлове, които операторът дава — схеми, тестови данни. Влизат в комита, от който тръгва всяка сесия на пускането (новите или ignored — в началната снимка, със списък за одобрение); следваща сесия, чието начало ги няма, ги получава комитнати върху него; SHA-256 сумите им се записват. В план: "userInputs": { "paths": [...] }.',
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
key: 'vcs.inputsReadOnly',
|
|
1245
|
+
kind: 'checkbox',
|
|
1246
|
+
en: 'Read-only while the tasks run',
|
|
1247
|
+
bg: 'Само за четене, докато задачите работят',
|
|
1248
|
+
doesEn: 'On by default. A changed, deleted or added input file is put back after every round and the chat told; one that cannot be put back fails the task.',
|
|
1249
|
+
doesBg: 'Включено по подразбиране. Променен, изтрит или добавен входен файл се връща след всеки рунд и чатът научава; такъв, който не може да се върне, проваля задачата.',
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
key: 'vcs.artifacts',
|
|
1253
|
+
kind: 'note',
|
|
1254
|
+
en: 'Artifacts',
|
|
1255
|
+
bg: 'Artifacts',
|
|
1256
|
+
doesEn: 'The heading over the evidence files the work writes.',
|
|
1257
|
+
doesBg: 'Заглавието над файловете с доказателства, които работата пише.',
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
key: 'vcs.artifactsField',
|
|
1261
|
+
kind: 'field',
|
|
1262
|
+
en: 'Evidence the work writes, one pattern per line (repository-relative)',
|
|
1263
|
+
bg: 'Доказателства, които работата пише, по един шаблон на ред (спрямо хранилището)',
|
|
1264
|
+
doesEn: 'Reports, ZIP archives, test results: never committed (kept out through .git/info/exclude, .gitignore untouched) and copied into each attempt\'s record with their sums. In a plan: "artifacts": { "paths": [...] }.',
|
|
1265
|
+
doesBg: 'Отчети, ZIP архиви, резултати от тестове: никога не се комитват (стоят извън git чрез .git/info/exclude, .gitignore не се пипа) и се копират към записа на всеки опит със сумите си. В план: "artifacts": { "paths": [...] }.',
|
|
1266
|
+
},
|
|
1227
1267
|
{
|
|
1228
1268
|
key: 'vcs.ready',
|
|
1229
1269
|
kind: 'note',
|
|
@@ -3201,6 +3241,14 @@ export const SYSTEM_GUIDE = [
|
|
|
3201
3241
|
en: 'This machine',
|
|
3202
3242
|
bg: 'Тази машина',
|
|
3203
3243
|
controls: [
|
|
3244
|
+
{
|
|
3245
|
+
key: 'sys.version',
|
|
3246
|
+
kind: 'note',
|
|
3247
|
+
en: 'copilot-operator version',
|
|
3248
|
+
bg: 'Версия на copilot-operator',
|
|
3249
|
+
doesEn: 'The first row: which version of the bot is running, as `cop --version` says. Name it when reporting a problem.',
|
|
3250
|
+
doesBg: 'Първият ред: коя версия на бота работи, както я казва `cop --version`. Кажи я, когато съобщаваш за проблем.',
|
|
3251
|
+
},
|
|
3204
3252
|
{
|
|
3205
3253
|
key: 'sys.signin',
|
|
3206
3254
|
kind: 'note',
|
|
@@ -83,7 +83,7 @@ export const DeviationSchema = z.object({
|
|
|
83
83
|
* claim to test, and to the record. The finding is named by the id the runner gave it.
|
|
84
84
|
*/
|
|
85
85
|
export const DisputeSchema = z.object({
|
|
86
|
-
/** The finding's id as it was given: `r1f2
|
|
86
|
+
/** The finding's id as it was given: `r1f2`, or `a2r1f2` on a later attempt. */
|
|
87
87
|
finding: z.string().trim().min(1),
|
|
88
88
|
/** Why it is wrong. */
|
|
89
89
|
why: z.string().trim().min(1),
|
|
@@ -166,13 +166,18 @@ export function isGrounded(basis, sources) {
|
|
|
166
166
|
return sources.some((s) => groundingKey(s).includes(key));
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
|
-
* The name a finding is referred to by, once the runner has it: round and position
|
|
169
|
+
* The name a finding is referred to by, once the runner has it: round and position, and from the
|
|
170
|
+
* task's second attempt on the attempt as well — `r1f2`, then `a2r1f2`.
|
|
170
171
|
*
|
|
171
|
-
* Given by the runner rather than the reviewer so that it is unique
|
|
172
|
-
*
|
|
172
|
+
* Given by the runner rather than the reviewer so that it is unique and cannot be forgotten. It is
|
|
173
|
+
* what the implementer names in `disputed`, what a check given with the finding is named after, and
|
|
174
|
+
* what the record shows. Unique across the task's attempts, not only its rounds, because those checks
|
|
175
|
+
* are kept for every later attempt (`Task.reviewChecks`) and the rounds count from 1 on each: with
|
|
176
|
+
* round and position alone, attempt 2's first finding was `r1f1` beside attempt 1's kept `r1f1`, and
|
|
177
|
+
* a dispute of the one suspended the check of the other. The first attempt keeps the short form.
|
|
173
178
|
*/
|
|
174
|
-
export function findingId(round, index) {
|
|
175
|
-
return `r${round}f${index + 1}`;
|
|
179
|
+
export function findingId(round, index, attempt = 1) {
|
|
180
|
+
return `${attempt > 1 ? `a${attempt}` : ''}r${round}f${index + 1}`;
|
|
176
181
|
}
|
|
177
182
|
/** True when nothing the reviewer found is something the implementer could fix. */
|
|
178
183
|
export function allAboutTheTask(findings) {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
import { existsSync } from 'node:fs';
|
|
22
22
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
+
import { TASK_FIELD_ON_RERUN } from './store.js';
|
|
25
|
+
import { botVersion } from '../config/version.js';
|
|
24
26
|
import { withoutPersona } from '../plan/importPlan.js';
|
|
25
27
|
const EXPECTED_HEADER = '### Expected result';
|
|
26
28
|
const GOAL_HEADER = '## Goal of this session';
|
|
@@ -149,6 +151,8 @@ export function buildPlanExport(scope) {
|
|
|
149
151
|
...(s.vcs?.updateFromRemote === false ? { updateFromRemote: false } : {}),
|
|
150
152
|
// Only when chosen: absent is reject, as it always was.
|
|
151
153
|
...(s.vcs?.dirtyWorktree && s.vcs.dirtyWorktree.policy !== 'reject' ? { dirtyWorktree: { ...s.vcs.dirtyWorktree } } : {}),
|
|
154
|
+
...(s.vcs?.userInputs?.paths.length ? { userInputs: { ...s.vcs.userInputs } } : {}),
|
|
155
|
+
...(s.vcs?.artifacts?.paths.length ? { artifacts: { ...s.vcs.artifacts } } : {}),
|
|
152
156
|
},
|
|
153
157
|
review: { enabled: s.review?.enabled !== false, model: s.review?.model ?? '' },
|
|
154
158
|
...(s.projectDir ? { projectDir: s.projectDir } : {}),
|
|
@@ -168,6 +172,8 @@ export function buildPlanExport(scope) {
|
|
|
168
172
|
task.readOnly = true;
|
|
169
173
|
if (t.scope && t.scope.length > 0)
|
|
170
174
|
task.scope = [...t.scope];
|
|
175
|
+
if (t.outputs && t.outputs.length > 0)
|
|
176
|
+
task.outputs = [...t.outputs];
|
|
171
177
|
const earlier = earlierPlans(t, level2);
|
|
172
178
|
if (earlier.length > 0)
|
|
173
179
|
task.earlierAttempts = earlier;
|
|
@@ -231,27 +237,32 @@ export function taskAtAttempt(task, attempt) {
|
|
|
231
237
|
const a = task.attempts?.[attempt - 1];
|
|
232
238
|
if (!a)
|
|
233
239
|
return null;
|
|
240
|
+
/*
|
|
241
|
+
* By the same table `rerunTask` follows: what was archived is read from the attempt, what was
|
|
242
|
+
* cleared is absent. Field by field, by hand, an earlier attempt read back with the latest one's
|
|
243
|
+
* handoff, counts, limit and fresh-chat mark.
|
|
244
|
+
*/
|
|
245
|
+
const asItWas = {};
|
|
246
|
+
for (const [key, what] of Object.entries(TASK_FIELD_ON_RERUN)) {
|
|
247
|
+
if (what === 'archived')
|
|
248
|
+
asItWas[key] = a[key];
|
|
249
|
+
if (what === 'cleared')
|
|
250
|
+
asItWas[key] = undefined;
|
|
251
|
+
}
|
|
234
252
|
return {
|
|
235
253
|
...task,
|
|
254
|
+
...asItWas,
|
|
236
255
|
attempt,
|
|
237
256
|
status: a.status,
|
|
238
|
-
runId: a.runId,
|
|
239
|
-
runGroup: a.runGroup,
|
|
240
|
-
startedAt: a.startedAt,
|
|
241
|
-
finishedAt: a.finishedAt,
|
|
242
257
|
iterations: a.iterations,
|
|
243
|
-
|
|
244
|
-
reason: a.reason,
|
|
245
|
-
deviations: a.deviations,
|
|
246
|
-
disputes: a.disputes,
|
|
258
|
+
// The task's own text and definition as they were when that attempt ran, where it was kept.
|
|
247
259
|
title: a.title,
|
|
248
260
|
prompt: a.prompt,
|
|
249
261
|
level2: a.level2,
|
|
250
262
|
checks: a.checks ?? task.checks,
|
|
251
263
|
vcsPlan: a.vcsPlan ?? task.vcsPlan,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
vcs: a.vcs,
|
|
264
|
+
scope: a.scope ?? task.scope,
|
|
265
|
+
readOnly: a.readOnly ?? task.readOnly,
|
|
255
266
|
attempts: task.attempts?.slice(0, attempt - 1),
|
|
256
267
|
continuing: undefined,
|
|
257
268
|
buildsOn: undefined,
|
|
@@ -436,7 +447,25 @@ async function domainTask(session, task, runsDir) {
|
|
|
436
447
|
const started = task.startedAt ? Date.parse(task.startedAt) : NaN;
|
|
437
448
|
const finished = task.finishedAt ? Date.parse(task.finishedAt) : NaN;
|
|
438
449
|
const out = {
|
|
439
|
-
|
|
450
|
+
/*
|
|
451
|
+
* Where the session started — the baseline commit, a snapshot of the operator's changes, the
|
|
452
|
+
* input files with their SHA-256 sums — so a reader can check the work against its inputs.
|
|
453
|
+
*/
|
|
454
|
+
session: {
|
|
455
|
+
id: session.id,
|
|
456
|
+
name: session.name,
|
|
457
|
+
...(session.vcsStart
|
|
458
|
+
? {
|
|
459
|
+
start: {
|
|
460
|
+
kind: session.vcsStart.kind,
|
|
461
|
+
commit: session.vcsStart.commit,
|
|
462
|
+
branch: session.vcsStart.branch,
|
|
463
|
+
...(session.vcsStart.snapshot ? { snapshot: session.vcsStart.snapshot } : {}),
|
|
464
|
+
...(session.vcsStart.inputs ? { inputs: session.vcsStart.inputs } : {}),
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
: {}),
|
|
468
|
+
},
|
|
440
469
|
task: {
|
|
441
470
|
id: task.id,
|
|
442
471
|
title: task.title,
|
|
@@ -444,6 +473,8 @@ async function domainTask(session, task, runsDir) {
|
|
|
444
473
|
readOnly: task.readOnly ?? false,
|
|
445
474
|
scope: task.scope ?? [],
|
|
446
475
|
scopeReverted: task.scopeReverted ?? [],
|
|
476
|
+
inputsRestored: task.inputsRestored ?? [],
|
|
477
|
+
artifactsKept: task.artifactsKept ?? [],
|
|
447
478
|
handoff: task.handoff ?? null,
|
|
448
479
|
stopCode: task.stopCode ?? null,
|
|
449
480
|
prompt: p.prompt,
|
|
@@ -526,7 +557,7 @@ async function domainTask(session, task, runsDir) {
|
|
|
526
557
|
whatTheChatTried: aRounds,
|
|
527
558
|
whatWasActuallyDone: { repository: a.vcs, deviations: a.deviations, disputes: a.disputes },
|
|
528
559
|
whyItFailed: aFailed
|
|
529
|
-
? { status: a.status, reason: a.reason, failingChecks: (a.checkResults ?? []).filter((c) => !c.passed), lastRound: aRounds.at(-1) }
|
|
560
|
+
? { status: a.status, reason: a.reason, failingChecks: (a.checkResults ?? []).filter((c) => !c.passed), lastRound: aRounds.at(-1), leftovers: a.leftovers }
|
|
530
561
|
: undefined,
|
|
531
562
|
});
|
|
532
563
|
}
|
|
@@ -550,6 +581,8 @@ export async function buildDomainExport(scope, runsDir) {
|
|
|
550
581
|
}
|
|
551
582
|
return {
|
|
552
583
|
exportedAt: new Date().toISOString(),
|
|
584
|
+
// Which copilot-operator made this, so a report from another machine says it without being asked.
|
|
585
|
+
botVersion: botVersion(),
|
|
553
586
|
about: `copilot-operator, the work: ${scope.label}. What each task asked, what the chat tried, what was done to the repository, and why a task did not end done. Nothing about the runner itself; that is the bot export.`,
|
|
554
587
|
counts,
|
|
555
588
|
tasks,
|
|
@@ -607,6 +640,9 @@ async function botTask(session, task, runsDir) {
|
|
|
607
640
|
steps: stepsOf(aEvents).map((s) => ({ iteration: s.iteration, id: s.id, command: s.description, outcome: s.outcome, exitCode: s.exitCode, durationMs: s.durationMs, refused: s.refused })),
|
|
608
641
|
problems: aEvents.filter((e) => e.level === 'error' || e.level === 'warn').map(trimmed),
|
|
609
642
|
events: aEvents.map(trimmed),
|
|
643
|
+
// That attempt's own: what it left running and the machine it ran on.
|
|
644
|
+
processes: { leftovers: a.leftovers },
|
|
645
|
+
environment: a.environment,
|
|
610
646
|
};
|
|
611
647
|
})),
|
|
612
648
|
};
|
|
@@ -618,6 +654,8 @@ export async function buildBotExport(scope, runsDir, machine) {
|
|
|
618
654
|
tasks.push(await botTask(session, task, runsDir));
|
|
619
655
|
return {
|
|
620
656
|
exportedAt: new Date().toISOString(),
|
|
657
|
+
// Which copilot-operator made this, so a report from another machine says it without being asked.
|
|
658
|
+
botVersion: botVersion(),
|
|
621
659
|
about: `copilot-operator, the runner: ${scope.label}. The environment, every transcript event, every step with its exit code, the transport's retries, what was reaped, what the review machinery did. Read the domain export for what the task was about.`,
|
|
622
660
|
machine,
|
|
623
661
|
tasks,
|
|
@@ -649,6 +687,8 @@ export async function buildBundleExport(scope, runsDir, machine) {
|
|
|
649
687
|
const pairs = tasksOf(scope);
|
|
650
688
|
return {
|
|
651
689
|
exportedAt: new Date().toISOString(),
|
|
690
|
+
// Which copilot-operator made this, so a report from another machine says it without being asked.
|
|
691
|
+
botVersion: botVersion(),
|
|
652
692
|
about: `copilot-operator, all three views of ${pairs.length} chosen task(s): ${scope.label}. ` +
|
|
653
693
|
'`plan` is what was asked, in the format that imports again; `work` is what happened to it; ' +
|
|
654
694
|
'`runner` is what the machine did. Read `work` first, then `runner` when the machine is the suspect.',
|
|
@@ -21,6 +21,26 @@ export function tidyVcsPlan(plan) {
|
|
|
21
21
|
export function isContinuable(t) {
|
|
22
22
|
return t.status === 'limit-reached' || t.status === 'aborted' || (!!t.limit && (t.status === 'failed' || t.status === 'blocked'));
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* How many times the runner ran the task again in a fresh conversation, on its own, in the run that
|
|
26
|
+
* produced its latest attempt (`limits.retryBlockedInFreshChat`): the attempts at the end of its
|
|
27
|
+
* history that it marked `freshRetry`, counted back to the one the operator started. A row that says
|
|
28
|
+
* "retried in a fresh chat, then done" means the chat was the cause; "still blocked after 2" means
|
|
29
|
+
* it was not.
|
|
30
|
+
*
|
|
31
|
+
* Derived, never stored. It was a counter on the task that the runner added to and nothing reset,
|
|
32
|
+
* so every Run again or Continue of a task that blocked added the run's retries to the last run's:
|
|
33
|
+
* the register said 2, then 4, then 6, while each run had retried twice.
|
|
34
|
+
*/
|
|
35
|
+
export function freshRetriesOfLatestRun(task) {
|
|
36
|
+
if (!task.freshRetry)
|
|
37
|
+
return 0;
|
|
38
|
+
let n = 1;
|
|
39
|
+
const before = task.attempts ?? [];
|
|
40
|
+
for (let i = before.length - 1; i >= 0 && before[i]?.freshRetry; i -= 1)
|
|
41
|
+
n += 1;
|
|
42
|
+
return n;
|
|
43
|
+
}
|
|
24
44
|
export function newId(prefix = '') {
|
|
25
45
|
const now = new Date();
|
|
26
46
|
const p = (n) => String(n).padStart(2, '0');
|
|
@@ -50,6 +50,67 @@ export function bySessionList(a, b) {
|
|
|
50
50
|
return byTime;
|
|
51
51
|
return b.id.localeCompare(a.id);
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* What `rerunTask` does with every field of a task when a new attempt begins — Run again, Continue,
|
|
55
|
+
* a new prompt, the runner's own fresh-chat retry.
|
|
56
|
+
*
|
|
57
|
+
* archived belongs to the attempt that ended: copied onto its `TaskAttempt`, cleared on the task
|
|
58
|
+
* cleared belongs to that attempt and is not kept on the record: cleared
|
|
59
|
+
* reset given the new attempt's starting value (status queued, iterations 0, attempt + 1, …)
|
|
60
|
+
* kept the task's own, across attempts: its definition, or something every attempt inherits
|
|
61
|
+
*
|
|
62
|
+
* A record over every key of `Task`, so a field added to the task without a decision here does not
|
|
63
|
+
* compile. Fields that were left out of `rerunTask` by default were counted on by the next attempt:
|
|
64
|
+
* the fresh-chat retry count said 2, then 4, then 6 while each run had retried twice, and what an
|
|
65
|
+
* attempt left running was listed under every attempt after it. `npm run check:attempts` holds
|
|
66
|
+
* `rerunTask` to this table.
|
|
67
|
+
*/
|
|
68
|
+
export const TASK_FIELD_ON_RERUN = {
|
|
69
|
+
id: 'kept',
|
|
70
|
+
title: 'kept',
|
|
71
|
+
level2: 'kept',
|
|
72
|
+
prompt: 'kept',
|
|
73
|
+
createdAt: 'kept',
|
|
74
|
+
checks: 'kept',
|
|
75
|
+
vcsPlan: 'kept',
|
|
76
|
+
reviewEnabled: 'kept',
|
|
77
|
+
readOnly: 'kept',
|
|
78
|
+
scope: 'kept',
|
|
79
|
+
outputs: 'kept',
|
|
80
|
+
// What one review noticed is arithmetic for every attempt after it.
|
|
81
|
+
reviewChecks: 'kept',
|
|
82
|
+
attempts: 'kept',
|
|
83
|
+
status: 'reset',
|
|
84
|
+
iterations: 'reset',
|
|
85
|
+
attempt: 'reset',
|
|
86
|
+
continuing: 'reset',
|
|
87
|
+
buildsOn: 'reset',
|
|
88
|
+
runId: 'archived',
|
|
89
|
+
runGroup: 'archived',
|
|
90
|
+
startedAt: 'archived',
|
|
91
|
+
finishedAt: 'archived',
|
|
92
|
+
summary: 'archived',
|
|
93
|
+
reason: 'archived',
|
|
94
|
+
deviations: 'archived',
|
|
95
|
+
disputes: 'archived',
|
|
96
|
+
checkResults: 'archived',
|
|
97
|
+
review: 'archived',
|
|
98
|
+
stats: 'archived',
|
|
99
|
+
freshRetry: 'archived',
|
|
100
|
+
stopCode: 'archived',
|
|
101
|
+
limit: 'archived',
|
|
102
|
+
scopeReverted: 'archived',
|
|
103
|
+
inputsRestored: 'archived',
|
|
104
|
+
artifactsKept: 'archived',
|
|
105
|
+
vcs: 'archived',
|
|
106
|
+
leftovers: 'archived',
|
|
107
|
+
environment: 'archived',
|
|
108
|
+
handoff: 'archived',
|
|
109
|
+
interruption: 'cleared',
|
|
110
|
+
finalReply: 'cleared',
|
|
111
|
+
firstMessage: 'cleared',
|
|
112
|
+
logFile: 'cleared',
|
|
113
|
+
};
|
|
53
114
|
export const DEFAULT_VCS = {
|
|
54
115
|
enabled: true,
|
|
55
116
|
repoDir: '',
|
|
@@ -408,6 +469,7 @@ export class SessionStore {
|
|
|
408
469
|
...(input.readOnly === true ? { readOnly: true } : {}),
|
|
409
470
|
// Stored only when it limits something: absent means anywhere, as before scopes existed.
|
|
410
471
|
...((input.scope ?? []).filter((p) => p.trim()).length > 0 ? { scope: (input.scope ?? []).map((p) => p.trim()).filter(Boolean) } : {}),
|
|
472
|
+
...((input.outputs ?? []).filter((p) => p.trim()).length > 0 ? { outputs: (input.outputs ?? []).map((p) => p.trim()).filter(Boolean) } : {}),
|
|
411
473
|
};
|
|
412
474
|
await this.updateSession(sessionId, (s) => {
|
|
413
475
|
s.tasks.push(task);
|
|
@@ -476,6 +538,9 @@ export class SessionStore {
|
|
|
476
538
|
...(current.interruption ? { interruption: current.interruption } : {}),
|
|
477
539
|
});
|
|
478
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* What a new attempt does with each field of a task — see `TASK_FIELD_ON_RERUN`.
|
|
543
|
+
*/
|
|
479
544
|
async rerunTask(sessionId, taskId, patch = {},
|
|
480
545
|
/** Present when the new attempt carries on from the last one (see `continueTask`). */
|
|
481
546
|
continuing,
|
|
@@ -495,6 +560,8 @@ export class SessionStore {
|
|
|
495
560
|
{
|
|
496
561
|
runId: t.runId,
|
|
497
562
|
runGroup: t.runGroup,
|
|
563
|
+
leftovers: t.leftovers,
|
|
564
|
+
environment: t.environment,
|
|
498
565
|
status: t.status,
|
|
499
566
|
startedAt: t.startedAt,
|
|
500
567
|
finishedAt: t.finishedAt,
|
|
@@ -518,7 +585,11 @@ export class SessionStore {
|
|
|
518
585
|
stopCode: t.stopCode,
|
|
519
586
|
limit: t.limit,
|
|
520
587
|
scope: t.scope,
|
|
588
|
+
readOnly: t.readOnly,
|
|
521
589
|
scopeReverted: t.scopeReverted,
|
|
590
|
+
handoff: t.handoff,
|
|
591
|
+
inputsRestored: t.inputsRestored,
|
|
592
|
+
artifactsKept: t.artifactsKept,
|
|
522
593
|
vcs: t.vcs,
|
|
523
594
|
},
|
|
524
595
|
];
|
|
@@ -553,9 +624,15 @@ export class SessionStore {
|
|
|
553
624
|
t.stats = undefined;
|
|
554
625
|
t.handoff = undefined;
|
|
555
626
|
t.scopeReverted = undefined;
|
|
627
|
+
t.inputsRestored = undefined;
|
|
628
|
+
t.artifactsKept = undefined;
|
|
556
629
|
t.freshRetry = undefined;
|
|
557
630
|
t.stopCode = undefined;
|
|
558
631
|
t.limit = undefined;
|
|
632
|
+
// What this attempt left running and the machine it ran on are this attempt's, as the rest:
|
|
633
|
+
// left in place, the next attempt's list was added to this one's and read as its own.
|
|
634
|
+
t.leftovers = undefined;
|
|
635
|
+
t.environment = undefined;
|
|
559
636
|
// The branch of the finished attempt stays in the repository and stays on the record
|
|
560
637
|
// above; the next attempt gets its own, cut from the same commit this one started at.
|
|
561
638
|
t.vcs = undefined;
|
|
@@ -618,6 +695,9 @@ export class SessionStore {
|
|
|
618
695
|
// Sessions written before the queue could be told what to do on a failure behave the
|
|
619
696
|
// way they always did: the chain stops.
|
|
620
697
|
s.onFailure ??= 'stop';
|
|
698
|
+
// A counter that added up across runs, written before it was derived (`freshRetriesOfLatestRun`).
|
|
699
|
+
for (const t of s.tasks ?? [])
|
|
700
|
+
delete t.autoRetries;
|
|
621
701
|
return s;
|
|
622
702
|
}
|
|
623
703
|
catch {
|