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/vcs/snapshot.js
CHANGED
|
@@ -22,12 +22,15 @@
|
|
|
22
22
|
* Like the rest of `vcs/`: nothing is reset, stashed, deleted or rewritten. The snapshot adds a
|
|
23
23
|
* branch and a commit.
|
|
24
24
|
*/
|
|
25
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
25
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
26
26
|
import { tmpdir } from 'node:os';
|
|
27
27
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
28
28
|
import { looksGenerated } from './commitHygiene.js';
|
|
29
29
|
import { branchNameFrom, freeBranchName, git, gitBytes, isValidBranchName, RUNNER_EMAIL } from './git.js';
|
|
30
|
-
import { repoDirOf, sessionStart } from './taskVcs.js';
|
|
30
|
+
import { repoDirOf, sessionBranchName, sessionStart } from './taskVcs.js';
|
|
31
|
+
import { inputChanges, inputFilesInTree, inputSettings, inputSumsAt, missingInputPatterns, recaptureInputs } from './inputs.js';
|
|
32
|
+
import { artifactPatterns } from './artifacts.js';
|
|
33
|
+
import { inScope } from './scope.js';
|
|
31
34
|
/** The session's dirty-tree policy, absent fields filled in: reject, and asking first. */
|
|
32
35
|
export function dirtyPolicy(vcs) {
|
|
33
36
|
const policy = vcs?.dirtyWorktree?.policy;
|
|
@@ -128,13 +131,18 @@ async function startProblem(session, dir, branch, head, allSessions) {
|
|
|
128
131
|
*/
|
|
129
132
|
export async function planSnapshot(session, allSessions = async () => []) {
|
|
130
133
|
const { policy, requireApproval } = dirtyPolicy(session.vcs);
|
|
134
|
+
const inputs = inputSettings(session.vcs);
|
|
131
135
|
const repoDir = repoDirOf(session);
|
|
132
136
|
const plan = { needed: false, ok: false, policy, requireApproval, repoDir, branch: null, head: null, entries: [] };
|
|
133
|
-
if (!session.vcs?.enabled || policy === 'reject'
|
|
137
|
+
if (!session.vcs?.enabled || (policy === 'reject' && !inputs))
|
|
138
|
+
return plan;
|
|
139
|
+
if (session.vcsBaseCommit && (!inputs || session.vcs.startFrom === 'existing-branch'))
|
|
134
140
|
return plan;
|
|
135
141
|
const top = repoDir ? await topOf(repoDir) : null;
|
|
136
142
|
if (!top)
|
|
137
143
|
return { ...plan, problem: `${repoDir || 'The repository folder'} is not a git repository.` };
|
|
144
|
+
if (session.vcsBaseCommit)
|
|
145
|
+
return await planRecapture(session, top, plan);
|
|
138
146
|
const status = await statusEntries(top);
|
|
139
147
|
if (!status)
|
|
140
148
|
return { ...plan, problem: 'git status could not be read.' };
|
|
@@ -142,13 +150,43 @@ export async function planSnapshot(session, allSessions = async () => []) {
|
|
|
142
150
|
const headR = await git(top, ['rev-parse', '--verify', '--quiet', 'HEAD']);
|
|
143
151
|
plan.branch = branchR.ok && branchR.stdout !== 'HEAD' ? branchR.stdout : null;
|
|
144
152
|
plan.head = headR.ok && headR.stdout ? headR.stdout : null;
|
|
145
|
-
if (status.length === 0)
|
|
146
|
-
return plan;
|
|
147
|
-
plan.needed = true;
|
|
148
153
|
// The project folder, when it is a folder inside the repository: a change outside it is not this session's.
|
|
149
154
|
const project = session.projectDir?.trim() ? relative(top, resolve(session.projectDir.trim())).replace(/\\/g, '/') : '';
|
|
150
155
|
const inProject = (p) => !project || project.startsWith('..') || isAbsolute(project) || p.toLowerCase() === project.toLowerCase() || p.toLowerCase().startsWith(`${project.toLowerCase()}/`);
|
|
151
|
-
|
|
156
|
+
const size = async (p) => (await stat(join(top, p)).catch(() => null))?.size;
|
|
157
|
+
/*
|
|
158
|
+
* The operator's input files that are not committed as they are: new, changed, or ignored. They
|
|
159
|
+
* are taken whatever the dirty-tree policy, or the snapshot is not; the rest of the tree is the
|
|
160
|
+
* policy's. A secrets file or one outside the project is never an input.
|
|
161
|
+
*/
|
|
162
|
+
const inputPaths = new Set();
|
|
163
|
+
if (inputs) {
|
|
164
|
+
const dirtyNow = new Set(status.map((s) => s.path));
|
|
165
|
+
for (const f of await inputFilesInTree(top, inputs.patterns)) {
|
|
166
|
+
if (f.kind === 'tracked' && !dirtyNow.has(f.path))
|
|
167
|
+
continue;
|
|
168
|
+
inputPaths.add(f.path);
|
|
169
|
+
const blocked = looksGenerated(f.path)?.reason === 'a secrets file' ? 'a secrets file is never taken as an input' : !inProject(f.path) ? `outside the project folder ${project}` : undefined;
|
|
170
|
+
plan.entries.push({ path: f.path, kind: f.kind, input: true, size: await size(f.path), allowed: blocked ? [] : ['include'], choice: blocked ? null : 'include', reason: blocked ?? 'an input file: committed with the starting snapshot, then read-only' });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const others = status.filter((s) => !inputPaths.has(s.path));
|
|
174
|
+
if (policy === 'reject') {
|
|
175
|
+
if (plan.entries.length === 0)
|
|
176
|
+
return plan;
|
|
177
|
+
plan.needed = true;
|
|
178
|
+
if (others.length > 0) {
|
|
179
|
+
plan.problem =
|
|
180
|
+
`besides the input files there are other uncommitted changes (${someOf(others.map((o) => o.path))}). ` +
|
|
181
|
+
'Commit them yourself, or choose "Take them as a starting snapshot" under "Uncommitted changes".';
|
|
182
|
+
return plan;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (plan.entries.length === 0 && others.length === 0)
|
|
186
|
+
return plan;
|
|
187
|
+
plan.needed = true;
|
|
188
|
+
plan.requireApproval = (others.length > 0 && requireApproval) || (plan.entries.some((e) => e.input) && !!inputs?.requireApproval);
|
|
189
|
+
for (const s of policy === 'reject' ? [] : others) {
|
|
152
190
|
const generated = looksGenerated(s.path);
|
|
153
191
|
const outside = !inProject(s.path);
|
|
154
192
|
if (!s.untracked) {
|
|
@@ -157,21 +195,31 @@ export async function planSnapshot(session, allSessions = async () => []) {
|
|
|
157
195
|
: outside
|
|
158
196
|
? `outside the project folder ${project}; it is tracked, so it cannot be left out. Commit or undo that change yourself`
|
|
159
197
|
: undefined;
|
|
160
|
-
plan.entries.push({ path: s.path, ...(s.from ? { from: s.from } : {}), kind: 'tracked', allowed: blocked ? [] : ['include'], choice: blocked ? null : 'include', ...(blocked ? { reason: blocked } : {}) });
|
|
198
|
+
plan.entries.push({ path: s.path, ...(s.from ? { from: s.from } : {}), kind: 'tracked', size: await size(s.path), allowed: blocked ? [] : ['include'], choice: blocked ? null : 'include', ...(blocked ? { reason: blocked } : {}) });
|
|
161
199
|
continue;
|
|
162
200
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
201
|
+
const artifact = artifactPatterns(session.vcs).length > 0 && inScope(s.path, artifactPatterns(session.vcs));
|
|
202
|
+
const why = artifact
|
|
203
|
+
? 'an artifact: kept with the run, never committed'
|
|
204
|
+
: generated ? generated.reason : outside ? `outside the project folder ${project}` : policy === 'tracked-only-snapshot' ? 'a new file, and this session takes tracked changes only' : undefined;
|
|
205
|
+
plan.entries.push({ path: s.path, kind: 'untracked', size: await size(s.path), allowed: why ? ['leave-out'] : ['include', 'leave-out'], choice: why ? 'leave-out' : 'include', ...(why ? { reason: why } : {}) });
|
|
165
206
|
}
|
|
166
207
|
if (policy === 'snapshot') {
|
|
167
208
|
for (const path of await scopedIgnored(top, scopedPatterns(session))) {
|
|
168
|
-
if (looksGenerated(path) || !inProject(path))
|
|
209
|
+
if (looksGenerated(path) || !inProject(path) || inputPaths.has(path))
|
|
169
210
|
continue;
|
|
170
211
|
plan.entries.push({ path, kind: 'ignored', allowed: ['include', 'leave-out'], choice: 'leave-out', reason: "ignored by git, and a task's scope names it: taken only when you tick it" });
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
const blocked = plan.entries.filter((e) => e.allowed.length === 0);
|
|
174
|
-
|
|
215
|
+
// A pattern that matches nothing anywhere is said now, before anything is committed for approval.
|
|
216
|
+
const missing = inputs ? await missingInputPatterns(session, repoDir, top, allSessions) : [];
|
|
217
|
+
if (missing.length > 0) {
|
|
218
|
+
plan.problem =
|
|
219
|
+
`the input file pattern(s) ${missing.map((m) => `"${m}"`).join(', ')} match no file in the project, and nothing earlier captured them. ` +
|
|
220
|
+
'Correct "Input files" (a "*" stays inside one folder; "**" crosses folders), or put the files in the project.';
|
|
221
|
+
}
|
|
222
|
+
else if (!plan.head)
|
|
175
223
|
plan.problem = 'the repository has no commit yet, so there is nothing for a snapshot to sit on. Make the first commit yourself.';
|
|
176
224
|
else if (blocked.length > 0)
|
|
177
225
|
plan.problem = `${blocked.length} change(s) can be neither taken nor left out: ${blocked.map((e) => `${e.path} (${e.reason})`).join('; ')}.`;
|
|
@@ -187,6 +235,62 @@ export async function planSnapshot(session, allSessions = async () => []) {
|
|
|
187
235
|
plan.ok = !plan.problem;
|
|
188
236
|
return plan;
|
|
189
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* After the session's start: the input files changed or added since it was recorded, to be taken
|
|
240
|
+
* onto its line of work (see `recaptureInputs`). Anything else uncommitted is in the way, as it is
|
|
241
|
+
* before a task, and refuses.
|
|
242
|
+
*/
|
|
243
|
+
async function planRecapture(session, top, plan) {
|
|
244
|
+
const inputs = inputSettings(session.vcs);
|
|
245
|
+
if (!inputs || !session.vcsBaseCommit)
|
|
246
|
+
return plan;
|
|
247
|
+
const branchR = await git(top, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
248
|
+
const headR = await git(top, ['rev-parse', '--verify', '--quiet', 'HEAD']);
|
|
249
|
+
plan.branch = branchR.ok && branchR.stdout !== 'HEAD' ? branchR.stdout : null;
|
|
250
|
+
plan.head = headR.ok && headR.stdout ? headR.stdout : null;
|
|
251
|
+
const changes = await inputChanges(top, inputs.patterns, session.vcsStart?.inputs?.files);
|
|
252
|
+
if (changes.length === 0)
|
|
253
|
+
return plan;
|
|
254
|
+
plan.needed = true;
|
|
255
|
+
plan.requireApproval = inputs.requireApproval;
|
|
256
|
+
const project = session.projectDir?.trim() ? relative(top, resolve(session.projectDir.trim())).replace(/\\/g, '/') : '';
|
|
257
|
+
const inProject = (p) => !project || project.startsWith('..') || isAbsolute(project) || p.toLowerCase() === project.toLowerCase() || p.toLowerCase().startsWith(`${project.toLowerCase()}/`);
|
|
258
|
+
for (const c of changes) {
|
|
259
|
+
const blocked = looksGenerated(c.path)?.reason === 'a secrets file' ? 'a secrets file is never taken as an input' : !inProject(c.path) ? `outside the project folder ${project}` : undefined;
|
|
260
|
+
plan.entries.push({
|
|
261
|
+
path: c.path,
|
|
262
|
+
kind: c.kind,
|
|
263
|
+
input: true,
|
|
264
|
+
size: (await stat(join(top, c.path)).catch(() => null))?.size,
|
|
265
|
+
allowed: blocked ? [] : ['include'],
|
|
266
|
+
choice: blocked ? null : 'include',
|
|
267
|
+
reason: blocked ?? 'an input file changed or added after the session started: committed on its line of work, then read-only',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const changed = new Set(changes.map((c) => c.path));
|
|
271
|
+
const others = ((await statusEntries(top)) ?? []).filter((s) => !changed.has(s.path)).map((s) => s.path);
|
|
272
|
+
const blocked = plan.entries.filter((e) => e.allowed.length === 0);
|
|
273
|
+
if (others.length > 0) {
|
|
274
|
+
plan.problem = `besides the input files there are other uncommitted changes (${someOf(others)}). Commit or stash them yourself first.`;
|
|
275
|
+
}
|
|
276
|
+
else if (blocked.length > 0) {
|
|
277
|
+
plan.problem = `${blocked.length} file(s) cannot be taken as inputs: ${blocked.map((e) => `${e.path} (${e.reason})`).join('; ')}.`;
|
|
278
|
+
}
|
|
279
|
+
// Where they go: on the session's one branch when it has it, else on the commit tasks are cut from.
|
|
280
|
+
const sessionBranch = sessionBranchName(session);
|
|
281
|
+
const tip = session.vcs?.branchMode === 'per-session' ? (await git(top, ['rev-parse', '--verify', '--quiet', `refs/heads/${sessionBranch}^{commit}`])).stdout : '';
|
|
282
|
+
if (tip) {
|
|
283
|
+
plan.recapture = { parent: tip, ffBranch: sessionBranch };
|
|
284
|
+
plan.baselineBranch = sessionBranch;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const newBranch = await freeBranchName(top, `${branchNameFrom(['input'], session.vcs?.branchPrefix || 'cop/')}/${session.id}`);
|
|
288
|
+
plan.recapture = { parent: session.vcsBaseCommit, newBranch };
|
|
289
|
+
plan.baselineBranch = newBranch;
|
|
290
|
+
}
|
|
291
|
+
plan.ok = !plan.problem;
|
|
292
|
+
return plan;
|
|
293
|
+
}
|
|
190
294
|
/** A path as a pattern for `.git/info/exclude` that matches that one file and nothing else. */
|
|
191
295
|
function excludePattern(path) {
|
|
192
296
|
return `/${path.replace(/[\\*?[]/g, (c) => `\\${c}`).replace(/ $/, '\\ ')}`;
|
|
@@ -215,16 +319,20 @@ async function leaveOut(top, session, paths) {
|
|
|
215
319
|
return undefined;
|
|
216
320
|
}
|
|
217
321
|
/** The commit message: what was taken, what was left out, from where, and on whose word. */
|
|
218
|
-
function snapshotMessage(session, plan,
|
|
322
|
+
function snapshotMessage(session, plan, taken, leftOut, approved) {
|
|
219
323
|
const list = (paths) => [...paths.slice(0, 200).map((p) => `- ${p}`), ...(paths.length > 200 ? [`- and ${paths.length - 200} more`] : [])].join('\n');
|
|
324
|
+
const inputs = taken.filter((e) => e.input);
|
|
325
|
+
const rest = taken.filter((e) => !e.input).map((e) => e.path);
|
|
326
|
+
const onlyInputs = rest.length === 0;
|
|
220
327
|
return [
|
|
221
|
-
'Capture operator baseline before run',
|
|
328
|
+
onlyInputs ? 'Capture user-provided inputs' : 'Capture operator baseline before run',
|
|
222
329
|
'',
|
|
223
|
-
`The uncommitted changes in the repository when session "${session.name}" was first run, committed so that`,
|
|
330
|
+
`The ${onlyInputs ? "operator's input files" : 'uncommitted changes in the repository'} when session "${session.name}" was first run, committed so that`,
|
|
224
331
|
"the session starts from them and every task's commit shows only that task's work.",
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
332
|
+
...(inputs.length > 0
|
|
333
|
+
? ['', `Input files, read by the work and not changed by it (${inputs.length}):`, list(inputs.map((e) => `${e.path}${e.size !== undefined ? ` (${e.size} bytes, ${e.kind})` : ''}`))]
|
|
334
|
+
: []),
|
|
335
|
+
...(rest.length > 0 ? ['', `Taken (${rest.length}):`, list(rest)] : []),
|
|
228
336
|
...(leftOut.length > 0 ? ['', `Left out, in .git/info/exclude (${leftOut.length}):`, list(leftOut)] : []),
|
|
229
337
|
'',
|
|
230
338
|
`Taken from ${plan.branch ?? 'a detached HEAD'} at ${(plan.head ?? '').slice(0, 8)}, ${approved ? "on the operator's approval of this list" : 'automatically (requireApproval: false)'}.`,
|
|
@@ -240,11 +348,55 @@ function snapshotMessage(session, plan, included, leftOut, approved) {
|
|
|
240
348
|
* `approved`, every file takes its default and anything to be left out refuses (see `DirtyWorktree`).
|
|
241
349
|
*/
|
|
242
350
|
export async function takeSnapshot(session, opts, bus, saveSession, allSessions = async () => []) {
|
|
243
|
-
if (dirtyPolicy(session.vcs).policy === 'reject') {
|
|
351
|
+
if (dirtyPolicy(session.vcs).policy === 'reject' && !inputSettings(session.vcs)) {
|
|
244
352
|
return { ok: false, problem: 'this session does not take uncommitted changes as a snapshot. Choose that under Version control → "Uncommitted changes" first.' };
|
|
245
353
|
}
|
|
246
|
-
if (session.vcsBaseCommit)
|
|
247
|
-
|
|
354
|
+
if (session.vcsBaseCommit) {
|
|
355
|
+
// Started already: only input files changed or added since can be taken, onto its line of work.
|
|
356
|
+
const plan = await planSnapshot(session, allSessions);
|
|
357
|
+
if (!plan.needed || !plan.recapture) {
|
|
358
|
+
return { ok: false, problem: 'this session has already started, and no input file has changed since: a snapshot is taken only before its first task.' };
|
|
359
|
+
}
|
|
360
|
+
if (!plan.ok)
|
|
361
|
+
return { ok: false, problem: plan.problem ?? 'the input files cannot be taken.' };
|
|
362
|
+
const paths = plan.entries.map((e) => e.path);
|
|
363
|
+
if (opts.approved) {
|
|
364
|
+
const shown = Object.keys(opts.choices ?? {}).sort();
|
|
365
|
+
if (JSON.stringify(shown) !== JSON.stringify([...paths].sort()) || paths.some((p) => opts.choices?.[p] !== 'include')) {
|
|
366
|
+
return { ok: false, problem: 'the input files changed since the list was shown. Look at the list again; nothing was done.' };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const made = await recaptureInputs(session, plan.repoDir, paths, plan.recapture);
|
|
370
|
+
if ('problem' in made)
|
|
371
|
+
return { ok: false, problem: made.problem };
|
|
372
|
+
const settings = inputSettings(session.vcs);
|
|
373
|
+
const top = (await topOf(plan.repoDir));
|
|
374
|
+
const before = session.vcsStart;
|
|
375
|
+
const start = {
|
|
376
|
+
...before,
|
|
377
|
+
inputs: {
|
|
378
|
+
patterns: settings?.patterns ?? [],
|
|
379
|
+
files: await inputSumsAt(top, made.commit, settings?.patterns ?? []),
|
|
380
|
+
readOnly: settings?.readOnly ?? true,
|
|
381
|
+
...(before.inputs?.carried ? { carried: before.inputs.carried } : {}),
|
|
382
|
+
recaptured: [...(before.inputs?.recaptured ?? []), { commit: made.commit, branch: made.branch, paths: paths.slice(0, 200), approved: opts.approved }],
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
await saveSession((s) => {
|
|
386
|
+
s.vcsStart = start;
|
|
387
|
+
// A commit tasks are cut from moves to the inputs' commit; a session branch moved forward itself.
|
|
388
|
+
if (plan.recapture?.newBranch)
|
|
389
|
+
s.vcsBaseCommit = made.commit;
|
|
390
|
+
});
|
|
391
|
+
bus.publish({
|
|
392
|
+
sessionId: session.id,
|
|
393
|
+
type: 'vcs-inputs-recaptured',
|
|
394
|
+
level: 'info',
|
|
395
|
+
message: `${paths.length} input file(s) changed or added since the session started are now the commit ${made.commit.slice(0, 8)} on ${made.branch}; the next task starts from it`,
|
|
396
|
+
data: { commit: made.commit, branch: made.branch, paths: paths.slice(0, 50) },
|
|
397
|
+
});
|
|
398
|
+
return { ok: true, start };
|
|
399
|
+
}
|
|
248
400
|
const plan = await planSnapshot(session, allSessions);
|
|
249
401
|
if (!plan.needed)
|
|
250
402
|
return { ok: false, problem: 'there is nothing uncommitted to take.' };
|
|
@@ -320,7 +472,7 @@ export async function takeSnapshot(session, opts, bus, saveSession, allSessions
|
|
|
320
472
|
finally {
|
|
321
473
|
await rm(list, { force: true });
|
|
322
474
|
}
|
|
323
|
-
const message = snapshotMessage(session, plan, take
|
|
475
|
+
const message = snapshotMessage(session, plan, take, leftOutAll, opts.approved);
|
|
324
476
|
const commit = await git(top, ['-c', 'user.name=copilot-operator', '-c', `user.email=${RUNNER_EMAIL}`, 'commit', '--no-verify', '-q', '-m', message]);
|
|
325
477
|
if (!commit.ok)
|
|
326
478
|
return await back(`the snapshot could not be committed: ${commit.stderr || commit.stdout}.`);
|