copilot-operator 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/api/operator.controller.js +23 -0
- package/dist/src/api/operator.service.js +55 -3
- package/dist/src/orchestrator/derivedChecks.js +19 -1
- package/dist/src/orchestrator/review.js +2 -2
- package/dist/src/orchestrator/taskRunner.js +81 -8
- package/dist/src/plan/brief.js +60 -0
- package/dist/src/plan/importPlan.js +35 -1
- package/dist/src/plan/schema.js +62 -0
- package/dist/src/plan/systemGuide.js +72 -0
- package/dist/src/protocol/replySchema.js +1 -1
- package/dist/src/protocol/reviewSchema.js +10 -5
- package/dist/src/session/exports.js +46 -13
- package/dist/src/session/model.js +20 -0
- package/dist/src/session/store.js +146 -6
- package/dist/src/vcs/artifacts.js +138 -0
- package/dist/src/vcs/inputs.js +332 -0
- package/dist/src/vcs/snapshot.js +395 -0
- package/dist/src/vcs/taskVcs.js +183 -27
- 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-c078b5b504bf4289.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-a0bd59986f7c8beb.js +1 -0
- package/dist/web/_next/static/chunks/app/page-409e37e208dc12e5.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-27cb2d544b498d92.js +1 -0
- package/dist/web/_next/static/css/{e2a2c9eb3c226313.css → c37978b44194c37b.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 +9 -3
- package/prompts/level1.md +547 -546
- package/dist/web/_next/static/chunks/549-7fd4fe96682299f5.js +0 -1
- package/dist/web/_next/static/chunks/867-631dc4314840fed6.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-d7314ac22d47a3ab.js +0 -1
- package/dist/web/_next/static/chunks/app/page-13572a726de0b7db.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-886f5fc99e146408.js +0 -1
- /package/dist/web/_next/static/{GhdY7nVcYi-WV3j7saVPK → JPmlfAEvbNkbghoyvY9uC}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{GhdY7nVcYi-WV3j7saVPK → JPmlfAEvbNkbghoyvY9uC}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operator's input files: in every session's starting commit, and not the bot's to change.
|
|
3
|
+
*
|
|
4
|
+
* Found in a real run: YAML schemas the work was built from were put into the project by hand and
|
|
5
|
+
* left untracked. Branches did not carry them predictably, a task could not tell "input from the
|
|
6
|
+
* operator" from "change made by the bot", and a session started from `main` began without them
|
|
7
|
+
* although they were in other commits. `vcs.userInputs` names them, and three things follow:
|
|
8
|
+
*
|
|
9
|
+
* - Before a session's first task they are in the commit it starts from. New, changed or ignored
|
|
10
|
+
* ones go into the starting snapshot (`snapshot.ts`), with the operator's approval. A session
|
|
11
|
+
* whose start lacks them — `startFrom: "branch"` after a session that captured them — has them
|
|
12
|
+
* committed on top of that start, "Capture user-provided inputs" on `<prefix>input/<session id>`,
|
|
13
|
+
* from where they were last committed: the repository's HEAD, else the latest session in the same
|
|
14
|
+
* repository that recorded them. Every session of a run sees the same inputs.
|
|
15
|
+
* - Their SHA-256 sums are recorded on the session's start, for the record and the exports.
|
|
16
|
+
* - Read-only, unless switched off: after every round of steps a changed, removed or added input
|
|
17
|
+
* file is put back from the task's starting commit and the chat is told; a task that ends with
|
|
18
|
+
* one that could not be put back fails.
|
|
19
|
+
*
|
|
20
|
+
* The commit on top of a start is made without touching the working tree or the index (a temporary
|
|
21
|
+
* index, `commit-tree`), so the operator's checkout is never disturbed to make it.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import { rm, stat } from 'node:fs/promises';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join, resolve, sep } from 'node:path';
|
|
27
|
+
import { looksGenerated } from './commitHygiene.js';
|
|
28
|
+
import { branchNameFrom, freeBranchName, git, gitBytes, isValidBranchName, RUNNER_EMAIL } from './git.js';
|
|
29
|
+
import { inScope } from './scope.js';
|
|
30
|
+
/** The session's input settings with defaults filled in, or null when it names no inputs. */
|
|
31
|
+
export function inputSettings(vcs) {
|
|
32
|
+
const patterns = normalisePatterns(vcs?.userInputs?.paths ?? []);
|
|
33
|
+
if (patterns.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
return { paths: patterns, patterns, readOnly: vcs?.userInputs?.readOnly !== false, requireApproval: vcs?.userInputs?.requireApproval !== false };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Patterns as the matching expects them, without the ones that name everything: an input is a file
|
|
39
|
+
* the operator can point at, and "the whole repository" would make every change the bot makes a
|
|
40
|
+
* change to an input.
|
|
41
|
+
*/
|
|
42
|
+
export function normalisePatterns(paths) {
|
|
43
|
+
const out = new Set();
|
|
44
|
+
for (const raw of paths) {
|
|
45
|
+
const p = raw.trim().replace(/\\/g, '/').replace(/^\.?\/+/, '');
|
|
46
|
+
if (!p || p === '.' || /^[*/]+$/.test(p))
|
|
47
|
+
continue;
|
|
48
|
+
out.add(p);
|
|
49
|
+
}
|
|
50
|
+
return [...out];
|
|
51
|
+
}
|
|
52
|
+
/** Whether a pattern names everything, which an input pattern may not. */
|
|
53
|
+
export function namesEverything(pattern) {
|
|
54
|
+
const p = pattern.trim().replace(/\\/g, '/').replace(/^\.?\/+/, '');
|
|
55
|
+
return !p || p === '.' || /^[*/]+$/.test(p);
|
|
56
|
+
}
|
|
57
|
+
/** The folder part of a pattern before its first wildcard, for narrowing what git lists. */
|
|
58
|
+
function literalPrefix(pattern) {
|
|
59
|
+
const parts = pattern.replace(/\/+$/, '').split('/');
|
|
60
|
+
const fixed = [];
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
if (/[*?[]/.test(part))
|
|
63
|
+
break;
|
|
64
|
+
fixed.push(part);
|
|
65
|
+
}
|
|
66
|
+
return fixed.join('/');
|
|
67
|
+
}
|
|
68
|
+
function pathspecs(patterns) {
|
|
69
|
+
const prefixes = [...new Set(patterns.map(literalPrefix))];
|
|
70
|
+
return prefixes.includes('') ? [] : prefixes.map((p) => `:(icase)${p}`);
|
|
71
|
+
}
|
|
72
|
+
const list = async (top, args) => {
|
|
73
|
+
const r = await gitBytes(top, args, 60_000, 64 * 1024 * 1024);
|
|
74
|
+
return r.ok ? r.stdout.toString('utf8').split('\0').filter(Boolean) : [];
|
|
75
|
+
};
|
|
76
|
+
/** The input files in the working tree, each with what git thinks of it. */
|
|
77
|
+
export async function inputFilesInTree(top, patterns) {
|
|
78
|
+
const specs = pathspecs(patterns);
|
|
79
|
+
const tail = specs.length > 0 ? ['--', ...specs] : [];
|
|
80
|
+
const seen = new Map();
|
|
81
|
+
for (const path of await list(top, ['ls-files', '-z', '--cached', ...tail]))
|
|
82
|
+
seen.set(path, 'tracked');
|
|
83
|
+
for (const path of await list(top, ['ls-files', '-z', '--others', '--exclude-standard', ...tail]))
|
|
84
|
+
if (!seen.has(path))
|
|
85
|
+
seen.set(path, 'untracked');
|
|
86
|
+
for (const path of await list(top, ['ls-files', '-z', '--others', '--ignored', '--exclude-standard', ...tail])) {
|
|
87
|
+
// Installed or built folders are never inputs, however wide the pattern.
|
|
88
|
+
if (!seen.has(path) && !looksGenerated(path)?.key.endsWith('/'))
|
|
89
|
+
seen.set(path, 'ignored');
|
|
90
|
+
}
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const [path, kind] of seen) {
|
|
93
|
+
if (!inScope(path, patterns))
|
|
94
|
+
continue;
|
|
95
|
+
// A tracked file deleted from the tree is still listed by --cached; it is not there to read.
|
|
96
|
+
if (kind === 'tracked' && !(await stat(join(top, path)).catch(() => null))?.isFile())
|
|
97
|
+
continue;
|
|
98
|
+
out.push({ path, kind });
|
|
99
|
+
}
|
|
100
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
101
|
+
}
|
|
102
|
+
/** The input files in one commit: path, mode and blob. */
|
|
103
|
+
async function inputsAt(top, commit, patterns) {
|
|
104
|
+
const specs = pathspecs(patterns);
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const rec of await list(top, ['ls-tree', '-r', '-z', '--full-tree', commit, ...(specs.length > 0 ? ['--', ...specs.map((s) => s.replace(':(icase)', ''))] : [])])) {
|
|
107
|
+
const tab = rec.indexOf('\t');
|
|
108
|
+
const [mode = '', type = '', blob = ''] = rec.slice(0, tab).split(' ');
|
|
109
|
+
const path = rec.slice(tab + 1);
|
|
110
|
+
if (type === 'blob' && inScope(path, patterns))
|
|
111
|
+
out.push({ path, mode, blob });
|
|
112
|
+
}
|
|
113
|
+
// `ls-tree` pathspecs are case-sensitive; a pattern written in another case is matched in full.
|
|
114
|
+
if (out.length === 0 && specs.length > 0) {
|
|
115
|
+
for (const rec of await list(top, ['ls-tree', '-r', '-z', '--full-tree', commit])) {
|
|
116
|
+
const tab = rec.indexOf('\t');
|
|
117
|
+
const [mode = '', type = '', blob = ''] = rec.slice(0, tab).split(' ');
|
|
118
|
+
const path = rec.slice(tab + 1);
|
|
119
|
+
if (type === 'blob' && inScope(path, patterns))
|
|
120
|
+
out.push({ path, mode, blob });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
126
|
+
/** The sums of the input files as one commit has them. */
|
|
127
|
+
export async function inputSumsAt(top, commit, patterns) {
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const f of await inputsAt(top, commit, patterns)) {
|
|
130
|
+
const r = await gitBytes(top, ['cat-file', 'blob', f.blob], 120_000, 512 * 1024 * 1024);
|
|
131
|
+
if (r.ok)
|
|
132
|
+
out.push({ path: f.path, sha256: sha(r.stdout), size: r.stdout.length, blob: f.blob });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The blob id the working-tree file would be committed as, or null when it is not there.
|
|
138
|
+
*
|
|
139
|
+
* Compared by git's own hashing, with its line-ending conversion, rather than by the bytes: with
|
|
140
|
+
* `core.autocrlf` — usual on Windows — a checked-out file has CRLF where the commit has LF, and a
|
|
141
|
+
* byte comparison called every untouched input changed, every round.
|
|
142
|
+
*/
|
|
143
|
+
export async function blobOfFile(top, path) {
|
|
144
|
+
if (!(await stat(join(top, path)).catch(() => null))?.isFile())
|
|
145
|
+
return null;
|
|
146
|
+
const r = await git(top, ['hash-object', `--path=${path}`, '--', join(top, path)]);
|
|
147
|
+
return r.ok && r.stdout ? r.stdout : null;
|
|
148
|
+
}
|
|
149
|
+
const norm = (dir) => dir.trim().replace(/[\\/]+$/, '').replace(/\//g, '\\').toLowerCase();
|
|
150
|
+
/**
|
|
151
|
+
* Makes `parent` plus the given files one commit, on a new branch, without touching the working
|
|
152
|
+
* tree or the index: a temporary index is read from `parent`, the files are set in it, and the
|
|
153
|
+
* tree is committed with `commit-tree`.
|
|
154
|
+
*/
|
|
155
|
+
async function commitOnto(top, parent, files, message, branch) {
|
|
156
|
+
const index = join(tmpdir(), `cop-inputs-index-${process.pid}-${Date.now()}`);
|
|
157
|
+
const env = { ...process.env, GIT_INDEX_FILE: index };
|
|
158
|
+
try {
|
|
159
|
+
const read = await git(top, ['read-tree', parent], 60_000, env);
|
|
160
|
+
if (!read.ok)
|
|
161
|
+
return { problem: read.stderr || 'git read-tree failed' };
|
|
162
|
+
for (const f of files) {
|
|
163
|
+
const set = await git(top, ['update-index', '--add', '--cacheinfo', `${f.mode},${f.blob},${f.path}`], 60_000, env);
|
|
164
|
+
if (!set.ok)
|
|
165
|
+
return { problem: `${f.path}: ${set.stderr || 'git update-index failed'}` };
|
|
166
|
+
}
|
|
167
|
+
const tree = await git(top, ['write-tree'], 60_000, env);
|
|
168
|
+
if (!tree.ok || !tree.stdout)
|
|
169
|
+
return { problem: tree.stderr || 'git write-tree failed' };
|
|
170
|
+
const commit = await git(top, ['-c', 'user.name=copilot-operator', '-c', `user.email=${RUNNER_EMAIL}`, 'commit-tree', tree.stdout, '-p', parent, '-m', message]);
|
|
171
|
+
if (!commit.ok || !commit.stdout)
|
|
172
|
+
return { problem: commit.stderr || 'git commit-tree failed' };
|
|
173
|
+
const made = await git(top, ['branch', branch, commit.stdout]);
|
|
174
|
+
if (!made.ok)
|
|
175
|
+
return { problem: made.stderr || `the branch ${branch} could not be made` };
|
|
176
|
+
return { commit: commit.stdout };
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await rm(index, { force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Makes sure the commit a session starts from has the operator's input files, and records their
|
|
184
|
+
* sums on the start. Returns the start to use — the same one, or one whose commit has the inputs
|
|
185
|
+
* committed on top — or why the session cannot start.
|
|
186
|
+
*
|
|
187
|
+
* Called once the start is known and the working tree holds nothing uncommitted of the inputs (new
|
|
188
|
+
* or changed ones went into the snapshot before this). Where the inputs are taken from: the
|
|
189
|
+
* repository's HEAD for every pattern it has files for, else the latest session in this same
|
|
190
|
+
* repository whose start recorded files for that pattern.
|
|
191
|
+
*/
|
|
192
|
+
export async function settleInputs(session, dir, start, allSessions) {
|
|
193
|
+
const settings = inputSettings(session.vcs);
|
|
194
|
+
if (!settings)
|
|
195
|
+
return { start };
|
|
196
|
+
const topR = await git(dir, ['rev-parse', '--show-toplevel']);
|
|
197
|
+
if (!topR.ok || !topR.stdout)
|
|
198
|
+
return { problem: `${dir} is not a git repository.` };
|
|
199
|
+
const top = resolve(topR.stdout);
|
|
200
|
+
const head = (await git(top, ['rev-parse', '--verify', '--quiet', 'HEAD'])).stdout;
|
|
201
|
+
// Where each pattern's files are: HEAD first, then the latest earlier capture in this repository.
|
|
202
|
+
const wanted = [];
|
|
203
|
+
const sources = new Set();
|
|
204
|
+
const missing = [];
|
|
205
|
+
const atHead = head ? await inputsAt(top, head, settings.patterns) : [];
|
|
206
|
+
const earlier = (await allSessions())
|
|
207
|
+
.filter((s) => s.id !== session.id && s.vcsStart?.inputs && norm(s.vcs?.repoDir || s.projectDir || '') === norm(dir))
|
|
208
|
+
// The newest first: a later capture is the operator's later word on the inputs.
|
|
209
|
+
.sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))
|
|
210
|
+
.map((s) => s.vcsStart);
|
|
211
|
+
for (const pattern of settings.patterns) {
|
|
212
|
+
let found = atHead.filter((f) => inScope(f.path, [pattern]));
|
|
213
|
+
let from = head;
|
|
214
|
+
// The start itself next: a snapshot approved on the page has them, wherever HEAD has moved since.
|
|
215
|
+
if (found.length === 0) {
|
|
216
|
+
found = (await inputsAt(top, start.commit, [pattern])).filter((f) => inScope(f.path, [pattern]));
|
|
217
|
+
from = start.commit;
|
|
218
|
+
}
|
|
219
|
+
for (const s of earlier) {
|
|
220
|
+
if (found.length > 0)
|
|
221
|
+
break;
|
|
222
|
+
found = (await inputsAt(top, s.commit, [pattern])).filter((f) => inScope(f.path, [pattern]));
|
|
223
|
+
from = s.commit;
|
|
224
|
+
}
|
|
225
|
+
if (found.length === 0)
|
|
226
|
+
missing.push(pattern);
|
|
227
|
+
else
|
|
228
|
+
sources.add(from);
|
|
229
|
+
for (const f of found)
|
|
230
|
+
if (!wanted.some((w) => w.path === f.path))
|
|
231
|
+
wanted.push(f);
|
|
232
|
+
}
|
|
233
|
+
if (missing.length > 0) {
|
|
234
|
+
return {
|
|
235
|
+
problem: `the input file pattern(s) ${missing.map((m) => `"${m}"`).join(', ')} match no file: not in the working tree, not in the repository's HEAD, ` +
|
|
236
|
+
'and no earlier session in this repository captured them. Put the files in the project (they will be listed for your approval), or correct "Input files".',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// What the start has already, file by file; only what differs is committed on top.
|
|
240
|
+
const inStart = new Map((await inputsAt(top, start.commit, settings.patterns)).map((f) => [f.path, f]));
|
|
241
|
+
const differ = wanted.filter((w) => inStart.get(w.path)?.blob !== w.blob);
|
|
242
|
+
let commit = start.commit;
|
|
243
|
+
let carried;
|
|
244
|
+
if (differ.length > 0) {
|
|
245
|
+
const branch = await freeBranchName(top, `${branchNameFrom(['input'], session.vcs?.branchPrefix || 'cop/')}/${session.id}`);
|
|
246
|
+
if (!(await isValidBranchName(top, branch)))
|
|
247
|
+
return { problem: `"${branch}" is not a name git accepts.` };
|
|
248
|
+
const from = [...sources].map((s) => s.slice(0, 8)).join(', ');
|
|
249
|
+
const message = [
|
|
250
|
+
'Capture user-provided inputs',
|
|
251
|
+
'',
|
|
252
|
+
`The operator's input files for session "${session.name}", which the commit it starts from (${start.commit.slice(0, 8)}) did not have,`,
|
|
253
|
+
`taken from ${from}, where they were last committed. They are inputs: the work reads them and does not change them.`,
|
|
254
|
+
'',
|
|
255
|
+
...differ.slice(0, 200).map((f) => `- ${f.path}`),
|
|
256
|
+
'',
|
|
257
|
+
'Committed by copilot-operator. Not pushed.',
|
|
258
|
+
'',
|
|
259
|
+
].join('\n');
|
|
260
|
+
const made = await commitOnto(top, start.commit, differ, message, branch);
|
|
261
|
+
if ('problem' in made)
|
|
262
|
+
return { problem: `the input files could not be committed on top of the session's start: ${made.problem}` };
|
|
263
|
+
commit = made.commit;
|
|
264
|
+
carried = { onto: start.commit, from, branch };
|
|
265
|
+
}
|
|
266
|
+
const files = await inputSumsAt(top, commit, settings.patterns);
|
|
267
|
+
return { start: { ...start, commit, inputs: { patterns: settings.patterns, files, readOnly: settings.readOnly, ...(carried ? { carried } : {}) } } };
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Puts the input files back as the task's starting commit has them: a changed or removed one from
|
|
271
|
+
* that commit, a new one matching an input pattern removed. Only on the task's own branch, whose
|
|
272
|
+
* starting commit has the inputs (see `settleInputs`).
|
|
273
|
+
*/
|
|
274
|
+
export async function protectInputs(dir, base, inputs) {
|
|
275
|
+
const out = { changed: [], restored: [], failed: [] };
|
|
276
|
+
const topR = await git(dir, ['rev-parse', '--show-toplevel']);
|
|
277
|
+
if (!topR.ok || !topR.stdout)
|
|
278
|
+
return out;
|
|
279
|
+
const top = resolve(topR.stdout);
|
|
280
|
+
const known = new Map(inputs.files.map((f) => [f.path, f.blob]));
|
|
281
|
+
for (const f of inputs.files) {
|
|
282
|
+
if ((await blobOfFile(top, f.path)) === f.blob)
|
|
283
|
+
continue;
|
|
284
|
+
out.changed.push(f.path);
|
|
285
|
+
const back = await git(top, ['checkout', base, '--', f.path]);
|
|
286
|
+
if (back.ok && (await blobOfFile(top, f.path)) === f.blob)
|
|
287
|
+
out.restored.push(f.path);
|
|
288
|
+
else
|
|
289
|
+
out.failed.push({ path: f.path, why: back.stderr || 'it is not as the starting commit has it' });
|
|
290
|
+
}
|
|
291
|
+
// A file the task added under an input pattern: inputs are the operator's, so it goes.
|
|
292
|
+
for (const { path, kind } of await inputFilesInTree(top, inputs.patterns)) {
|
|
293
|
+
if (known.has(path) || kind === 'tracked')
|
|
294
|
+
continue;
|
|
295
|
+
out.changed.push(path);
|
|
296
|
+
const abs = resolve(join(top, path));
|
|
297
|
+
if (!abs.startsWith(resolve(top) + sep)) {
|
|
298
|
+
out.failed.push({ path, why: 'not inside the repository' });
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
await rm(abs, { force: true });
|
|
303
|
+
out.restored.push(path);
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
out.failed.push({ path, why: e.message });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
/** What the chat is told when input files were put back. */
|
|
312
|
+
export function inputsMessage(check) {
|
|
313
|
+
const names = (paths) => paths.slice(0, 15).join(', ') + (paths.length > 15 ? `, and ${paths.length - 15} more` : '');
|
|
314
|
+
return [
|
|
315
|
+
`The operator's input files are read-only, so the runner put back: ${names(check.restored)}.`,
|
|
316
|
+
check.failed.length > 0 ? `Could not be put back: ${check.failed.map((f) => `${f.path} (${f.why})`).join('; ')}.` : '',
|
|
317
|
+
'Read them as they are; do not change, move, delete or add to them. If the work truly needs a different input, end with status "blocked" and say which and why.',
|
|
318
|
+
]
|
|
319
|
+
.filter(Boolean)
|
|
320
|
+
.join(' ');
|
|
321
|
+
}
|
|
322
|
+
/** The lines the version-control note carries about the inputs. */
|
|
323
|
+
export function inputsNote(inputs) {
|
|
324
|
+
if (!inputs || inputs.files.length === 0)
|
|
325
|
+
return '';
|
|
326
|
+
const shown = inputs.files.slice(0, 20).map((f) => `\`${f.path}\``).join(', ');
|
|
327
|
+
return (`The operator's input files are in your working tree, committed in the starting commit: ${shown}` +
|
|
328
|
+
`${inputs.files.length > 20 ? `, and ${inputs.files.length - 20} more` : ''}. ` +
|
|
329
|
+
(inputs.readOnly
|
|
330
|
+
? 'They are read-only: read them, never change, move or delete them, and add nothing under their paths. The runner puts them back after every round of steps.'
|
|
331
|
+
: 'They are the work\'s input; change them only if the task says so.'));
|
|
332
|
+
}
|