copilot-operator 0.1.15 → 0.1.17
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 +34 -0
- package/dist/src/api/operator.service.js +112 -1
- package/dist/src/plan/brief.js +34 -0
- package/dist/src/plan/importPlan.js +8 -1
- package/dist/src/plan/schema.js +24 -0
- package/dist/src/plan/systemGuide.js +63 -0
- package/dist/src/session/exports.js +2 -0
- package/dist/src/session/store.js +68 -6
- package/dist/src/transport/copilotTransport.js +39 -1
- package/dist/src/vcs/snapshot.js +354 -0
- package/dist/src/vcs/taskVcs.js +68 -23
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/549-a13ae394c5e6a837.js +1 -0
- package/dist/web/_next/static/chunks/867-7f27ef819d251cc5.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-fd819bbfa7f33982.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-d28825430ca280de.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 +6 -3
- package/dist/web/_next/static/chunks/549-9548d9a132c8c7d4.js +0 -1
- package/dist/web/_next/static/chunks/867-8b95ae984befc0e2.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-71a43ed69134b48d.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/{IwNZiBrEMAcXEUFj2miiU → kkxFcGzA9Uo1uRAgcoDti}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{IwNZiBrEMAcXEUFj2miiU → kkxFcGzA9Uo1uRAgcoDti}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operator's uncommitted changes, taken as the commit a session starts from.
|
|
3
|
+
*
|
|
4
|
+
* Version control used to have one answer to a dirty tree before a session's first task: not ours
|
|
5
|
+
* to decide, so no branch at all. That stopped real work for the wrong reason — the YAML files a
|
|
6
|
+
* migration needed as input were untracked, the runner kept the repository clean and changed
|
|
7
|
+
* nothing, and the session could not do its job until the operator committed prerequisites by hand.
|
|
8
|
+
*
|
|
9
|
+
* `dirtyWorktree.policy: "snapshot"` makes it a decision the operator takes once, with the list in
|
|
10
|
+
* front of them: the changes become one commit, "Capture operator baseline before run", on a branch
|
|
11
|
+
* of its own (`<prefix>baseline/<session id>`), and the session's branches are cut from it. Nothing
|
|
12
|
+
* is lost (the changes are a commit), the starting state can be gone back to, and every task's
|
|
13
|
+
* commit shows only the task's work.
|
|
14
|
+
*
|
|
15
|
+
* What never goes in, whatever is chosen: secrets and tool output (`commitHygiene.ts`), files
|
|
16
|
+
* outside the session's project folder, and ignored files that no task's scope names. A file that
|
|
17
|
+
* does not go in cannot stay loose in the tree either — the first task's commit takes everything
|
|
18
|
+
* (`commitAll`) and a scope removes a new file outside it — so it is "left out": written to the
|
|
19
|
+
* repository's own `.git/info/exclude`, which is local, never committed, and leaves `.gitignore`
|
|
20
|
+
* alone. A tracked change cannot be left out that way; one that may not go in refuses the snapshot.
|
|
21
|
+
*
|
|
22
|
+
* Like the rest of `vcs/`: nothing is reset, stashed, deleted or rewritten. The snapshot adds a
|
|
23
|
+
* branch and a commit.
|
|
24
|
+
*/
|
|
25
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
26
|
+
import { tmpdir } from 'node:os';
|
|
27
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
28
|
+
import { looksGenerated } from './commitHygiene.js';
|
|
29
|
+
import { branchNameFrom, freeBranchName, git, gitBytes, isValidBranchName, RUNNER_EMAIL } from './git.js';
|
|
30
|
+
import { repoDirOf, sessionStart } from './taskVcs.js';
|
|
31
|
+
/** The session's dirty-tree policy, absent fields filled in: reject, and asking first. */
|
|
32
|
+
export function dirtyPolicy(vcs) {
|
|
33
|
+
const policy = vcs?.dirtyWorktree?.policy;
|
|
34
|
+
return {
|
|
35
|
+
policy: policy === 'snapshot' || policy === 'tracked-only-snapshot' ? policy : 'reject',
|
|
36
|
+
requireApproval: vcs?.dirtyWorktree?.requireApproval !== false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** A shortened list of paths for a message. */
|
|
40
|
+
export function someOf(paths, n = 5) {
|
|
41
|
+
return `${paths.slice(0, n).join(', ')}${paths.length > n ? `, and ${paths.length - n} more` : ''}`;
|
|
42
|
+
}
|
|
43
|
+
/** What `git status` says is uncommitted, file by file, read raw so no path loses a character. */
|
|
44
|
+
async function statusEntries(top) {
|
|
45
|
+
const r = await gitBytes(top, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
|
|
46
|
+
if (!r.ok)
|
|
47
|
+
return null;
|
|
48
|
+
const parts = r.stdout.toString('utf8').split('\0');
|
|
49
|
+
const out = [];
|
|
50
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
51
|
+
const rec = parts[i] ?? '';
|
|
52
|
+
if (rec.length < 4)
|
|
53
|
+
continue;
|
|
54
|
+
const xy = rec.slice(0, 2);
|
|
55
|
+
const path = rec.slice(3);
|
|
56
|
+
// A rename or a copy is followed by the path it came from, as a record of its own.
|
|
57
|
+
if (xy[0] === 'R' || xy[0] === 'C') {
|
|
58
|
+
out.push({ path, from: parts[i + 1] || undefined, untracked: false });
|
|
59
|
+
i += 1;
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
out.push({ path, untracked: xy === '??' });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The patterns of the session's task scopes that name files explicitly enough to let an ignored
|
|
68
|
+
* file in. "The whole repository" (`.`, `**`, `*`) names nothing in particular.
|
|
69
|
+
*/
|
|
70
|
+
function scopedPatterns(session) {
|
|
71
|
+
const out = new Set();
|
|
72
|
+
for (const task of session.tasks) {
|
|
73
|
+
for (const raw of task.scope ?? []) {
|
|
74
|
+
let p = raw.trim().replace(/\\/g, '/').replace(/^\.?\/+/, '');
|
|
75
|
+
if (p.endsWith('/'))
|
|
76
|
+
p += '**';
|
|
77
|
+
if (!p || p === '.' || /^[*/]+$/.test(p))
|
|
78
|
+
continue;
|
|
79
|
+
out.add(p);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...out];
|
|
83
|
+
}
|
|
84
|
+
/** Ignored files a task's scope names, for the operator to tick. */
|
|
85
|
+
async function scopedIgnored(top, patterns) {
|
|
86
|
+
if (patterns.length === 0)
|
|
87
|
+
return [];
|
|
88
|
+
const r = await gitBytes(top, ['ls-files', '-z', '--others', '--ignored', '--exclude-standard', '--', ...patterns.map((p) => `:(glob,icase)${p}`)], 60_000, 32 * 1024 * 1024);
|
|
89
|
+
if (!r.ok)
|
|
90
|
+
return [];
|
|
91
|
+
return r.stdout.toString('utf8').split('\0').filter(Boolean).slice(0, 2000);
|
|
92
|
+
}
|
|
93
|
+
/** The repository's top folder, which every path git prints is relative to. */
|
|
94
|
+
async function topOf(dir) {
|
|
95
|
+
const r = await git(dir, ['rev-parse', '--show-toplevel']);
|
|
96
|
+
return r.ok && r.stdout ? resolve(r.stdout) : null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether the session's start allows a snapshot, and why not. A snapshot is taken where the changes
|
|
100
|
+
* are — on HEAD — so the session has to start from HEAD: `head`, or a branch or previous session
|
|
101
|
+
* whose tip HEAD is. Asked without updating from the remote: a fetch is not the question here.
|
|
102
|
+
*/
|
|
103
|
+
async function startProblem(session, dir, branch, head, allSessions) {
|
|
104
|
+
const how = session.vcs?.startFrom ?? 'head';
|
|
105
|
+
if (how === 'head')
|
|
106
|
+
return undefined;
|
|
107
|
+
const where = `${branch ? `the branch ${branch}` : 'a detached HEAD'} at ${head.slice(0, 8)}`;
|
|
108
|
+
if (how === 'existing-branch') {
|
|
109
|
+
return (`this session carries on the existing branch "${session.vcs?.existingBranch ?? ''}", and a starting snapshot is a branch of its own, ` +
|
|
110
|
+
'so the work would not be on the branch you named. Commit the changes on that branch yourself, or choose another "Start from".');
|
|
111
|
+
}
|
|
112
|
+
const offline = { ...session, vcs: { ...session.vcs, updateFromRemote: false } };
|
|
113
|
+
const resolved = await sessionStart(offline, dir, head, allSessions);
|
|
114
|
+
if ('problem' in resolved)
|
|
115
|
+
return resolved.problem;
|
|
116
|
+
const commit = resolved.start?.commit;
|
|
117
|
+
if (!commit || commit === head)
|
|
118
|
+
return undefined;
|
|
119
|
+
const wanted = resolved.start?.kind === 'previous-session' ? `the end of "${resolved.start.fromSession?.name ?? ''}"'s branch ${resolved.start.branch}` : `the local branch ${resolved.start?.branch ?? ''}`;
|
|
120
|
+
return (`the changes are on ${where}, and this session is set to start from ${wanted} (${commit.slice(0, 8)}). ` +
|
|
121
|
+
'A snapshot is taken where the changes are, so the session would not start where it was told to. ' +
|
|
122
|
+
`Put the repository on ${resolved.start?.branch ?? 'that branch'} first, or choose "From wherever the repository is (as until now)".`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* What a snapshot of the repository would take now, and what may be done with each file.
|
|
126
|
+
*
|
|
127
|
+
* Not needed when the session has no snapshot policy, already has its start, or the tree is clean.
|
|
128
|
+
*/
|
|
129
|
+
export async function planSnapshot(session, allSessions = async () => []) {
|
|
130
|
+
const { policy, requireApproval } = dirtyPolicy(session.vcs);
|
|
131
|
+
const repoDir = repoDirOf(session);
|
|
132
|
+
const plan = { needed: false, ok: false, policy, requireApproval, repoDir, branch: null, head: null, entries: [] };
|
|
133
|
+
if (!session.vcs?.enabled || policy === 'reject' || session.vcsBaseCommit)
|
|
134
|
+
return plan;
|
|
135
|
+
const top = repoDir ? await topOf(repoDir) : null;
|
|
136
|
+
if (!top)
|
|
137
|
+
return { ...plan, problem: `${repoDir || 'The repository folder'} is not a git repository.` };
|
|
138
|
+
const status = await statusEntries(top);
|
|
139
|
+
if (!status)
|
|
140
|
+
return { ...plan, problem: 'git status could not be read.' };
|
|
141
|
+
const branchR = await git(top, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
142
|
+
const headR = await git(top, ['rev-parse', '--verify', '--quiet', 'HEAD']);
|
|
143
|
+
plan.branch = branchR.ok && branchR.stdout !== 'HEAD' ? branchR.stdout : null;
|
|
144
|
+
plan.head = headR.ok && headR.stdout ? headR.stdout : null;
|
|
145
|
+
if (status.length === 0)
|
|
146
|
+
return plan;
|
|
147
|
+
plan.needed = true;
|
|
148
|
+
// The project folder, when it is a folder inside the repository: a change outside it is not this session's.
|
|
149
|
+
const project = session.projectDir?.trim() ? relative(top, resolve(session.projectDir.trim())).replace(/\\/g, '/') : '';
|
|
150
|
+
const inProject = (p) => !project || project.startsWith('..') || isAbsolute(project) || p.toLowerCase() === project.toLowerCase() || p.toLowerCase().startsWith(`${project.toLowerCase()}/`);
|
|
151
|
+
for (const s of status) {
|
|
152
|
+
const generated = looksGenerated(s.path);
|
|
153
|
+
const outside = !inProject(s.path);
|
|
154
|
+
if (!s.untracked) {
|
|
155
|
+
const blocked = generated?.reason === 'a secrets file'
|
|
156
|
+
? `a secrets file with uncommitted changes; it is tracked, so it cannot be left out. Commit or undo that change yourself`
|
|
157
|
+
: outside
|
|
158
|
+
? `outside the project folder ${project}; it is tracked, so it cannot be left out. Commit or undo that change yourself`
|
|
159
|
+
: 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 } : {}) });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const why = generated ? generated.reason : outside ? `outside the project folder ${project}` : policy === 'tracked-only-snapshot' ? 'a new file, and this session takes tracked changes only' : undefined;
|
|
164
|
+
plan.entries.push({ path: s.path, kind: 'untracked', allowed: why ? ['leave-out'] : ['include', 'leave-out'], choice: why ? 'leave-out' : 'include', ...(why ? { reason: why } : {}) });
|
|
165
|
+
}
|
|
166
|
+
if (policy === 'snapshot') {
|
|
167
|
+
for (const path of await scopedIgnored(top, scopedPatterns(session))) {
|
|
168
|
+
if (looksGenerated(path) || !inProject(path))
|
|
169
|
+
continue;
|
|
170
|
+
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
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const blocked = plan.entries.filter((e) => e.allowed.length === 0);
|
|
174
|
+
if (!plan.head)
|
|
175
|
+
plan.problem = 'the repository has no commit yet, so there is nothing for a snapshot to sit on. Make the first commit yourself.';
|
|
176
|
+
else if (blocked.length > 0)
|
|
177
|
+
plan.problem = `${blocked.length} change(s) can be neither taken nor left out: ${blocked.map((e) => `${e.path} (${e.reason})`).join('; ')}.`;
|
|
178
|
+
// The session's own spelling of the folder, which is what other sessions are matched by.
|
|
179
|
+
else
|
|
180
|
+
plan.problem = await startProblem(session, repoDir, plan.branch, plan.head, allSessions);
|
|
181
|
+
if (!plan.problem) {
|
|
182
|
+
const prefix = session.vcs.branchPrefix || 'cop/';
|
|
183
|
+
plan.baselineBranch = await freeBranchName(top, branchNameFrom(['baseline'], prefix) + `/${session.id}`);
|
|
184
|
+
if (!(await isValidBranchName(top, plan.baselineBranch)))
|
|
185
|
+
plan.problem = `"${plan.baselineBranch}" is not a name git accepts.`;
|
|
186
|
+
}
|
|
187
|
+
plan.ok = !plan.problem;
|
|
188
|
+
return plan;
|
|
189
|
+
}
|
|
190
|
+
/** A path as a pattern for `.git/info/exclude` that matches that one file and nothing else. */
|
|
191
|
+
function excludePattern(path) {
|
|
192
|
+
return `/${path.replace(/[\\*?[]/g, (c) => `\\${c}`).replace(/ $/, '\\ ')}`;
|
|
193
|
+
}
|
|
194
|
+
/** Adds the files to the repository's own exclude list, under a line that says who and why. */
|
|
195
|
+
async function leaveOut(top, session, paths) {
|
|
196
|
+
if (paths.length === 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
const where = await git(top, ['rev-parse', '--git-path', 'info/exclude']);
|
|
199
|
+
if (!where.ok || !where.stdout)
|
|
200
|
+
return 'the repository\'s exclude file could not be found';
|
|
201
|
+
const file = resolve(top, where.stdout);
|
|
202
|
+
await mkdir(dirname(file), { recursive: true });
|
|
203
|
+
const before = await readFile(file, 'utf8').catch(() => '');
|
|
204
|
+
const block = [
|
|
205
|
+
`# copilot-operator: left out of the starting snapshot of session "${session.name.replace(/[\r\n]+/g, ' ')}" (${session.id}), ${new Date().toISOString()}`,
|
|
206
|
+
...paths.map(excludePattern),
|
|
207
|
+
'',
|
|
208
|
+
].join('\n');
|
|
209
|
+
try {
|
|
210
|
+
await writeFile(file, `${before}${before && !before.endsWith('\n') ? '\n' : ''}${block}`, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
return `the exclude file could not be written: ${err.message}`;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
/** The commit message: what was taken, what was left out, from where, and on whose word. */
|
|
218
|
+
function snapshotMessage(session, plan, included, leftOut, approved) {
|
|
219
|
+
const list = (paths) => [...paths.slice(0, 200).map((p) => `- ${p}`), ...(paths.length > 200 ? [`- and ${paths.length - 200} more`] : [])].join('\n');
|
|
220
|
+
return [
|
|
221
|
+
'Capture operator baseline before run',
|
|
222
|
+
'',
|
|
223
|
+
`The uncommitted changes in the repository when session "${session.name}" was first run, committed so that`,
|
|
224
|
+
"the session starts from them and every task's commit shows only that task's work.",
|
|
225
|
+
'',
|
|
226
|
+
`Taken (${included.length}):`,
|
|
227
|
+
list(included),
|
|
228
|
+
...(leftOut.length > 0 ? ['', `Left out, in .git/info/exclude (${leftOut.length}):`, list(leftOut)] : []),
|
|
229
|
+
'',
|
|
230
|
+
`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)'}.`,
|
|
231
|
+
'Committed by copilot-operator. Not pushed.',
|
|
232
|
+
'',
|
|
233
|
+
].join('\n');
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Takes the snapshot: leaves out what is to be left out, cuts the baseline branch, commits the rest.
|
|
237
|
+
*
|
|
238
|
+
* `choices` is the operator's answer, one per file of the list they were shown; a list that no
|
|
239
|
+
* longer matches the repository is refused, because what was approved is not what is there. Without
|
|
240
|
+
* `approved`, every file takes its default and anything to be left out refuses (see `DirtyWorktree`).
|
|
241
|
+
*/
|
|
242
|
+
export async function takeSnapshot(session, opts, bus, saveSession, allSessions = async () => []) {
|
|
243
|
+
if (dirtyPolicy(session.vcs).policy === 'reject') {
|
|
244
|
+
return { ok: false, problem: 'this session does not take uncommitted changes as a snapshot. Choose that under Version control → "Uncommitted changes" first.' };
|
|
245
|
+
}
|
|
246
|
+
if (session.vcsBaseCommit)
|
|
247
|
+
return { ok: false, problem: 'this session has already started; a snapshot is taken only before its first task.' };
|
|
248
|
+
const plan = await planSnapshot(session, allSessions);
|
|
249
|
+
if (!plan.needed)
|
|
250
|
+
return { ok: false, problem: 'there is nothing uncommitted to take.' };
|
|
251
|
+
if (!plan.ok || !plan.baselineBranch || !plan.head)
|
|
252
|
+
return { ok: false, problem: plan.problem ?? 'the snapshot cannot be taken.' };
|
|
253
|
+
const chosen = new Map();
|
|
254
|
+
if (opts.approved) {
|
|
255
|
+
const choices = opts.choices ?? {};
|
|
256
|
+
const shown = Object.keys(choices).sort();
|
|
257
|
+
const now = plan.entries.map((e) => e.path).sort();
|
|
258
|
+
if (JSON.stringify(shown) !== JSON.stringify(now)) {
|
|
259
|
+
return { ok: false, problem: 'the uncommitted files changed since the list was shown. Look at the list again; nothing was done.' };
|
|
260
|
+
}
|
|
261
|
+
for (const e of plan.entries) {
|
|
262
|
+
const c = choices[e.path];
|
|
263
|
+
if (!c || !e.allowed.includes(c))
|
|
264
|
+
return { ok: false, problem: `${e.path} cannot be "${c ?? 'nothing'}": ${e.reason ?? 'not allowed'}. Nothing was done.` };
|
|
265
|
+
chosen.set(e.path, c);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
for (const e of plan.entries)
|
|
270
|
+
chosen.set(e.path, e.choice);
|
|
271
|
+
const toExclude = plan.entries.filter((e) => e.kind === 'untracked' && chosen.get(e.path) === 'leave-out');
|
|
272
|
+
if (toExclude.length > 0) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
problem: `${toExclude.length} new file(s) would have to be left out of the snapshot (${toExclude.slice(0, 5).map((e) => `${e.path}: ${e.reason}`).join('; ')}), ` +
|
|
276
|
+
"and that writes to the repository's .git/info/exclude, which needs your approval. Approve the list on the session's page, or deal with the files yourself.",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const top = (await topOf(plan.repoDir));
|
|
281
|
+
const take = plan.entries.filter((e) => chosen.get(e.path) === 'include');
|
|
282
|
+
const leaveOutPaths = plan.entries.filter((e) => e.kind === 'untracked' && chosen.get(e.path) === 'leave-out').map((e) => e.path);
|
|
283
|
+
const leftOutAll = plan.entries.filter((e) => chosen.get(e.path) === 'leave-out').map((e) => e.path);
|
|
284
|
+
if (take.length === 0)
|
|
285
|
+
return { ok: false, problem: 'nothing would be taken: every file is left out. Leave them out yourself, or take at least one.' };
|
|
286
|
+
const excluded = await leaveOut(top, session, leaveOutPaths);
|
|
287
|
+
if (excluded)
|
|
288
|
+
return { ok: false, problem: excluded };
|
|
289
|
+
// With the left-out files gone from git's view, what is uncommitted must be exactly what is taken.
|
|
290
|
+
const after = await statusEntries(top);
|
|
291
|
+
const wanted = new Set(take.filter((e) => e.kind !== 'ignored').map((e) => e.path));
|
|
292
|
+
const stray = (after ?? []).filter((s) => !wanted.has(s.path)).map((s) => s.path);
|
|
293
|
+
if (!after || stray.length > 0) {
|
|
294
|
+
return { ok: false, problem: `the repository changed while the snapshot was being taken (${someOf(stray)}). Nothing was committed; look at the list again.` };
|
|
295
|
+
}
|
|
296
|
+
const branch = plan.baselineBranch;
|
|
297
|
+
const cut = await git(top, ['checkout', '-b', branch]);
|
|
298
|
+
if (!cut.ok)
|
|
299
|
+
return { ok: false, problem: `the branch ${branch} could not be made: ${cut.stderr || cut.stdout}` };
|
|
300
|
+
const back = async (why) => {
|
|
301
|
+
if (plan.branch)
|
|
302
|
+
await git(top, ['checkout', plan.branch]);
|
|
303
|
+
return { ok: false, problem: `${why} Nothing was committed; the repository is back on ${plan.branch ?? 'where it was'} with your changes as they were.` };
|
|
304
|
+
};
|
|
305
|
+
// Literal paths from a file: no glob in a name is read as a pattern, and no command line gets too long.
|
|
306
|
+
const list = join(tmpdir(), `cop-snapshot-${process.pid}-${Date.now()}.txt`);
|
|
307
|
+
const add = async (paths, force) => {
|
|
308
|
+
if (paths.length === 0)
|
|
309
|
+
return undefined;
|
|
310
|
+
await writeFile(list, `${paths.join('\0')}\0`, 'utf8');
|
|
311
|
+
const r = await git(top, ['--literal-pathspecs', 'add', ...(force ? ['-f'] : ['-A']), `--pathspec-from-file=${list}`, '--pathspec-file-nul']);
|
|
312
|
+
return r.ok ? undefined : r.stderr || r.stdout || 'git add failed';
|
|
313
|
+
};
|
|
314
|
+
try {
|
|
315
|
+
const plain = take.filter((e) => e.kind !== 'ignored').flatMap((e) => (e.from ? [e.path, e.from] : [e.path]));
|
|
316
|
+
const failed = (await add(plain, false)) ?? (await add(take.filter((e) => e.kind === 'ignored').map((e) => e.path), true));
|
|
317
|
+
if (failed)
|
|
318
|
+
return await back(`the files could not be staged: ${failed}.`);
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
await rm(list, { force: true });
|
|
322
|
+
}
|
|
323
|
+
const message = snapshotMessage(session, plan, take.map((e) => e.path), leftOutAll, opts.approved);
|
|
324
|
+
const commit = await git(top, ['-c', 'user.name=copilot-operator', '-c', `user.email=${RUNNER_EMAIL}`, 'commit', '--no-verify', '-q', '-m', message]);
|
|
325
|
+
if (!commit.ok)
|
|
326
|
+
return await back(`the snapshot could not be committed: ${commit.stderr || commit.stdout}.`);
|
|
327
|
+
const sha = (await git(top, ['rev-parse', 'HEAD'])).stdout;
|
|
328
|
+
const start = {
|
|
329
|
+
kind: 'snapshot',
|
|
330
|
+
commit: sha,
|
|
331
|
+
branch,
|
|
332
|
+
snapshot: {
|
|
333
|
+
...(plan.branch ? { fromBranch: plan.branch } : {}),
|
|
334
|
+
fromCommit: plan.head,
|
|
335
|
+
included: take.map((e) => e.path).slice(0, 500),
|
|
336
|
+
leftOut: leftOutAll.slice(0, 500),
|
|
337
|
+
approved: opts.approved,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
await saveSession((s) => {
|
|
341
|
+
s.vcsBaseCommit = sha;
|
|
342
|
+
s.vcsStart = start;
|
|
343
|
+
});
|
|
344
|
+
bus.publish({
|
|
345
|
+
sessionId: session.id,
|
|
346
|
+
type: 'vcs-snapshot',
|
|
347
|
+
level: 'info',
|
|
348
|
+
message: `your uncommitted changes are now the commit ${sha.slice(0, 8)} on ${branch} (${take.length} file(s)` +
|
|
349
|
+
`${leftOutAll.length > 0 ? `, ${leftOutAll.length} left out` : ''}); ${plan.branch ?? 'the branch you were on'} itself is unchanged. ` +
|
|
350
|
+
'The session starts from that commit.',
|
|
351
|
+
data: { branch, commit: sha, included: take.length, leftOut: leftOutAll.length },
|
|
352
|
+
});
|
|
353
|
+
return { ok: true, start };
|
|
354
|
+
}
|
package/dist/src/vcs/taskVcs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { findSuspicious } from './commitHygiene.js';
|
|
2
|
+
import { dirtyPolicy, planSnapshot, someOf, takeSnapshot } from './snapshot.js';
|
|
2
3
|
import { branchExists, branchNameFrom, describeUpdate, updateFromRemote, localBranches, commitAll, commitFiles, commitsBetween, commitSubject, createBranch, foreignCommits, isAncestor, checkoutExisting, freeBranchName, git, isValidBranchName, plannedBranchName, repoState } from './git.js';
|
|
3
4
|
/** Which repository a session works in: its own setting, else the project it mirrors. */
|
|
4
5
|
export function repoDirOf(session) {
|
|
@@ -30,7 +31,7 @@ allSessions = async () => []) {
|
|
|
30
31
|
if (!settings?.enabled)
|
|
31
32
|
return { vcs: {}, note: '' };
|
|
32
33
|
const dir = repoDirOf(session);
|
|
33
|
-
|
|
34
|
+
let state = await repoState(dir);
|
|
34
35
|
if (!state.isRepo) {
|
|
35
36
|
const problem = state.problem ?? 'The repository could not be read.';
|
|
36
37
|
bus.publish({ sessionId: session.id, taskId: task.id, type: 'vcs-unavailable', level: 'warn',
|
|
@@ -39,8 +40,37 @@ allSessions = async () => []) {
|
|
|
39
40
|
}
|
|
40
41
|
if (settings.startFrom === 'existing-branch')
|
|
41
42
|
return await onExistingBranch(session, task, dir, state, bus, saveSession);
|
|
43
|
+
// The session's base is fixed the first time it runs: every per-task branch is cut from it,
|
|
44
|
+
// which is what makes the tasks independent of each other rather than of the calendar. Where
|
|
45
|
+
// it is taken from is the session's `startFrom`; see `sessionStart`.
|
|
46
|
+
let base = session.vcsBaseCommit;
|
|
47
|
+
let start = session.vcsStart;
|
|
48
|
+
/*
|
|
49
|
+
* The operator's own changes before the session's first task, under a snapshot policy: they
|
|
50
|
+
* become the commit the session starts from (see `snapshot.ts`). Asked for approval, the run is
|
|
51
|
+
* refused until the list has been approved on the session's page; running without version
|
|
52
|
+
* control instead would be the one outcome the operator chose against.
|
|
53
|
+
*/
|
|
54
|
+
const dirty = dirtyPolicy(settings);
|
|
55
|
+
if (state.dirty && !base && dirty.policy !== 'reject') {
|
|
56
|
+
const refuse = (why) => {
|
|
57
|
+
bus.publish({ sessionId: session.id, taskId: task.id, type: 'vcs-refused', level: 'error', message: why });
|
|
58
|
+
return { vcs: { problem: why }, note: '', refuse: why };
|
|
59
|
+
};
|
|
60
|
+
if (dirty.requireApproval) {
|
|
61
|
+
return refuse(`the repository has uncommitted changes (${someOf(state.changed)}), and this session takes them as its starting snapshot ` +
|
|
62
|
+
'once you have approved the list: on the session\'s page, Version control → "Uncommitted changes", choose what goes in and take the snapshot. Nothing was run.');
|
|
63
|
+
}
|
|
64
|
+
const taken = await takeSnapshot(session, { approved: false }, bus, saveSession, allSessions);
|
|
65
|
+
if (!taken.ok)
|
|
66
|
+
return refuse(`the starting snapshot of the uncommitted changes was not taken: ${taken.problem} Nothing was run.`);
|
|
67
|
+
base = taken.start.commit;
|
|
68
|
+
start = taken.start;
|
|
69
|
+
state = await repoState(dir);
|
|
70
|
+
}
|
|
42
71
|
// A dirty tree is the operator's own work in progress. Committing it under the bot's name
|
|
43
|
-
// or moving it to another branch would both be decisions that are not ours to make
|
|
72
|
+
// or moving it to another branch would both be decisions that are not ours to make — unless
|
|
73
|
+
// the operator made it, with a snapshot policy, above.
|
|
44
74
|
if (state.dirty) {
|
|
45
75
|
const problem = `the repository has uncommitted changes (${state.changed.slice(0, 5).join(', ')}` +
|
|
46
76
|
`${state.changed.length > 5 ? `, and ${state.changed.length - 5} more` : ''}). ` +
|
|
@@ -49,11 +79,6 @@ allSessions = async () => []) {
|
|
|
49
79
|
message: `version control is on but inactive: ${problem}` });
|
|
50
80
|
return { vcs: { problem }, note: '' };
|
|
51
81
|
}
|
|
52
|
-
// The session's base is fixed the first time it runs: every per-task branch is cut from it,
|
|
53
|
-
// which is what makes the tasks independent of each other rather than of the calendar. Where
|
|
54
|
-
// it is taken from is the session's `startFrom`; see `sessionStart`.
|
|
55
|
-
let base = session.vcsBaseCommit;
|
|
56
|
-
let start = session.vcsStart;
|
|
57
82
|
if (!base) {
|
|
58
83
|
const resolved = await sessionStart(session, dir, state.head, allSessions);
|
|
59
84
|
if ('problem' in resolved) {
|
|
@@ -139,7 +164,8 @@ async function onExistingBranch(session, task, dir, state, bus, saveSession) {
|
|
|
139
164
|
}
|
|
140
165
|
if (state.dirty) {
|
|
141
166
|
return refuse(`the repository has uncommitted changes (${state.changed.slice(0, 5).join(', ')}${state.changed.length > 5 ? `, and ${state.changed.length - 5} more` : ''}), ` +
|
|
142
|
-
`so the work cannot be put on "${wanted}" without taking them along. Commit or stash them first. Nothing was run.`
|
|
167
|
+
`so the work cannot be put on "${wanted}" without taking them along. Commit or stash them first. Nothing was run.` +
|
|
168
|
+
(dirtyPolicy(session.vcs).policy !== 'reject' ? ' A starting snapshot does not apply here: it is a branch of its own, and this session works on the branch you named.' : ''));
|
|
143
169
|
}
|
|
144
170
|
let start = session.vcsStart;
|
|
145
171
|
if (!session.vcsBaseCommit || start?.kind !== 'existing-branch' || start.branch !== wanted) {
|
|
@@ -233,13 +259,15 @@ function normalise(dir) {
|
|
|
233
259
|
/** One line for the log and the event stream: where this session starts, and why. */
|
|
234
260
|
export function describeStart(start) {
|
|
235
261
|
const at = start.commit.slice(0, 8);
|
|
236
|
-
const said = start.kind === '
|
|
237
|
-
? `this session
|
|
238
|
-
: start.kind === '
|
|
239
|
-
? `this session
|
|
240
|
-
: start.kind === 'branch'
|
|
241
|
-
? `this session
|
|
242
|
-
:
|
|
262
|
+
const said = start.kind === 'snapshot'
|
|
263
|
+
? `this session starts from a snapshot of your uncommitted changes: ${start.branch} (${at}), taken on ${start.snapshot?.fromBranch ?? 'a detached HEAD'} at ${(start.snapshot?.fromCommit ?? '').slice(0, 8)}`
|
|
264
|
+
: start.kind === 'previous-session'
|
|
265
|
+
? `this session continues "${start.fromSession?.name ?? '?'}": it starts from the end of its branch ${start.branch} (${at})`
|
|
266
|
+
: start.kind === 'existing-branch'
|
|
267
|
+
? `this session carries on the existing branch ${start.branch}, from its tip ${at}`
|
|
268
|
+
: start.kind === 'branch'
|
|
269
|
+
? `this session starts from the local branch ${start.branch} (${at})`
|
|
270
|
+
: `this session starts from where the repository was (${at})`;
|
|
243
271
|
const updated = start.update ? ` (${describeUpdate(start.update)})` : '';
|
|
244
272
|
return start.note ? `${said}${updated} — ${start.note}` : `${said}${updated}`;
|
|
245
273
|
}
|
|
@@ -355,13 +383,15 @@ export function noteFor(repoDir, vcs, ctx = { mode: 'per-task', earlier: [] }) {
|
|
|
355
383
|
* session's files in its tree, and a model not told so reads them as something to redo or
|
|
356
384
|
* doubt; one that starts from `main` has none of them, and a model not told so goes looking.
|
|
357
385
|
*/
|
|
358
|
-
const began = ctx.start?.kind === '
|
|
359
|
-
? `This session
|
|
360
|
-
: ctx.start?.kind === '
|
|
361
|
-
? `This session
|
|
362
|
-
: ctx.start?.kind === 'branch'
|
|
363
|
-
? `This session
|
|
364
|
-
: ''
|
|
386
|
+
const began = ctx.start?.kind === 'snapshot'
|
|
387
|
+
? `This session started from \`${ctx.start.branch}\`: a commit of the operator's own uncommitted changes, taken before the run so that your work is kept apart from theirs. Those files are in your working tree as the starting point; they are not work for you to redo or undo.`
|
|
388
|
+
: ctx.start?.kind === 'previous-session'
|
|
389
|
+
? `This session continues the work of the earlier session "${ctx.start.fromSession?.name ?? ''}": it was started from the end of its branch \`${ctx.start.branch}\`, so that session's work is already in your working tree. Build on it; do not redo it.`
|
|
390
|
+
: ctx.start?.kind === 'existing-branch'
|
|
391
|
+
? `This session carries on the existing branch \`${ctx.start.branch}\`: the work already on it is in your working tree. Build on it; do not redo it.`
|
|
392
|
+
: ctx.start?.kind === 'branch'
|
|
393
|
+
? `This session started from the local branch \`${ctx.start.branch}\`, not from any earlier session's work${ctx.start.note ? ` (${ctx.start.note})` : ''}.`
|
|
394
|
+
: '';
|
|
365
395
|
return [
|
|
366
396
|
'## Version control',
|
|
367
397
|
'',
|
|
@@ -661,7 +691,7 @@ export async function restoreToBase(session, task, bus) {
|
|
|
661
691
|
};
|
|
662
692
|
}
|
|
663
693
|
/** What the UI shows before a run: is version control going to work here? */
|
|
664
|
-
export async function vcsPreflight(session) {
|
|
694
|
+
export async function vcsPreflight(session, allSessions = async () => []) {
|
|
665
695
|
const settings = session.vcs;
|
|
666
696
|
if (!settings?.enabled)
|
|
667
697
|
return { ok: false, repoDir: '', problem: 'off' };
|
|
@@ -669,6 +699,21 @@ export async function vcsPreflight(session) {
|
|
|
669
699
|
const state = await repoState(repoDir);
|
|
670
700
|
if (!state.isRepo)
|
|
671
701
|
return { ok: false, repoDir, problem: state.problem };
|
|
702
|
+
// Before the first task, under a snapshot policy: the list to approve, file by file.
|
|
703
|
+
if (state.dirty && !session.vcsBaseCommit && dirtyPolicy(settings).policy !== 'reject' && settings.startFrom !== 'existing-branch') {
|
|
704
|
+
const snapshot = await planSnapshot(session, allSessions);
|
|
705
|
+
if (snapshot.needed) {
|
|
706
|
+
return {
|
|
707
|
+
ok: false,
|
|
708
|
+
repoDir,
|
|
709
|
+
branch: state.branch ?? undefined,
|
|
710
|
+
problem: snapshot.ok
|
|
711
|
+
? `There are uncommitted changes (${someOf(state.changed, 3)}). They become the session's starting snapshot ${snapshot.requireApproval ? 'once you approve the list below' : 'when its first task starts'}.`
|
|
712
|
+
: `There are uncommitted changes, and no starting snapshot can be taken: ${snapshot.problem}`,
|
|
713
|
+
snapshot,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
672
717
|
if (state.dirty) {
|
|
673
718
|
return {
|
|
674
719
|
ok: false,
|
package/dist/web/404.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!DOCTYPE html><!--
|
|
1
|
+
<!DOCTYPE html><!--kkxFcGzA9Uo1uRAgcoDti--><html lang="en" data-theme="light" data-textsize="normal" data-contrast="normal" data-motion="normal" data-links="plain" data-focus="normal"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/c37978b44194c37b.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-0f31b926d9a9c5a4.js"/><script src="/_next/static/chunks/c7879cf7-b5ab1053c1d9a2e7.js" async=""></script><script src="/_next/static/chunks/131-bae98f790a541679.js" async=""></script><script src="/_next/static/chunks/main-app-e3a5f713ba31557e.js" async=""></script><script src="/_next/static/chunks/197-f7c33a0114fc3db3.js" async=""></script><script src="/_next/static/chunks/867-7f27ef819d251cc5.js" async=""></script><script src="/_next/static/chunks/549-a13ae394c5e6a837.js" async=""></script><script src="/_next/static/chunks/app/layout-560bd6d173302e5a.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>copilot-operator</title><meta name="description" content="Runs task loops with Microsoft 365 Copilot on this machine."/><script>
|
|
2
2
|
(function () {
|
|
3
3
|
try {
|
|
4
4
|
var d = document.documentElement;
|
|
@@ -13,4 +13,4 @@
|
|
|
13
13
|
d.dataset.focus = a.strongFocus ? 'strong' : 'normal';
|
|
14
14
|
} catch (e) {}
|
|
15
15
|
})();
|
|
16
|
-
</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><a class="skip-link" href="#main">Skip to the content</a><div class="wrap"><header class="top"><h1><a href="/">copilot-operator</a></h1><nav aria-label="Main"><a href="/"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M4 5h16v4H4zM4 11h16v4H4zM4 17h10v2H4z"></path></svg><span>Sessions</span></a><a href="/defaults"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm8.5 4 1.5 1-1.6 2.8-1.8-.4a7 7 0 0 1-1.5 1.5l.4 1.8L14.7 21l-1-1.5a7 7 0 0 1-3.4 0l-1 1.5-2.8-1.6.4-1.8A7 7 0 0 1 5.4 16l-1.8.4L2 13.6 3.5 12.6V11.4L2 10.4l1.6-2.8 1.8.4A7 7 0 0 1 6.9 6.5l-.4-1.8L9.3 3l1 1.5a7 7 0 0 1 3.4 0l1-1.5 2.8 1.6-.4 1.8a7 7 0 0 1 1.5 1.5l1.8-.4L22 10.4l-1.5 1z"></path></svg><span>Settings</span></a><a href="/import"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3v10m0 0 4-4m-4 4-4-4M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span>Plan from JSON</span></a><a href="/history"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3a9 9 0 1 0 8.5 6M12 7v5l3 2M3 4v5h5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span>Task register</span></a><a href="/level1"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M6 3h9l5 5v13H6zM14 3v6h6M9 13h7M9 17h7" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>Level 1 prompt</span></a><a href="/presets"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M5 4h14v4H5zM5 10h14v4H5zM5 16h9v4H5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>Level 2 presets</span></a><a href="/appearance"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3a9 9 0 0 0 0 18c1.5 0 2-1 2-2s-1-1.5-1-2.5S14 15 15.5 15H17a4 4 0 0 0 4-4 8 8 0 0 0-9-8zM8 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm4-3a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm4 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"></path></svg><span>Appearance</span></a><a href="/system"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M4 5h16v10H4zM2 19h20M9 15v4M15 15v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>System</span></a><select aria-label="Language" style="width:auto"><option value="en" selected="">English</option><option value="bg">Български</option></select></nav></header><main id="main"><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--></main></div><script src="/_next/static/chunks/webpack-0f31b926d9a9c5a4.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"themeScript\"]\n3:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"AppearanceProvider\"]\n4:I[867,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"LanguageProvider\"]\n5:I[1035,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"DialogHost\"]\n6:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"SkipLink\"]\n7:I[8197,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"\"]\n8:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"Nav\"]\n9:I[6657,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"GlobalApprovals\"]\na:I[5204,[],\"\"]\nb:I[2642,[],\"\"]\nc:I[7712,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-8b95ae984befc0e2.js\",\"549\",\"static/chunks/549-9548d9a132c8c7d4.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"ScrollJump\"]\nd:I[433,[],\"OutletBoundary\"]\nf:I[9884,[],\"AsyncMeta"])</script><script>self.__next_f.push([1,"dataOutlet\"]\n11:I[433,[],\"ViewportBoundary\"]\n13:I[433,[],\"MetadataBoundary\"]\n14:\"$Sreact.suspense\"\n16:I[3536,[],\"\"]\n:HL[\"/_next/static/css/e2a2c9eb3c226313.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"IwNZiBrEMAcXEUFj2miiU\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e2a2c9eb3c226313.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"suppressHydrationWarning\":true,\"data-theme\":\"light\",\"data-textsize\":\"normal\",\"data-contrast\":\"normal\",\"data-motion\":\"normal\",\"data-links\":\"plain\",\"data-focus\":\"normal\",\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"$2\"}}]}],[\"$\",\"body\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"children\":[\"$\",\"$L5\",null,{\"children\":[[\"$\",\"$L6\",null,{}],[\"$\",\"div\",null,{\"className\":\"wrap\",\"children\":[[\"$\",\"header\",null,{\"className\":\"top\",\"children\":[[\"$\",\"h1\",null,{\"children\":[\"$\",\"$L7\",null,{\"href\":\"/\",\"children\":\"copilot-operator\"}]}],[\"$\",\"$L8\",null,{}]]}],[\"$\",\"$L9\",null,{}],[\"$\",\"main\",null,{\"id\":\"main\",\"children\":[\"$\",\"$La\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$Lb\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]]}],[\"$\",\"$Lc\",null,{}]]}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$La\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$Lb\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$Ld\",null,{\"children\":[\"$Le\",[\"$\",\"$Lf\",null,{\"promise\":\"$@10\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L11\",null,{\"children\":\"$L12\"}],null],[\"$\",\"$L13\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$14\",null,{\"fallback\":null,\"children\":\"$L15\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$16\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"12:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\ne:null\n"])</script><script>self.__next_f.push([1,"10:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"copilot-operator\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Runs task loops with Microsoft 365 Copilot on this machine.\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"15:\"$10:metadata\"\n"])</script></body></html>
|
|
16
|
+
</script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><a class="skip-link" href="#main">Skip to the content</a><div class="wrap"><header class="top"><h1><a href="/">copilot-operator</a></h1><nav aria-label="Main"><a href="/"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M4 5h16v4H4zM4 11h16v4H4zM4 17h10v2H4z"></path></svg><span>Sessions</span></a><a href="/defaults"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm8.5 4 1.5 1-1.6 2.8-1.8-.4a7 7 0 0 1-1.5 1.5l.4 1.8L14.7 21l-1-1.5a7 7 0 0 1-3.4 0l-1 1.5-2.8-1.6.4-1.8A7 7 0 0 1 5.4 16l-1.8.4L2 13.6 3.5 12.6V11.4L2 10.4l1.6-2.8 1.8.4A7 7 0 0 1 6.9 6.5l-.4-1.8L9.3 3l1 1.5a7 7 0 0 1 3.4 0l1-1.5 2.8 1.6-.4 1.8a7 7 0 0 1 1.5 1.5l1.8-.4L22 10.4l-1.5 1z"></path></svg><span>Settings</span></a><a href="/import"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3v10m0 0 4-4m-4 4-4-4M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span>Plan from JSON</span></a><a href="/history"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3a9 9 0 1 0 8.5 6M12 7v5l3 2M3 4v5h5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span>Task register</span></a><a href="/level1"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M6 3h9l5 5v13H6zM14 3v6h6M9 13h7M9 17h7" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>Level 1 prompt</span></a><a href="/presets"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M5 4h14v4H5zM5 10h14v4H5zM5 16h9v4H5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>Level 2 presets</span></a><a href="/appearance"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3a9 9 0 0 0 0 18c1.5 0 2-1 2-2s-1-1.5-1-2.5S14 15 15.5 15H17a4 4 0 0 0 4-4 8 8 0 0 0-9-8zM8 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm4-3a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm4 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"></path></svg><span>Appearance</span></a><a href="/system"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M4 5h16v10H4zM2 19h20M9 15v4M15 15v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>System</span></a><select aria-label="Language" style="width:auto"><option value="en" selected="">English</option><option value="bg">Български</option></select></nav></header><main id="main"><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--></main></div><script src="/_next/static/chunks/webpack-0f31b926d9a9c5a4.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"themeScript\"]\n3:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"AppearanceProvider\"]\n4:I[867,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"LanguageProvider\"]\n5:I[1035,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"DialogHost\"]\n6:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"SkipLink\"]\n7:I[8197,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"\"]\n8:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"Nav\"]\n9:I[6657,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"GlobalApprovals\"]\na:I[5204,[],\"\"]\nb:I[2642,[],\"\"]\nc:I[7712,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-7f27ef819d251cc5.js\",\"549\",\"static/chunks/549-a13ae394c5e6a837.js\",\"177\",\"static/chunks/app/layout-560bd6d173302e5a.js\"],\"ScrollJump\"]\nd:I[433,[],\"OutletBoundary\"]\nf:I[9884,[],\"AsyncMeta"])</script><script>self.__next_f.push([1,"dataOutlet\"]\n11:I[433,[],\"ViewportBoundary\"]\n13:I[433,[],\"MetadataBoundary\"]\n14:\"$Sreact.suspense\"\n16:I[3536,[],\"\"]\n:HL[\"/_next/static/css/c37978b44194c37b.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"kkxFcGzA9Uo1uRAgcoDti\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/c37978b44194c37b.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"suppressHydrationWarning\":true,\"data-theme\":\"light\",\"data-textsize\":\"normal\",\"data-contrast\":\"normal\",\"data-motion\":\"normal\",\"data-links\":\"plain\",\"data-focus\":\"normal\",\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"$2\"}}]}],[\"$\",\"body\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"children\":[\"$\",\"$L5\",null,{\"children\":[[\"$\",\"$L6\",null,{}],[\"$\",\"div\",null,{\"className\":\"wrap\",\"children\":[[\"$\",\"header\",null,{\"className\":\"top\",\"children\":[[\"$\",\"h1\",null,{\"children\":[\"$\",\"$L7\",null,{\"href\":\"/\",\"children\":\"copilot-operator\"}]}],[\"$\",\"$L8\",null,{}]]}],[\"$\",\"$L9\",null,{}],[\"$\",\"main\",null,{\"id\":\"main\",\"children\":[\"$\",\"$La\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$Lb\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]]}],[\"$\",\"$Lc\",null,{}]]}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$La\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$Lb\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$Ld\",null,{\"children\":[\"$Le\",[\"$\",\"$Lf\",null,{\"promise\":\"$@10\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L11\",null,{\"children\":\"$L12\"}],null],[\"$\",\"$L13\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$14\",null,{\"fallback\":null,\"children\":\"$L15\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$16\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"12:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\ne:null\n"])</script><script>self.__next_f.push([1,"10:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"copilot-operator\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Runs task loops with Microsoft 365 Copilot on this machine.\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"15:\"$10:metadata\"\n"])</script></body></html>
|