copilot-operator 0.1.3 → 0.1.5

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.
Files changed (49) hide show
  1. package/README.md +8 -2
  2. package/dist/src/api/operator.controller.js +6 -1
  3. package/dist/src/api/operator.service.js +69 -22
  4. package/dist/src/exec/authorizer.js +2 -2
  5. package/dist/src/exec/commandHeads.js +799 -0
  6. package/dist/src/exec/policy.js +1 -1
  7. package/dist/src/exec/programs.js +22 -82
  8. package/dist/src/exec/reportFile.js +5 -2
  9. package/dist/src/orchestrator/review.js +4 -3
  10. package/dist/src/orchestrator/taskRunner.js +33 -8
  11. package/dist/src/plan/brief.js +1248 -1221
  12. package/dist/src/plan/schema.js +19 -0
  13. package/dist/src/plan/systemGuide.js +40 -0
  14. package/dist/src/protocol/reporter.js +8 -6
  15. package/dist/src/session/exports.js +131 -1
  16. package/dist/src/session/store.js +2 -0
  17. package/dist/src/vcs/taskVcs.js +153 -11
  18. package/dist/web/404.html +2 -2
  19. package/dist/web/_next/static/chunks/867-0cd2c3cfd0ff0a45.js +1 -0
  20. package/dist/web/_next/static/chunks/89-382e9b29486d2747.js +1 -0
  21. package/dist/web/_next/static/chunks/app/defaults/page-1747c931adc746fd.js +1 -0
  22. package/dist/web/_next/static/chunks/app/history/page-83543937cecde5de.js +1 -0
  23. package/dist/web/_next/static/chunks/app/sessions/view/page-4631bdf5113a73b2.js +1 -0
  24. package/dist/web/appearance.html +2 -2
  25. package/dist/web/appearance.txt +10 -10
  26. package/dist/web/defaults.html +2 -2
  27. package/dist/web/defaults.txt +10 -10
  28. package/dist/web/history.html +2 -2
  29. package/dist/web/history.txt +10 -10
  30. package/dist/web/import.html +2 -2
  31. package/dist/web/import.txt +10 -10
  32. package/dist/web/index.html +2 -2
  33. package/dist/web/index.txt +10 -10
  34. package/dist/web/level1.html +2 -2
  35. package/dist/web/level1.txt +10 -10
  36. package/dist/web/presets.html +2 -2
  37. package/dist/web/presets.txt +10 -10
  38. package/dist/web/sessions/view.html +2 -2
  39. package/dist/web/sessions/view.txt +10 -10
  40. package/dist/web/system.html +2 -2
  41. package/dist/web/system.txt +10 -10
  42. package/package.json +4 -3
  43. package/dist/web/_next/static/chunks/867-55627c5122ff6336.js +0 -1
  44. package/dist/web/_next/static/chunks/89-5585315f51e4c481.js +0 -1
  45. package/dist/web/_next/static/chunks/app/defaults/page-4ff25cafe6de779c.js +0 -1
  46. package/dist/web/_next/static/chunks/app/history/page-2232949a2ff8559e.js +0 -1
  47. package/dist/web/_next/static/chunks/app/sessions/view/page-9dda275f4b1b0423.js +0 -1
  48. /package/dist/web/_next/static/{QwjyyBjkO5xlKFU5O6Wux → 7TE2rEuHqGdGx14If8TEb}/_buildManifest.js +0 -0
  49. /package/dist/web/_next/static/{QwjyyBjkO5xlKFU5O6Wux → 7TE2rEuHqGdGx14If8TEb}/_ssgManifest.js +0 -0
@@ -38,6 +38,18 @@ const VcsInput = z
38
38
  branchPrefix: z.string().optional(),
39
39
  /** The name of the one branch a per-session run works on. Ignored in per-task mode. */
40
40
  branchName: z.string().optional(),
41
+ /**
42
+ * Where the session's first branch is cut from: a local branch (`baseBranch`, `main` unless
43
+ * said otherwise), or the end of the branch of the session before it in the same repository.
44
+ * Absent keeps the old behaviour, wherever the repository is. See `VersionControl.startFrom`.
45
+ */
46
+ startFrom: z
47
+ .enum(['branch', 'previous-session', 'head'], {
48
+ error: 'startFrom must be "branch" (every session starts from the local branch in baseBranch, "main" unless said otherwise) or "previous-session" (each session carries on from the branch of the session before it in the same repository).',
49
+ })
50
+ .optional(),
51
+ /** The local branch `startFrom: "branch"` starts from, and `previous-session`'s fallback. */
52
+ baseBranch: z.string().trim().optional(),
41
53
  }, {
42
54
  error: 'Every session needs a "vcs" object saying whether the runner does version control for it and, when it does, which git repository to work in. Ask the user both questions and write the answers here.',
43
55
  })
@@ -195,6 +207,13 @@ const TaskInput = z.object({
195
207
  * branch so nothing is lost and the next task starts clean.
196
208
  */
197
209
  readOnly: z.boolean().default(false),
210
+ /**
211
+ * What earlier attempts of this task were asked, written by the plan export for a task that has
212
+ * been run more than once (`earlierPlans` in `session/exports.ts`). History, not instruction:
213
+ * accepted so that an exported plan imports again without a warning about it, and then ignored —
214
+ * an imported task starts from its current text, with no attempts behind it.
215
+ */
216
+ earlierAttempts: z.array(z.unknown()).optional(),
198
217
  });
199
218
  const SessionInput = z.object({
200
219
  name: z
@@ -804,6 +804,38 @@ export const SYSTEM_GUIDE = [
804
804
  doesEn: 'Every task continues on the branch the task before it left, so later work builds on earlier work.',
805
805
  doesBg: 'Всяка задача продължава на клона от предишната, така че по-късната работа стъпва на по-ранната.',
806
806
  },
807
+ {
808
+ key: 'vcs.startBranch',
809
+ kind: 'radio',
810
+ en: 'From a local branch (main)',
811
+ bg: 'От локален клон (main)',
812
+ doesEn: 'The session’s first branch is cut from the local branch in "Local branch to start from" (main when empty), so every session set this way starts from the same clean code. In a plan: "startFrom": "branch" with "baseBranch".',
813
+ doesBg: 'Първият клон на сесията се отрязва от локалния клон в „Локален клон, от който да тръгне“ (main, ако е празно), така че всяка сесия с този избор тръгва от един и същ чист код. В план: "startFrom": "branch" с "baseBranch".',
814
+ },
815
+ {
816
+ key: 'vcs.startPrevious',
817
+ kind: 'radio',
818
+ en: 'From the previous session’s branch',
819
+ bg: 'От клона на предната сесия',
820
+ doesEn: 'The session carries on from the end of the branch of the session that last committed work in the same repository — in a run, the one before it — so sessions form a chain; with none, it starts from the local branch. In a plan: "startFrom": "previous-session".',
821
+ doesBg: 'Сесията продължава от края на клона на сесията, която последна е комитнала работа в същото хранилище — при пускане, предната — така че сесиите образуват верига; ако няма такава, тръгва от локалния клон. В план: "startFrom": "previous-session".',
822
+ },
823
+ {
824
+ key: 'vcs.startHead',
825
+ kind: 'radio',
826
+ en: 'From wherever the repository is (as until now)',
827
+ bg: 'Откъдето е хранилището (както досега)',
828
+ doesEn: 'The old behaviour, and what a session without a choice does: whatever branch is checked out at its first run, often the one the previous session left.',
829
+ doesBg: 'Старото поведение и това, което прави сесия без избор: какъвто клон е избран при първото ѝ пускане, често оставеният от предната сесия.',
830
+ },
831
+ {
832
+ key: 'vcs.baseBranch',
833
+ kind: 'field',
834
+ en: 'Local branch to start from',
835
+ bg: 'Локален клон, от който да тръгне',
836
+ doesEn: 'The branch "From a local branch" starts from, and where a chain begins when there is no earlier session; main when empty.',
837
+ doesBg: 'Клонът, от който тръгва „От локален клон“, и откъдето започва веригата, когато няма по-ранна сесия; main, ако е празно.',
838
+ },
807
839
  {
808
840
  key: 'vcs.commit',
809
841
  kind: 'checkbox',
@@ -1481,6 +1513,14 @@ export const SYSTEM_GUIDE = [
1481
1513
  doesEn: 'Saves the log of one earlier attempt, rather than of the latest one, to the Desktop.',
1482
1514
  doesBg: 'Запазва на десктопа log-а на конкретен по-ранен опит, а не на последния.',
1483
1515
  },
1516
+ {
1517
+ key: 'save.attemptRecord',
1518
+ kind: 'button',
1519
+ en: 'download plan, work and runner of this attempt',
1520
+ bg: 'изтегли план, работа и runner на този опит',
1521
+ doesEn: 'Downloads one earlier attempt as it ran — the plan it was given, what happened to the work, what the runner did — as one file, even if the task was edited since. An attempt that did not end done also keeps the three as plan.json, work.json and runner.json in its run folder.',
1522
+ doesBg: 'Изтегля един по-ранен опит, както е вървял — планът, който е получил, какво е станало с работата, какво е направил runner-ът — в един файл, дори ако задачата е редактирана след това. Опит, който не е завършил готов, пази трите и като plan.json, work.json и runner.json в папката на пускането си.',
1523
+ },
1484
1524
  {
1485
1525
  key: 'reg.retriedFreshDone',
1486
1526
  kind: 'badge',
@@ -20,13 +20,15 @@ function outcomeSummary(r) {
20
20
  return `step ${r.id} hit its time limit`;
21
21
  case 'idle-timeout':
22
22
  return `step ${r.id} produced no output and was stopped`;
23
+ case 'refused':
24
+ // The runner declined it — a policy, a deny pattern, a repeat, damaged text, a shell that is
25
+ // not here — and it never reached a process, so it has no exit code to report. Said as a
26
+ // refusal, apart from a step that ran and exited non-zero: the move after one is to write
27
+ // the step differently, after the other it is to read the output and fix the work.
28
+ return `step ${r.id} was refused by the runner and never ran (no exit code); the reason is under it`;
23
29
  case 'aborted':
24
- // Exit -4 is `refusedResult`: the runner declined it — a deny pattern, a repeat, damaged
25
- // text, a shell that is not here — and it never reached a process. Calling all of those
26
- // "aborted by the operator" told the model a person had stopped it, which is a reason to
27
- // wait rather than to write the step again, and the reason it could act on was in the
28
- // stderr underneath a sentence contradicting it.
29
- return r.exitCode === -4 ? `step ${r.id} was not run; see why under it` : `step ${r.id} aborted by the operator`;
30
+ // A person stopped or skipped it — a reason to carry on without it, not to rewrite it.
31
+ return r.exitCode === -4 ? `step ${r.id} was not run: the operator skipped or stopped it` : `step ${r.id} aborted by the operator`;
30
32
  case 'spawn-error':
31
33
  return `step ${r.id} could not be started`;
32
34
  }
@@ -18,7 +18,8 @@
18
18
  * All three exist for one task and for one run, because those are the two sizes a question
19
19
  * comes in: "why did this one fail" and "what did this whole run do".
20
20
  */
21
- import { readFile } from 'node:fs/promises';
21
+ import { existsSync } from 'node:fs';
22
+ import { readFile, writeFile } from 'node:fs/promises';
22
23
  import { join } from 'node:path';
23
24
  import { withoutPersona } from '../plan/importPlan.js';
24
25
  const EXPECTED_HEADER = '### Expected result';
@@ -140,6 +141,9 @@ export function buildPlanExport(scope) {
140
141
  commitOnFinish: s.vcs?.commitOnFinish ?? true,
141
142
  branchPrefix: s.vcs?.branchPrefix ?? 'cop/',
142
143
  branchName: s.vcs?.branchName ?? '',
144
+ // Only when chosen: absent is the old behaviour, and writing it out would suggest a choice.
145
+ ...(s.vcs?.startFrom ? { startFrom: s.vcs.startFrom } : {}),
146
+ ...(s.vcs?.baseBranch ? { baseBranch: s.vcs.baseBranch } : {}),
143
147
  },
144
148
  review: { enabled: s.review?.enabled !== false, model: s.review?.model ?? '' },
145
149
  mirror: {
@@ -164,6 +168,9 @@ export function buildPlanExport(scope) {
164
168
  task.review = false;
165
169
  if (t.readOnly)
166
170
  task.readOnly = true;
171
+ const earlier = earlierPlans(t, level2);
172
+ if (earlier.length > 0)
173
+ task.earlierAttempts = earlier;
167
174
  return task;
168
175
  }),
169
176
  };
@@ -171,6 +178,129 @@ export function buildPlanExport(scope) {
171
178
  }),
172
179
  };
173
180
  }
181
+ /**
182
+ * What each earlier attempt of a task was asked, in the plan's own words.
183
+ *
184
+ * The plan is the task as it is now, and after "Edit and run again" that is not what the failed
185
+ * attempt ran: its text, its checks, its branch plan may all have changed. The attempt's own
186
+ * snapshot is here, beside it, so the plan of a failure can be read — and handed on — after the
187
+ * task has moved past it. Only what differs from the task as it stands is written out; an attempt
188
+ * that ran the same plan says so by its status alone. The importer ignores this key: a plan that
189
+ * comes back in is the current one.
190
+ */
191
+ function earlierPlans(t, sessionLevel2) {
192
+ const current = splitPrompt(t.prompt);
193
+ return (t.attempts ?? []).map((a, i) => {
194
+ const out = { attempt: i + 1, status: a.status };
195
+ if (a.reason)
196
+ out.reason = a.reason;
197
+ if (a.title !== t.title)
198
+ out.title = a.title;
199
+ const p = splitPrompt(a.prompt);
200
+ if (p.prompt !== current.prompt)
201
+ out.prompt = p.prompt;
202
+ if (p.expected !== current.expected)
203
+ out.expected = p.expected;
204
+ const aLevel2 = planLevel2({ ...t, level2: a.level2 });
205
+ if (aLevel2 !== planLevel2(t))
206
+ out.level2 = aLevel2 === sessionLevel2 ? '' : aLevel2;
207
+ if (a.checks && JSON.stringify(a.checks) !== JSON.stringify(t.checks ?? []))
208
+ out.checks = a.checks.map(planCheck);
209
+ if (a.vcsPlan && JSON.stringify(a.vcsPlan) !== JSON.stringify(t.vcsPlan ?? {}))
210
+ out.vcs = a.vcsPlan;
211
+ return out;
212
+ });
213
+ }
214
+ // ------------------------------------------------------------------------------------------
215
+ // one attempt, as it was
216
+ // ------------------------------------------------------------------------------------------
217
+ /**
218
+ * The task as it stood for one of its attempts: that attempt's text, checks, outcome and run
219
+ * folder, with only the attempts before it as its history. The current attempt is the task
220
+ * itself. Null for an attempt the task never had.
221
+ *
222
+ * This is what lets every view above be taken of a past attempt without a second set of
223
+ * builders: the views read a task, and this hands them the task that attempt was.
224
+ */
225
+ export function taskAtAttempt(task, attempt) {
226
+ const current = task.attempt ?? 1;
227
+ if (attempt === current)
228
+ return task;
229
+ if (!Number.isInteger(attempt) || attempt < 1 || attempt > current)
230
+ return null;
231
+ const a = task.attempts?.[attempt - 1];
232
+ if (!a)
233
+ return null;
234
+ return {
235
+ ...task,
236
+ attempt,
237
+ status: a.status,
238
+ runId: a.runId,
239
+ runGroup: a.runGroup,
240
+ startedAt: a.startedAt,
241
+ finishedAt: a.finishedAt,
242
+ iterations: a.iterations,
243
+ summary: a.summary,
244
+ reason: a.reason,
245
+ deviations: a.deviations,
246
+ disputes: a.disputes,
247
+ title: a.title,
248
+ prompt: a.prompt,
249
+ level2: a.level2,
250
+ checks: a.checks ?? task.checks,
251
+ vcsPlan: a.vcsPlan ?? task.vcsPlan,
252
+ checkResults: a.checkResults,
253
+ review: a.review,
254
+ vcs: a.vcs,
255
+ attempts: task.attempts?.slice(0, attempt - 1),
256
+ continuing: undefined,
257
+ finalReply: undefined,
258
+ firstMessage: undefined,
259
+ logFile: a.runId ? 'task-log.txt' : undefined,
260
+ };
261
+ }
262
+ /** The session with one of its tasks replaced, so a scope can be built around that version. */
263
+ export function withTask(session, task) {
264
+ return { ...session, tasks: session.tasks.map((t) => (t.id === task.id ? task : t)) };
265
+ }
266
+ /** What the runner export says about the machine, from the configuration in force. */
267
+ export function exportMachine(cfg) {
268
+ return {
269
+ node: process.versions.node,
270
+ platform: process.platform,
271
+ cwd: cfg.resolved.cwd,
272
+ limits: { ...cfg.limits, execution: { commandTimeoutSec: cfg.execution.commandTimeoutSec, idleTimeoutSec: cfg.execution.idleTimeoutSec, mode: cfg.execution.mode } },
273
+ };
274
+ }
275
+ /** The three files an attempt that did not end done leaves in its own run folder. */
276
+ export const ATTEMPT_RECORD_FILES = { plan: 'plan.json', work: 'work.json', runner: 'runner.json' };
277
+ /**
278
+ * Writes the plan, work and runner views of an attempt into its run folder, and returns the folder.
279
+ *
280
+ * For an attempt that did not end done, written as it ends. The same three views can be
281
+ * downloaded at any time — but only while the task's record exists and still describes that
282
+ * attempt: a deleted task or session takes the record with it, and the files are then the only
283
+ * account of what the failure was asked, did and ran into. The run folder outlives both (it goes
284
+ * only by `runsRetentionDays`), which is why they are kept there, beside the transcript they are
285
+ * read from. Null when the attempt has no run folder, which is an attempt that never started.
286
+ */
287
+ export async function writeAttemptRecord(session, task, runsDir, machine) {
288
+ if (!task.runId)
289
+ return null;
290
+ const dir = join(runsDir, task.runId);
291
+ if (!existsSync(dir))
292
+ return null;
293
+ const scope = {
294
+ sessions: [withTask(session, task)],
295
+ taskFilter: (_s, t) => t.id === task.id,
296
+ label: `${session.name}-${task.title}-attempt-${task.attempt ?? 1}`,
297
+ };
298
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
299
+ await writeFile(join(dir, ATTEMPT_RECORD_FILES.plan), json(buildPlanExport(scope)), 'utf8');
300
+ await writeFile(join(dir, ATTEMPT_RECORD_FILES.work), json(await buildDomainExport(scope, runsDir)), 'utf8');
301
+ await writeFile(join(dir, ATTEMPT_RECORD_FILES.runner), json(await buildBotExport(scope, runsDir, machine)), 'utf8');
302
+ return dir;
303
+ }
174
304
  async function readEvents(runsDir, runId) {
175
305
  if (!runId)
176
306
  return [];
@@ -335,6 +335,8 @@ export class SessionStore {
335
335
  prompt: t.prompt,
336
336
  level2: t.level2,
337
337
  checkResults: t.checkResults,
338
+ checks: t.checks,
339
+ vcsPlan: t.vcsPlan,
338
340
  review: t.review,
339
341
  vcs: t.vcs,
340
342
  },
@@ -1,5 +1,5 @@
1
1
  import { findSuspicious } from './commitHygiene.js';
2
- import { branchNameFrom, commitAll, commitFiles, commitsBetween, commitSubject, createBranch, checkoutExisting, freeBranchName, isValidBranchName, plannedBranchName, repoState } from './git.js';
2
+ import { branchNameFrom, commitAll, commitFiles, commitsBetween, commitSubject, createBranch, checkoutExisting, freeBranchName, git, isValidBranchName, plannedBranchName, repoState } from './git.js';
3
3
  /** Which repository a session works in: its own setting, else the project it mirrors. */
4
4
  export function repoDirOf(session) {
5
5
  return (session.vcs?.repoDir?.trim() || session.mirror.rootDir?.trim() || '').trim();
@@ -11,7 +11,9 @@ export function repoDirOf(session) {
11
11
  * on without it, not a reason to throw away a queue of work: the task itself may not even
12
12
  * touch the repository.
13
13
  */
14
- export async function prepareForTask(session, task, bus, saveSession) {
14
+ export async function prepareForTask(session, task, bus, saveSession,
15
+ /** Every session on record, for `startFrom: previous-session` to find the one before this. */
16
+ allSessions = async () => []) {
15
17
  const settings = session.vcs;
16
18
  if (!settings?.enabled)
17
19
  return { vcs: {}, note: '' };
@@ -34,13 +36,28 @@ export async function prepareForTask(session, task, bus, saveSession) {
34
36
  return { vcs: { problem }, note: '' };
35
37
  }
36
38
  // The session's base is fixed the first time it runs: every per-task branch is cut from it,
37
- // which is what makes the tasks independent of each other rather than of the calendar.
39
+ // which is what makes the tasks independent of each other rather than of the calendar. Where
40
+ // it is taken from is the session's `startFrom`; see `sessionStart`.
38
41
  let base = session.vcsBaseCommit;
39
- if (!base && state.head) {
40
- base = state.head;
41
- await saveSession((s) => {
42
- s.vcsBaseCommit = base;
43
- });
42
+ let start = session.vcsStart;
43
+ if (!base) {
44
+ const resolved = await sessionStart(session, dir, state.head, allSessions);
45
+ if ('problem' in resolved) {
46
+ bus.publish({ sessionId: session.id, taskId: task.id, type: 'vcs-problem', level: 'warn',
47
+ message: `version control is on but inactive: ${resolved.problem}` });
48
+ return { vcs: { problem: resolved.problem }, note: '' };
49
+ }
50
+ start = resolved.start;
51
+ base = start?.commit;
52
+ if (start) {
53
+ const chosen = start;
54
+ await saveSession((s) => {
55
+ s.vcsBaseCommit = chosen.commit;
56
+ s.vcsStart = chosen;
57
+ });
58
+ bus.publish({ sessionId: session.id, taskId: task.id, type: 'vcs-start', level: chosen.note ? 'warn' : 'info',
59
+ message: describeStart(chosen), data: { ...chosen } });
60
+ }
44
61
  }
45
62
  const attempt = task.attempt ?? 1;
46
63
  const prefix = settings.branchPrefix || 'cop/';
@@ -49,7 +66,7 @@ export async function prepareForTask(session, task, bus, saveSession) {
49
66
  const wanted = planned
50
67
  ? plannedBranchName(planned, prefix)
51
68
  : branchNameFrom([session.name, session.id.slice(0, 13)], prefix);
52
- return await switchTo(session, task, dir, wanted, base, bus, { reuseExisting: true });
69
+ return await switchTo(session, task, dir, wanted, base, bus, { reuseExisting: true, start });
53
70
  }
54
71
  // A name the task carries wins over one derived from its title: whoever wrote the plan knew
55
72
  // what the task was for, and a title is only ever a label for the list.
@@ -61,11 +78,91 @@ export async function prepareForTask(session, task, bus, saveSession) {
61
78
  // its work is what is being continued. See `continueTask` in the store.
62
79
  const previousBranch = task.continuing ? task.attempts?.at(-1)?.vcs?.branch : undefined;
63
80
  if (previousBranch)
64
- return await switchTo(session, task, dir, previousBranch, base, bus, { reuseExisting: true });
81
+ return await switchTo(session, task, dir, previousBranch, base, bus, { reuseExisting: true, start });
65
82
  // A re-run starts from where that task started the first time, not from where the previous
66
83
  // attempt ended. That is the whole point of recording the base commit.
67
84
  const from = firstAttemptBase(task) ?? base;
68
- return await switchTo(session, task, dir, wanted, from, bus, { reuseExisting: false });
85
+ return await switchTo(session, task, dir, wanted, from, bus, { reuseExisting: false, start });
86
+ }
87
+ /**
88
+ * The commit a session's first branch is cut from, by its `startFrom`, or why there is none.
89
+ *
90
+ * `previous-session` looks for the session whose work was committed last in this same repository —
91
+ * not the one listed before it, because a plan may put sessions in several repositories, and
92
+ * continuing a front end from a back end's branch would be nonsense. In a run of several sessions
93
+ * that is the one that ran just before; started on its own, it is the last one that worked here.
94
+ * When there is none, it starts from `baseBranch` and says so, rather than from wherever the
95
+ * repository happens to be.
96
+ */
97
+ export async function sessionStart(session, dir, head, allSessions) {
98
+ const how = session.vcs?.startFrom ?? 'head';
99
+ const baseBranch = session.vcs?.baseBranch?.trim() || 'main';
100
+ if (how === 'head')
101
+ return head ? { start: { kind: 'head', commit: head } } : {};
102
+ const fromBranch = async (note) => {
103
+ const tip = await branchTip(dir, baseBranch);
104
+ if (!tip) {
105
+ return {
106
+ problem: `this session is set to start from the local branch "${baseBranch}", and ${dir} has no such branch. ` +
107
+ 'Name the branch the work should start from (Version control → "Start from"), or create it.',
108
+ };
109
+ }
110
+ return { start: { kind: 'branch', commit: tip, branch: baseBranch, ...(note ? { note } : {}) } };
111
+ };
112
+ if (how === 'branch')
113
+ return await fromBranch();
114
+ const here = normalise(dir);
115
+ let best = null;
116
+ for (const other of await allSessions()) {
117
+ if (other.id === session.id || !other.vcs?.enabled || normalise(repoDirOf(other)) !== here)
118
+ continue;
119
+ const last = lastWorkOf(other);
120
+ if (last && (!best || last.at > best.at))
121
+ best = { session: other, ...last };
122
+ }
123
+ if (!best)
124
+ return await fromBranch(`no earlier session has committed work in this repository, so it starts from "${baseBranch}"`);
125
+ const tip = await branchTip(dir, best.branch);
126
+ if (!tip) {
127
+ return await fromBranch(`the branch of the previous session "${best.session.name}" (${best.branch}) is no longer in the repository, so it starts from "${baseBranch}"`);
128
+ }
129
+ return { start: { kind: 'previous-session', commit: tip, branch: best.branch, fromSession: { id: best.session.id, name: best.session.name } } };
130
+ }
131
+ /**
132
+ * The branch holding a session's latest work, and when that work ended: in `per-session` mode its
133
+ * one branch, in `per-task` mode the branch of the task that finished last — only tasks that
134
+ * committed something, since a branch with nothing on it is the session's start, not its work.
135
+ */
136
+ function lastWorkOf(session) {
137
+ let best = null;
138
+ for (const t of session.tasks) {
139
+ for (const v of [...(t.attempts ?? []).map((a) => ({ vcs: a.vcs, at: a.finishedAt })), { vcs: t.vcs, at: t.finishedAt }]) {
140
+ if (!v.vcs?.branch || !v.vcs.commit || !v.at)
141
+ continue;
142
+ if (!best || v.at > best.at)
143
+ best = { branch: v.vcs.branch, at: v.at };
144
+ }
145
+ }
146
+ return best;
147
+ }
148
+ async function branchTip(dir, branch) {
149
+ if (!(await isValidBranchName(dir, branch)))
150
+ return null;
151
+ const r = await git(dir, ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}^{commit}`]);
152
+ return r.ok && r.stdout.trim() ? r.stdout.trim() : null;
153
+ }
154
+ function normalise(dir) {
155
+ return dir.trim().replace(/[\\/]+$/, '').replace(/\//g, '\\').toLowerCase();
156
+ }
157
+ /** One line for the log and the event stream: where this session starts, and why. */
158
+ export function describeStart(start) {
159
+ const at = start.commit.slice(0, 8);
160
+ const said = start.kind === 'previous-session'
161
+ ? `this session continues "${start.fromSession?.name ?? '?'}": it starts from the end of its branch ${start.branch} (${at})`
162
+ : start.kind === 'branch'
163
+ ? `this session starts from the local branch ${start.branch} (${at})`
164
+ : `this session starts from where the repository was (${at})`;
165
+ return start.note ? `${said} — ${start.note}` : said;
69
166
  }
70
167
  /**
71
168
  * Which branch has the complete work of a session, when one does.
@@ -135,6 +232,7 @@ async function switchTo(session, task, dir, wantedName, from, bus, opts) {
135
232
  mode,
136
233
  base: vcs.baseCommit ? { commit: vcs.baseCommit, subject } : undefined,
137
234
  earlier,
235
+ start: opts.start,
138
236
  });
139
237
  return { vcs, note };
140
238
  }
@@ -167,12 +265,23 @@ export function noteFor(repoDir, vcs, ctx = { mode: 'per-task', earlier: [] }) {
167
265
  ? `Every task of this session gets its own branch from that same commit, so the work of the earlier tasks is NOT in your working tree: ` +
168
266
  `${ctx.earlier.map((e) => `"${e.title}" is on \`${e.branch}\``).join(', ')}. If this task depends on what one of them produced, say so in the summary rather than looking for files that are not here.`
169
267
  : 'Every task of this session gets its own branch from that same commit.';
268
+ /*
269
+ * Where the session began, said when it was chosen. A session that continues another has that
270
+ * session's files in its tree, and a model not told so reads them as something to redo or
271
+ * doubt; one that starts from `main` has none of them, and a model not told so goes looking.
272
+ */
273
+ const began = ctx.start?.kind === 'previous-session'
274
+ ? `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.`
275
+ : ctx.start?.kind === 'branch'
276
+ ? `This session started from the local branch \`${ctx.start.branch}\`, not from any earlier session's work${ctx.start.note ? ` (${ctx.start.note})` : ''}.`
277
+ : '';
170
278
  return [
171
279
  '## Version control',
172
280
  '',
173
281
  `The runner has already put ${repoDir} on the branch \`${vcs.branch}\`, created for this task from ${base},`,
174
282
  'and it will commit whatever you change when the task finishes.',
175
283
  '',
284
+ ...(began ? [began, ''] : []),
176
285
  standing,
177
286
  '',
178
287
  '- Do not create branches, switch branches, commit, stash, reset or revert. That is the',
@@ -225,6 +334,39 @@ export async function commitTaskResult(session, task, outcome, bus) {
225
334
  }
226
335
  return { ...current, commit: result.commit, commits, files, ...(suspicious.length > 0 ? { suspicious } : {}) };
227
336
  }
337
+ /**
338
+ * Commits what a task left behind when the process running it ended before it could.
339
+ *
340
+ * `commitTaskResult` runs when a task finishes, whatever the outcome, which covers every failure
341
+ * the runner itself decides. It does not cover the process going away mid-task — `npm start`
342
+ * stopped with Ctrl+C, a restart, a crash, the laptop shutting down. Then nothing committed the
343
+ * work: at the next start the task was only marked aborted, its changes stayed loose in the tree,
344
+ * and the next attempt found a dirty repository and ran with version control switched off — so
345
+ * the first attempt's work was neither on a branch nor carried forward, and one `git checkout .`
346
+ * from gone. Called at startup for each task the recovery pass closes.
347
+ *
348
+ * Only on the task's own branch. The runner created that branch for this task and the process
349
+ * that owned it is gone, so what is uncommitted there is the task's work; a repository that has
350
+ * since been moved to another branch is someone's deliberate act, and is left alone with a
351
+ * warning, as a dirty tree is left alone before a task.
352
+ */
353
+ export async function commitInterrupted(session, task, reason, bus) {
354
+ const settings = session.vcs;
355
+ const branch = task.vcs?.branch;
356
+ if (!settings?.enabled || !settings.commitOnFinish || !branch)
357
+ return undefined;
358
+ const dir = repoDirOf(session);
359
+ const state = await repoState(dir);
360
+ if (!state.isRepo || !state.dirty)
361
+ return undefined;
362
+ if (state.branch !== branch) {
363
+ bus.publish({ sessionId: session.id, taskId: task.id, type: 'vcs-problem', level: 'warn',
364
+ message: `"${task.title}" was interrupted with uncommitted changes, but ${dir} is now on ${state.branch ?? 'no branch'}, ` +
365
+ `not on the task's branch ${branch}; nothing was committed, so the changes are where they are` });
366
+ return undefined;
367
+ }
368
+ return await commitTaskResult(session, task, { status: 'aborted', reason }, bus);
369
+ }
228
370
  /**
229
371
  * A commit message that says what was asked and how it ended, in git's own shape.
230
372
  *
package/dist/web/404.html CHANGED
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html><!--QwjyyBjkO5xlKFU5O6Wux--><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/5a10c60adf9c5336.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-55627c5122ff6336.js" async=""></script><script src="/_next/static/chunks/549-2086c05135bc9753.js" async=""></script><script src="/_next/static/chunks/app/layout-92fa876afe619cf3.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>
1
+ <!DOCTYPE html><!--7TE2rEuHqGdGx14If8TEb--><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/5a10c60adf9c5336.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-0cd2c3cfd0ff0a45.js" async=""></script><script src="/_next/static/chunks/549-2086c05135bc9753.js" async=""></script><script src="/_next/static/chunks/app/layout-92fa876afe619cf3.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;
@@ -12,4 +12,4 @@
12
12
  d.dataset.focus = a.strongFocus ? 'strong' : 'normal';
13
13
  } catch (e) {}
14
14
  })();
15
- </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,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;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-55627c5122ff6336.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"themeScript\"]\n3:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-55627c5122ff6336.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"AppearanceProvider\"]\n4:I[867,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-55627c5122ff6336.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"LanguageProvider\"]\n5:I[1035,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-55627c5122ff6336.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"DialogHost\"]\n6:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-55627c5122ff6336.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"SkipLink\"]\n7:I[8197,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-55627c5122ff6336.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"\"]\n8:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-55627c5122ff6336.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"Nav\"]\n9:I[6657,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-55627c5122ff6336.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"GlobalApprovals\"]\na:I[5204,[],\"\"]\nb:I[2642,[],\"\"]\nc:I[433,[],\"OutletBoundary\"]\ne:I[9884,[],\"AsyncMetadataOutlet\"]\n10:I[433,[],\"ViewportBoundary\"]\n12:I[433,[],\"MetadataBoundary\"]\n13:\"$Sreact.suspense\"\n15:I[3536,[],\"\"]\n:HL[\"/_next/static/css/5a10c60adf9c5336.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"QwjyyBjkO5xlKFU5O6Wux\",\"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/5a10c60adf9c5336.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\"}]}]]}]]}]}]}]}]]}]]}],{\"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,[\"$\",\"$Lc\",null,{\"children\":[\"$Ld\",[\"$\",\"$Le\",null,{\"promise\":\"$@f\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L10\",null,{\"children\":\"$L11\"}],null],[\"$\",\"$L12\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$13\",null,{\"fallback\":null,\"children\":\"$L14\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$15\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"11:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nd:null\n"])</script><script>self.__next_f.push([1,"f:{\"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,"14:\"$f:metadata\"\n"])</script></body></html>
15
+ </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,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;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-0cd2c3cfd0ff0a45.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"themeScript\"]\n3:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-0cd2c3cfd0ff0a45.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"AppearanceProvider\"]\n4:I[867,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-0cd2c3cfd0ff0a45.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"LanguageProvider\"]\n5:I[1035,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-0cd2c3cfd0ff0a45.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"DialogHost\"]\n6:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-0cd2c3cfd0ff0a45.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"SkipLink\"]\n7:I[8197,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-0cd2c3cfd0ff0a45.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"\"]\n8:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-0cd2c3cfd0ff0a45.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"Nav\"]\n9:I[6657,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-0cd2c3cfd0ff0a45.js\",\"549\",\"static/chunks/549-2086c05135bc9753.js\",\"177\",\"static/chunks/app/layout-92fa876afe619cf3.js\"],\"GlobalApprovals\"]\na:I[5204,[],\"\"]\nb:I[2642,[],\"\"]\nc:I[433,[],\"OutletBoundary\"]\ne:I[9884,[],\"AsyncMetadataOutlet\"]\n10:I[433,[],\"ViewportBoundary\"]\n12:I[433,[],\"MetadataBoundary\"]\n13:\"$Sreact.suspense\"\n15:I[3536,[],\"\"]\n:HL[\"/_next/static/css/5a10c60adf9c5336.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"7TE2rEuHqGdGx14If8TEb\",\"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/5a10c60adf9c5336.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\"}]}]]}]]}]}]}]}]]}]]}],{\"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,[\"$\",\"$Lc\",null,{\"children\":[\"$Ld\",[\"$\",\"$Le\",null,{\"promise\":\"$@f\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L10\",null,{\"children\":\"$L11\"}],null],[\"$\",\"$L12\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$13\",null,{\"fallback\":null,\"children\":\"$L14\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$15\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"11:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\nd:null\n"])</script><script>self.__next_f.push([1,"f:{\"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,"14:\"$f:metadata\"\n"])</script></body></html>